you don't know js -- Scope and Closures学习笔记——第五章(闭包) 上篇
以下内容为自己看原版尝试做的翻译,仅当一个自己的看书记录,书中内容绝大部分都翻译了,但由于个人能力有限,建议各位看客不要迷信翻译的质量,推荐购买其英文原版学习观看。
到达此处,我们已经对作用域是如何工作的有了一个扎实的理解。
现在我们把注意力放到JavaScript的一个异常重要,但是又很晦涩,几近神话的部分:闭包。如果你跟着我们学习了词法作用域,那么闭包将会是显而易见,清楚明了的。
如果你还对词法作用域有疑问,在往下进行前可以翻回到第二章看看。
启蒙
对于这些已有JavaScript经验的但是可能从来没有完全掌握闭包概念的小伙伴来说,理解闭包就像是一个人必须努力和牺牲才能得到的涅槃。
【。。。作者的回忆。。。省略。。。】
在JavaScript中闭包就在你身边,你只需要识别和接受它。闭包并不是一个需要你学习新的语法和模式的特殊工具。
闭包是基于词法作用域写代码的结果。你甚至不需要为了充分利用它而故意的创造闭包。闭包在你的整个代码中被创建和使用。你缺少的是按你的需要,找到合理的上下文来认识,拥抱和利用闭包。
整个启蒙运动应该是:噢,闭包已经存在我的代码中,我终于可以看到它们了。理解闭包就像尼奥第一次看见矩阵一样。
细节(Nitty Gritty)
下面是一个关于闭包的直截了当的你需要知道的定义:
Closure is when a function is able to remember and access its lexical
scope even when that function is executing outside its lexical scope.
来看看上述定义的例子。
function foo(){
var a = 2;
function bar(){
console.log(a);
}
bar();
}
foo();
这段代码在我们讨论嵌套作用域时看起来应该非常熟悉。函数bar()
因为词法作用域的查找规则(这里是一个RHS引用查找)访问了外部作用域的变量a
。
这是一个闭包吗?
技术上来说。。。可能是的。但从我们上面说的你需要知道的定义来说。。。不全是。我认为最准确的方式来解释bar()
引用a
是通过词法作用域查找规则,并且这些规则仅仅是闭包的(一个重要!)部分。
从纯学术观点来说,就上面的代码段而言,函数bar()
有一个在foo()
上的闭包(甚至,在整个剩下的它能访问的作用域上,比如本例中的全局作用域)。
但,以这种方式定义的闭包不是很直观,我们也没在上面的代码段中看到闭包的运用。我们清楚的看到了词法作用域,但是闭包仍然是隐藏在代码后面的神秘影子。
我们来看看更加明显的闭包例子:
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 -- 哇,闭包出现了。
函数bar()
有可以访问foo()
内部作用域的词法作用域。然后我们将bar()
函数本身作为一个值传递。在这里,我们返回了bar
函数对象的一个引用。
在我们执行了foo()
函数后,我们将其返回的值(内部的bar()
函数)赋给了一个叫做baz
的变量,并且接下来调用了baz()
,这个baz()
通过一个不同的标识符引用,实际调用了内部的函数bar()
。
bar()
肯定被执行了。但是在这里,它在其申明的词法作用域外部被执行。
在foo()
被执行后,通常我们会期望整个foo()
内部的作用域都会消失,因为我们知道引擎有一个垃圾回收器,会出现和回收不再使用的内存。因此,由于foo()
将再也不会使用,看起来他会被很自然的回收。
但是闭包的“魔法”不允许这种情况出现。这个内部作用域实际上仍然被使用,因此并不会消失。谁在使用它?bar()
函数本身。
凭借它在何处被声明,bar()
在foo()
的内部作用域上有一个词法作用域闭包,为了对bar()
的引用,这使得这个内部作用域在任何时候都存在。
bar()
仍然有一个对那个作用域的引用,这个引用就被成为闭包。
所以,几微秒后,当变量baz
被调用(调用内部的bar
)时,它理所应当的有权访问author-time词法作用域,所以他可以访问变量a
就像我们期望的一样。
闭包使得函数能够继续访问在author time定义的词法作用域。
当然,任何将函数作为一个值传递且在其他地方调用的方式,都是应用闭包的例子。
function foo(){
var a = 2;
function baz(){
console.log(a); //2
}
bar(baz);
}
function bar(fn){
fn(); // 看,闭包。
}
我们将内部函数baz
传递给bar
,并且调用了这个内部函数(被标记为fn
),当我们这样做的时候,它在foo()
内部作用域上面的闭包在访问a
的时候就是可见的。
这些函数也可以间接的传递。
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 将baz赋给全局变量
}
function bar() {
fn(); // 看,闭包!
}
foo();
bar(); // 2
不管我们使用何种技术来将一个内部函数转移到它词法作用域的外面,他都将会维持一个其最初声明所在地方的作用域的引用,并且不管在何处执行它,这个闭包都会被运用。
现在我可以看见
前面的用于举例使用闭包的代码有点学院派作风。但我保证闭包在你已有的代码中肯定存在。让我们来见证一下。
function wait(message){
setTimeout(function timer(){
console.log(message);
}, 1000);
}
wait("hello, closure!");
我们使用了一个(名为timer
的)内部函数并且将它传递给setTimeout(..)
。但是timer
在wait(..)
作用域上有一个闭包,从而事实上拥有和使用了一个对于message
变量的引用。
在我们执行wait(..)
1000毫秒后,它的内部作用域本应该早就消失了,但timer()
仍然在其作用域上有一个闭包。
引擎的内建工具函数setTimeout(..)
有对一些参数的引用,可能被称为fn
或者func
或其他类似的。引擎转向调用这个函数,这里是调用我们的内部timer
函数,并且其词法作用域引用仍然是完整的。
闭包
或者,如果你是jQuery的信徒(或者其他JS框架):
function setupBot(name, selector){
$(selector).click(function activator(){
console.log("Activating: " + name);
});
}
setupBot("Closure Bot 1", "#bot_1");
setupBot("Closure Bot 2", "#bot_2");
我不确定你写的是哪种类型的代码,但我通常写能够完全控制整个全局作用域的闭包机器人军队,这绝对是真实的!
言归正传,本质上无论何时何地你将(可访问他们各自词法作用域)函数作为值到处传递,你很可能会看到这些函数正在运用闭包。因此,定时器,甚至handlers,Ajax请求,crosswindow messaging,web workers,或其他任何同步(或异步)任务,当你传递一个callback function的时候,做好吊打闭包的准备。
Chapter 3 introduced the IIFE pattern. While it is often said
that IIFE (alone) is an example of observed closure, I would
somewhat disagree, by our previous definition.
var a = 2;
(function IIFE(){
console.log(a);
})();
这段代码能工作,但是严格来说它不是一个明显的闭包。为什么?因为这个函数(这里名字是IIFE)没有在它的词法作用域外部被执行。它仍然在它被声明时的作用域内部被调用。a
是通过普通的词法作用域查找被找到,而不是通过闭包。
尽管一个IIEF本身并不是一个明显的闭包例子,它却创造了作用域,同时也是一个常用的用来创造作用域并随后就会被关闭的工具。因此,IIFE与闭包有密切关系,尽管可能他们自己不运用闭包。
循环和闭包
运用闭包的一个典范是循环。
for(var i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i);
}, i * 1000);
}
Linters often complain when you put functions inside of loops,
because the mistakes of not understanding closure are so common
among developers. We explain how to do so properly here,
leveraging the full power of closure. But that subtlety is often
lost on linters, and they will complain regardless, assuming you
don’t actually know what you’re doing.
上述代码段我们通常会期望最后的行为是数字1,2,...5会被打印出来,一次一个,每个间隔1秒。
实际上,如果你运行上面的代码,你会得到打印了5次的6,每次间隔1秒。
卧槽?
首先,我们来解释下6从哪来。循环的终止条件是当i
不再<=5
。第一次满足这个条件的时候,i
的值是6.因此,输出的结果反映了当循环终止的时候,i
的最终值。
当再看一眼的时候,这其实很明显。timeout的回调函数都在循环结束后才执行。实际上,就算每次循环里面的代码是setTimeout(.., 0)
,所以这些回调函数仍然会严格的在循环结束后执行,最终每次打印6
。
但这里有一个问题。我们的代码与我们在语义上暗示的比起来缺少了什么?
缺少的是我们试着在每次循环里实现“抓住”它对i
的拷贝。但,作用域的工作方式是,所有这5个函数,尽管他们在每次循环里被分别定义,都被包含在同一个共享的全局作用域,这个作用域实际上只有一个i
在里面。
按这种方式,当然所有的函数都共享一个对相同i
的引用。
回到我们的问题。缺少了什么?我们需要更多的闭包作用域。明确的说,对每次循环,我们都需要一个新的闭包作用域。
我们在第三章学过IIFE通过声明一个函数并立即执行来创造闭包。
我们试试:
for(var i = 1; i <= 5; i++){
(function(){
setTimeout(function timer(){
console.log(i);
}, i*1000);
})();
}
这样起作用么?试试。我等你。
我会为你结束悬念。不起作用为啥?我们现在明显有个多的词法作用域。每一个timeout回调函数的确在循环中通过IIFE创造了各自的循环作用域。
光有一个空的闭包作用域还不够。仔细看看。我们的IIFE其实是一个啥都不做的空作用域。他需要一些对我们有用的东西。
他需要自己的变量,这个变量在每次循环中有一个对i
的拷贝。
for (var i=1; i<=5; i++) {
(function(){
var j = i;
setTimeout( function timer(){
console.log( j );
}, j*1000 );
})();
}
成功了!
一个更好的小变种是:
for(var i = 1; i <= 5; i++){
(function(j){
setTimeout(function timer(){
console.log(j);
}, j*1000);
})(i);
}
当然,因为这些IIFE就是函数,我们可以传递i
给他,并且如果乐意的话我们可以将它称为j
,或者我们甚至仍然可以叫他i
。不管怎么说,代码正常了。
在循环中的IIFE的作用是为每次循环创建了一个新的作用域,这给我们的timeout回调函数机会来访问每次循环时传递的值。