前言
这篇文章使用有效的javascript代码向程序员们解释了闭包,大牛和功能型程序员请自行忽略。
基础篇
闭包(closure)是javascript的一大难点,也是它的特色。很多高级应用都要依靠闭包来实现。
---A.变量作用域
要理解闭包,首先要理解javascript的特殊的变量作用域。
变量的作用域无非就两种:全局变量和局部变量。
javascript语言的特别之处就在于:函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。
注意点:在函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明的是一个全局变量!
---B.变量作用域
出于种种原因,我们有时候需要获取到函数内部的局部变量。但是,上面已经说过了,正常情况下,这是办不到的!只有通过变通的方法才能实现。
那就是在函数内部,再定义一个函数。
function f1(){
var n=999;
function f2(){
alert(n); // 999
}
}
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。
这就是Javascript语言特有的"链式作用域"结构(chain scope),
子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!
----C.闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,不会在f1调用后被自动清除。
进阶篇
--两种方式概括:
- 闭包是javascript支持头等函数的一种方式,它是一个能够引用其内部作用域变量(在本作用域第一次声明的变量)的表达式,这个表达式可以赋值给某个变量,可以作为参数传递给函数,也可以作为一个函数返回值返回。
- 闭包是函数开始执行的时候被分配的一个栈帧,在函数执行结束返回后仍不会被释放(就好像一个栈帧被分配在堆里而不是栈里!)
下面这段代码返回了一个指向这个函数的引用:
function sayHello2(name) {
var text = 'Hello ' + name; // 局部变量text
var say = function() { console.log(text); }
return say;
}
var say2 = sayHello2('Bob');
say2(); // 打印日志: "Hello Bob"
绝大部分Javascript程序员能够理解上面代码中的一个函数引用是如何返回赋值给变量say2的,如果你不理解,那么你需要理解之后再来学习闭包。C语言程序员会认为这个函数返回一个指向某函数的指针,变量say和say2都是指向某个函数的指针。
Javascript的函数引用和C语言指针相比还有一个关键性的不同之处,在Javascript中,一个引用函数的变量可以看做是有两个指针,一个是指向函数的指针,一个是指向闭包的隐藏指针。
上面代码中就有一个闭包,为什么呢?因为匿名函数function() { console.log(text); }是在另一个函数(在本例中就是sayHello2()函数)声明的。在Javascript中,如果你在另一个函数中使用了function关键字,那么你就创建了一个闭包。
在C语言和大多数常用程序语言中,当一个函数返回后,函数内声明的局部变量就不能再被访问了,因为该函数对应的栈帧已经被销毁了。
在Javscript中,如果你在一个函数中声明了另一个函数,那么在你调用这个函数返回后里面的局部变量仍然是可以访问的。这个已经在上面的代码中演示过了,即我们在函数sayHello()返回后仍然可以调用函数say2()。注意:我们在代码中引用的变量text是我们在函数sayHello2()中声明的局部变量。
function() { console.log(text); } // 输出say2.toString();
观察say2.toString()的输出,我们可以看到确实引用了text变量。匿名函数之所以可以引用包含'Hello Bob'的text变量就是因为sayhello2()的局部变量被保存在了闭包中。
神奇的是,在JavaScript中,函数引用还有一个对于它所创建的闭包的秘密引用,类似于事件委托是一个方法指针加上对于某个对象的秘密引用。
--例子1
这个示例对于很多人来说都是一个挑战,所以希望你能弄懂它。注意:当你在一个循环里面定义一个函数的时候,闭包里的局部变量可能不会像你想的那样。
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]);
// 使用j是为了防止搞混---可以使用i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList() //输出 "item2 undefined" 3 次
result.push( function() {console.log(item + ' ' + list[i])}这一行给result数组添加了三次
函数匿名引用。如果你不熟悉匿名函数可以想象成下面代码:
pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);
注意,当你运行上述代码的时候会打印"item2 undefined"
三次!和前面的示例一样,和buildList的局部变量对应的闭包只有一个。当匿名函数在fnlist[j]()
这一行调用的时候,他们使用同一个闭包,而且是使用的这个闭包里i和item现在的值(循环结束后i的值为3,item的值为'item2')。注意:我们从索引0开始,所以item最后的值为item2',i的值会被i++增加到3 。
--例子2
这个例子表明了闭包会保存函数退出之前内部定义的所有的局部变量。注意:变量alice
是在匿名函数之前创建的。 匿名函数先被声明,然后当它被调用的时候之所以能够访问alice
是因为他们在同一个作用域内(JavaScript做了变量提升),sayAlice()()
直接调用了从sayAlice()
中返回的函数引用——这个和前面的完全一样,只是少了临时的变量【译者注:存储sayAlice()返回的函数引用的变量】
function sayAlice() {
var say = function() { console.log(alice); }
// 局部变量最后保存在闭包中
var alice = 'Hello Alice';
return say;
}
sayAlice()();// 输出"Hello Alice"
技巧:需要注意变量say也是在闭包内部,也能被在sayAlice()内部声明的其它函数访问,或者也可以在函数内部递归访问它。
--例子3
最后一个例子说明了每次调用函数都会为局部变量创建一个闭包。实际上每次函数声明并不会创建一个单独的闭包,但每次调用函数都会创建一个独立的闭包。
function newClosure(someNum, someRef) {
// 局部变量最终保存在闭包中
var num = someNum;
var anArray = [1,2,3];
var ref = someRef;
return function(x) {
num += x;
anArray.push(num);
console.log('num: ' + num +
'\nanArray ' + anArray.toString() +
'\nref.someVar ' + ref.someVar);
}
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
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;
总结
- 每当你在另一个函数里使用了关键字function,一个闭包就被创建了
- 每当你在一个函数内部使用了eval(),一个闭包就被创建了。在eval内部你可以引用外部函数定义的局部变量,同样的,在eval内部也可以通过eval('var foo = …')来创建新的局部变量。
- 当你在一个函数内部使用new function(...)(即构造函数)时,它不会创建闭包(新函数不能引用外部函数的局部变量)。
- JavaScript中的闭包,就像一个副本,将某函数在退出时候的所有局部变量复制保存其中。
- 也许最好的理解是闭包总是在进入某个函数的时候被创建,而局部变量是被加入到这个闭包中。
- 闭包函数每次被调用的时候都会创建一组新的局部变量存储。(前提是这个函数包含一个内部的函数声明,并且这个函数的引用被返回或者用某种方法被存储到一个外部的引用中)
- 两个函数或许从源代码文本上看起来一样,但因为隐藏闭包的存在会让两个函数具有不同的行为。我认为Javascript代码实际上并不能找出一个函数引用是否有闭包。
- 如果你正尝试做一些动态源代码的修改(例如:myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));
),如果myFunction
是一个闭包的话,那么这并不会生效(当然,你甚至可能从来都没有在运行的时候考虑过修改源代码字符串,但是。。。)。 - 在函数内部的函数的内部声明函数是可以的——可以获得不止一个层级的闭包。
- 通常我认为闭包是一个同时包含函数和被捕捉的变量的术语,但是请注意我并没有在本文中使用这个定义。
- 我觉得JavaScript中的闭包跟其它函数式编程语言中的闭包是有不同之处的。