前言: 在这篇文章里,我将对那些在各种有关闭包的资料中频繁出现,但却又千篇一律,且暧昧模糊得让人难以理解的表述,做一次自己的解读。或者说是对“红宝书”的《函数表达式/闭包》的那一章节所写的简洁短小的描述,做一些自己的注解,仅供抛砖引玉
好,看到文章标题,你就应该知道我下文的画风是怎样的了,嘿嘿嘿...
闭包的概念
首先要搞懂的就是闭包的概念: 闭包是能够访问另一个函数作用域中变量的函数(这个“另外一个函数”,通常指的是包含闭包函数的外部函数), 例如:
function outerFunction () { var a = 1 return function () { console.log(a); } } var innerFunction = outerFunction(); innerFunction();
在这个例子里:负责打印a的匿名函数被包裹在外部函数outerFunction里面,且访问了外部函数outerFunction作用域里的变量a,所以从定义上说,它是一个闭包。
我在标题上说过我要讲故事的对吧,但... 在听故事前,你需要先看以完下两个方面的知识:
1. 谈谈函数执行环境,作用域链以及变量对象
2. 闭包和函数柯里化
谈谈函数执行环境,作用域链以及变量对象
(作用域和执行环境其实是同一个概念,我下面的介绍主要会以后者为名)
首先我想让大家理解的是: 函数执行环境,作用域链以及变量对象的相互关系以及各自作用
先引用一下《javaScript高级语言程序》中的两段原话:
1. "当某个函数被调用时,会创建一个执行环境 (execution context)及相应的作用域链(scope Chain)" — —第178页 7.2 闭包
2. "每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中" — — 第73页 4.2 执行环境及其作用域
这是我在“红宝书”上所能找到的最关键的一句话,但看完后,我。。。。一脸懵逼!!!! 现在我知道了函数被调用的时候就会连带产生和这个函数息息相关的三个东东:
执行环境(execution context),作用域链(scope Chain)以及变量对象(variable object),但这三者们具体是什么关系呢?
后来我看了汤姆大叔的文章,顿时豁然开朗: (文末有相关链接)
下面贴出他写的伪代码:
ExecutionContext = { variableObject: { .... }, this: thisValue, Scope: [ // Scope chain // 所有变量对象的列表 ] };
所以说,关于三者,更准确的描述或许是这样的: 在函数调用的时候,会创建一个函数的执行环境,这个执行环境有一个与之对应的变量对象和作用域链。
嗯,这下三者的关系应该就比较明朗了吧(虽然好像也并没有什么卵用。。)
所以说,下面我要介绍的是变量对象和作用域链的作用。
变量对象的作用:
每个函数的变量对象保存了它所拥有的数据,以供函数内部访问和调用,这些数据包括:(位于执行环境内部的)
1.声明变量
2.声明函数
3.接收参数
”虽然我们编写的代码无法访问到这个对象,但解析器还处理数据的时候会在后台使用它“
例如:
function foo (arg) { var variable = ’我是变量‘; function innerFoo () { alert("我是彭湖湾") } } foo('我是参数');
这个时候执行环境对应的变量对象就变成了这样:
ExecutionContext = { variableObject: { variable:’我是变量‘ innerFoo: [对函数声明innerFoo的引用] arg: '我是参数' }, this: thisValue, Scope: [ // Scope chain // 所有变量对象的列表 ] };
作用域链的作用
通过作用域链,函数能够访问来自它上层作用域(执行环境)中的变量
先看一个例子
function foo () { var a = 1; function innerFoo () { console.log(a) } innerFoo(); } foo(); // 打印 1
在这里,变量a并不是innerFoo作用域(执行环境)内声明的变量呀,为什么能够取到它外部函数foo作用域内的变量呢? 这就是作用域链的作用啦,现在的执行环境用汤姆大叔的伪代码描述是这样的:
InnerFoo函数的执行环境:
InnerFooExecutionContext = { variableObject: { }, this: thisValue, Scope: [ // Scope chain innerFooExecutionContext. variableObject, // innerFoo的变量对象 FooExecutionContext.variableObject, // Foo的变量对象 globalContext.variableObject // 全局执行环境window的变量对象 ] };
Foo函数的执行环境:
FooExecutionContext = { variableObject: { a: 1 }, this: thisValue, Scope: [ // Scope chain FooExecutionContext.variableObject, // Foo的变量对象 globalContext.variableObject // 全局执行环境window的变量对象 ] };
你可以看到,作用域链其实就是个从当前函数的变量对象开始,从里到外取出所有变量对象,组成的一个列表。通过这个作用域链列表,就可以实现对上层作用域的访问。
innerFoo在自己的执行环境的变量对象中没有找到 a 的变量声明, 它感到很苦恼,但转念一想: 诶! 我可以向上层函数执行环境的变量对象(variableObject)中找嘛! 于是乎沿着作用域链( Scope chain)攀爬,往上找变量a,幸运的是,在父函数Foo的变量对象,它找到了自己需要的变量a
“啊! 找到a了! 它的值是1”
如果今天innerFoo恰逢水逆,没有在Foo的变量对象中找到a呢? 那么它会沿着作用域链继续向上“攀爬',直到它到达全局执行环境window(global)
闭包和函数柯里化
闭包和函数柯里化在定义一个函数的时候,可能会使用到多层嵌套的闭包,这种用法,叫做“柯里化”。 而闭包柯里化有两大作用:参数累加和延迟调用
例子:
function foo (a) { return function (b) { return function (c) { console.log(a + b + c); } } }
foo('我')('叫')('彭湖湾'); // 打印 我叫彭湖湾
从这里,我们可以很直观地看出闭包柯里化的时候参数累加的作用
我们把上面那个例子改变一下:
function foo (a) { return function (b) { return function (c) { console.log(a + b + c); } } } var foo1 = foo('我'); var foo2 = foo1('叫'); foo2('彭湖湾'); // 打印 我叫彭湖湾
可以看到,最内层的闭包在外层函数foo和foo1调用的时候都没有调用,直到最后得到foo2并调用foo2()的时候,这个最内层的闭包才得到执行, 这也是闭包的一大特性——延迟执行
好,如果你看完了以上两个方面的内容,那接下来就可以听我将故事啦。
闭包造成的额外的内存占用 (注意我说的不是“内存泄漏”!)
函数的变量对象一般在函数调用结束后被销毁(它的“任务”已经完成了,可以被垃圾回收了)
但闭包的情况却不同
function foo (a) { return function () { console.log(a) } } var foo1 = foo(1); var foo2 = foo(2); var foo3 = foo(3); foo1(); // 输出1 foo2(); // 输出2 foo3(); // 输出3
实际上,foo函数调用结束后, foo函数的变量对象并不会被立即销毁,而是只有当取得foo函数闭包的值的foo1, foo2, foo3调用结束, 这三个函数的变量对象和作用域链被销毁后, foo函数才算“完成任务”,这时,它才能被销毁。
所以说,闭包会造成额外的内存占用(注意这种内存占用是有必要的,和内存泄漏不同!!)
如果你不是很明白。看看我下面这个故事:
故事: 有这么一个差异化明显的班级,班级成员由一个学霸和一堆学渣组成,在某次监管很宽松的测验中(老师不在) , 为了其他人能够不去教导处喝茶,非常老好人的学霸用10分钟做完了试卷后,把卷子给全班同学抄, 弘扬了中华民族一贯以来的团结和谐,共同奋斗的精神。。。。
这个外层函数,就是那个学霸;
里面的闭包,就是那些学渣;
闭包所引用的外层函数的变量,就是学霸递给学渣们的试卷!!!!!
问:
学霸10分钟就做完了试卷,那为什么他一整节课都忙的满头大汗???(为什么外层函数的变量对象在外层函数调用完毕之后没有立即销毁???)
答案:
因为他要忙着给其他同学们传递他做好的试卷,又因为他是个老好人,所以只有最后一个同学做完试卷后,这位善良“负责”的学霸才能休息 呀!!!!!!!(因为闭包通过作用域链还保留着对这个外部函数的变量对象的引用,所以外部函数并不能立即得到销毁)
闭包只能取得包含函数的最后一个值
让我们来看看《红宝书》闭包那一章节中的一个典型例子:
function createArray() { var arr = new Array(); for (var i = 0; i < 10; i++) { arr[i] = function () { return i; } } return arr; } var funcs = createArray(); for (var i = 0; i < funcs.length; i++) { document.write(funcs[i]() + "
"); }
实际上,最后输出的不是1,2,3,4,5,6,7 。。10,而是全部都是10,为什么? 因为:
1. 这几个函数都保留着对同一个外部函数的变量对象的引用
2. 因为闭包函数“延迟调用”的特性,而关键变量值i的获取是在闭包函数调用(f也即uncs[i]())的时候才从外部函数的变量对象中获取,而这个时候,外部函数早就完成for循环使 i =10了 !!!
还不太理解的话看我接下来的这个故事:
改完卷子后, 老师把除了学霸以外的所有同学叫到办公室:为什么你们的答案TM都是一样的???
在这之前我再附加一个现实场景: 学霸虽然学力无穷,但对一些比较难的题目,也不是一下子就能答对的,比如下面这道选择题:
请问中国最富盛名的博客社区是以下哪个?
A: 博客园 B: CSDN C:51CTO D: JB之家
但学霸不知道哪根筋断了选了B, 后来变本加厉改为C, 最后无可救药的改为D,但最后学霸发现做这道题的时候自己犯了万分之一的脑子进水的概率,于是把前面的答案都涂掉了,重新选为A !!
问题: 学霸做这道题的时候,他先后选了A—>B—>C—>D—>A; 那么!!!为什么全班人都不是:“有的选A有的选B有的选C有的选D”, 而是全部选了A呢?
答案:
因为!! 学霸把试卷全班传阅的时候
1.其他人参考的只有学霸那唯一一张试卷(唯一一章是重点,划起来呀!!)
2.其他人抄的时候,学霸已经做完了!做完了!做完了!(重要的事情说三遍)所以那道选择题其他人只能看到他最后选的A,而不是B,C,D!!
参考书籍或文章:
1.《javaScrpt高级语言程序设计》
2. 深入理解JavaScript系列(12):变量对象(Variable Object) ——汤姆大叔 http://www.cnblogs.com/TomXu/archive/2012/01/16/2309728.html
3.深入理解JavaScript系列(14):作用域链(Scope Chain) http://www.cnblogs.com/TomXu/archive/2012/01/18/2312463.html