面向初学者的JavaScript关闭
Morris在2006年2月2日星期二提交。从此开始由社区编辑。
关闭不是魔术
本页介绍了闭包,以便程序员可以使用有效的JavaScript代码来理解闭包。它不适用于专家或功能性程序员。
一旦核心概念浮出水面,关闭就不难理解。但是,通过阅读任何理论或学术上的解释是不可能理解它们的!
本文面向具有某种主流语言编程经验并且可以阅读以下JavaScript函数的程序员:
function sayHello(name) {
var text = 'Hello ' + name;
var say = function() { console.log(text); }
say();
}
sayHello('Joe');
两篇摘要
当函数(
foo
)声明其他函数(bar和baz)时,在函数退出时,不会破坏在
foo
中创建的局部变量系列。这些变量只是对外界不可见。因此,
foo
可以巧妙地返回函数
bar
和
baz
,并且它们可以继续通过这个封闭的变量家族(“闭包”)彼此进行读取,写入和通信,而其他人则无法干预,甚至没有将来再打
foo
的人。
闭包是支持
first-class functions的一种方式。它是一个表达式,可以引用其范围内的变量(首次声明时),分配给变量,作为参数传递给函数或作为函数结果返回。
闭包的例子
以下代码返回对函数的引用:
function sayHello2(name) {
var text = 'Hello ' + name; // Local variable
var say = function() { console.log(text); }
return say;
}
var say2 = sayHello2('Bob');
say2(); // logs "Hello Bob"
大多数JavaScript程序员将理解上面代码中如何将对函数的引用返回到变量(
say2
)。如果不这样做,那么您需要先研究一下闭包。使用C的程序员会认为函数是返回函数的指针,而变量
say
和
say2
都是函数的指针。
指向函数的C指针和指向函数的JavaScript引用之间存在关键区别。在JavaScript中,您可以认为函数引用变量既具有指向函数的指针又具有指向闭包的隐藏指针。
上面的代码有一个闭包,因为匿名函数
function() { console.log(text); }
是在另一个函数(在此示例中为
sayHello2()
)内声明的。在JavaScript中,如果在另一个函数中使用
function
关键字,则将创建一个闭包。
在C语言和大多数其他常见语言中,函数返回后,所有本地变量将不再可访问,因为堆栈框架被破坏了。
在JavaScript中,如果您在另一个函数中声明一个函数,则外部函数的本地变量在返回后仍可访问。上面已经说明了这一点,因为从
say2()
返回后我们调用了函数
sayHello2()
。注意,我们调用的代码引用了变量
text
,它是函数
sayHello2()
的局部变量。
function() { console.log(text); } // Output of say2.toString();
查看
say2.toString()
的输出,可以看到该代码引用了变量
text
。匿名函数可以引用保存值
text
的
'Hello Bob'
,因为
sayHello2()
的局部变量已秘密地保持在闭包中。
天才之处在于,在JavaScript中,函数引用还具有对其创建的闭包的秘密引用—类似于委托如何成为方法指针以及对对象的秘密引用。
更多例子
出于某种原因,当您阅读闭包时,似乎真的很难理解它,但是当您看到一些示例时,很清楚它们是如何工作的(花了我一段时间)。
我建议仔细研究这些示例,直到您理解它们的工作原理。如果您在不完全了解闭包如何使用的情况下开始使用闭包,那么您很快就会创建一些非常奇怪的错误!
例子3
此示例显示局部变量未复制-通过引用保留它们。似乎即使外部函数退出后,堆栈框架仍在内存中保持活动状态!
function say667() {
// Local variable that ends up within closure
var num = 42;
var say = function() { console.log(num); }
num++;
return say;
}
var sayNumber = say667();
sayNumber(); // logs 43
例子4
这三个全局函数都对同一闭包有共同的引用,因为它们都在对
setupSomeGlobals()
的单个调用中声明。
var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
// Local variable that ends up within closure
var num = 42;
// Store some references to functions as global variables
gLogNumber = function() { console.log(num); }
gIncreaseNumber = function() { num++; }
gSetNumber = function(x) { num = x; }
}
setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5
var oldLog = gLogNumber;
setupSomeGlobals();
gLogNumber(); // 42
oldLog() // 5
这三个函数具有对同一闭包的共享访问权限-定义这三个函数时,
setupSomeGlobals()
的局部变量。
请注意,在上面的示例中,如果再次调用
setupSomeGlobals()
,则会创建一个新的闭包(堆栈框架!)。旧的
gLogNumber
,
gIncreaseNumber
,
gSetNumber
变量将被具有新闭包的新函数覆盖。 (在JavaScript中,每当在另一个函数中声明一个函数时,每次调用外部函数时都会重新创建一个或多个内部函数。)
例子5
此示例显示闭包包含退出前在外部函数内部声明的任何局部变量。请注意,变量
alice
实际上是在匿名函数之后声明的。首先声明匿名函数,并在调用该函数时可以访问
alice
变量,因为
alice
在同一作用域中(JavaScript在
variable hoisting内)。
同样,
sayAlice()()
只是直接调用从
sayAlice()
返回的函数引用-它与之前所做的完全相同,但没有临时变量。
function sayAlice() {
var say = function() { console.log(alice); }
// Local variable that ends up within closure
var alice = 'Hello Alice';
return say;
}
sayAlice()();// logs "Hello Alice"
棘手:请注意,
say
变量也位于闭包内部,可以由在
sayAlice()
中声明的任何其他函数访问,或者可以在内部函数中递归访问。
例子6
对于许多人来说,这是一个真正的陷阱,因此您需要了解它。如果要在循环中定义函数,请非常小心:闭包中的局部变量可能不会像您首先想到的那样起作用。
您需要了解Javascript中的“变量提升”功能才能了解此示例。
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + i;
result.push( function() {console.log(item + ' ' + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1,2,3]);
// Using j only to help prevent confusion -- could use i.
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList() //logs "item2 undefined" 3 times
result.push( function() {console.log(item + ' ' + list[i])}
行将对匿名函数的引用添加到结果数组三次。如果您对匿名函数不太熟悉,可以考虑一下:
pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);
请注意,在运行示例时,
"item2 undefined"
将被记录3次!这是因为与前面的示例一样,
buildList
的局部变量(
result
,
i
,
list
和
item
)只有一个闭包。在
fnlist[j]()
行上调用匿名函数时;它们都使用相同的单个闭包,并且使用该闭包内
i
和
item
的当前值(其中
i
的值为
3
,因为循环已完成,而
item
的值为
'item2'
的值)。请注意,我们从0开始索引,因此
item
的值为
item2
。 i ++将把
i
递增到值
3
。
如果使用变量
item
的块级声明(通过
let
关键字)而不是通过函数
var
进行函数范围的变量声明,则可能会有所帮助。如果进行了更改,则数组
result
中的每个匿名函数都有其自己的闭包。运行示例时,输出如下:
item0 undefined
item1 undefined
item2 undefined
如果还使用
i
而不是
let
定义了变量
var
,则输出为:
item0 1
item1 2
item2 3
例子7
在最后一个示例中,对主函数的每次调用都会创建一个单独的闭包。
function newClosure(someNum, someRef) {
// Local variables that end up within closure
var num = someNum;
var anArray = [1,2,3];
var ref = someRef;
return function(x) {
num += x;
anArray.push(num);
console.log('num: ' + num +
'; anArray: ' + anArray.toString() +
'; ref.someVar: ' + ref.someVar + ';');
}
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj); // attention here: new closure assigned to a new variable!
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
摘要
如果一切似乎都不是很清楚,那么最好的办法就是尝试这些示例。阅读说明比理解示例困难得多。
我对闭包和堆栈框架等的解释在技术上不正确-它们是旨在帮助理解的粗略简化。一旦了解了基本概念,您便可以稍后进行详细了解。
最后一点:
每当在JavaScript中声明函数时,都会创建一个闭包。
从另一个函数内部返回
function
是闭包的经典示例,因为即使在外部函数完成执行之后,外部函数内部的状态也隐式地可用于返回的内部函数。这有许多用例,包括模拟私有变量。
每当在函数内部使用
eval()
时,都会使用闭包。您
eval
的文本可以引用该函数的局部变量,并且在
eval
中甚至可以使用
eval('var foo = …')
创建新的局部变量
当在函数内部使用
new Function(…)
(
Function constructor)时,它不会关闭其词法环境:而是关闭全局上下文。 (新函数不能引用外部函数的局部变量。)
JavaScript中的闭包就像在函数声明时保留对范围的引用(不是副本)...以及指向嵌套在该范围之外的所有范围的链接。
声明函数时创建一个闭包。当调用函数时,此闭包用于配置执行上下文。
每次调用函数时,都会保留一组新的局部变量(假设该函数内部包含一个函数声明,并且将返回对该内部函数的引用,或者以某种方式为其保留外部引用)。
两个函数可能看起来像具有相同的源文本,但是由于它们的“隐藏”关闭而具有完全不同的行为。我认为JavaScript代码实际上无法找出函数引用是否具有闭包。
如果您尝试进行任何动态源代码修改(例如:
myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));
),则如果
myFunction
是闭包,则将无法正常工作(当然,您甚至都不会想到在运行时进行源代码字符串替换,但...)。
可以将函数声明嵌套在函数声明内的函数声明中……;您可以获得多个级别的关闭。
我认为通常闭包是函数和捕获的变量环境的统称。请注意,我不在本文中使用该定义!
我怀疑JavaScript中的闭包与功能语言中的闭包不同。
链接
道格拉斯·克罗克福德(Douglas Crockford)使用闭包为对象模拟了
private attributes and private methods。
很好的解释了如果不小心的话,闭包如何
cause memory leaks in IE。
谢谢
如果您刚刚学习了闭包(在这里或其他地方!),那么我对您可能会建议使本文更清晰的任何更改所产生的反馈意见感兴趣。发送电子邮件至morrisjohns.com(morris_closure @)。请注意,我不是JavaScript专家,也不是闭包专家。
莫里斯(Morris)的原始文章可以在
Internet Archive中找到。
我是一名优秀的程序员,十分优秀!