本章咱们将介绍在JavaScript里你们经常来讨论的话题 —— 闭包(closure)。闭包事实上你们都已经谈烂了。虽然如此,这里仍是要试着从理论角度来讨论下闭包,看看ECMAScript中的闭包内部究竟是怎样工做的。html
正如在前面的文章中提到的。这些文章都是系列文章,相互之间都是有关联的。所以。为了更好的理解本文要介绍的内容,建议先去阅读第14章做用域链和第12章变量对象。算法
英文原文:http://dmitrysoshnikov.com/ecmascript/chapter-6-closures/
在直接讨论ECMAScript闭包以前,仍是有必要来看一下函数式编程中一些基本定义。编程
众所周知,在函数式语言中(ECMAScript也支持这样的风格),函数便是数据。就例如说,函数可以赋值给变量,可以当參数传递给其它函数。还可以从函数里返回等等。这类函数有特殊的名字和结构。数组
A functional argument (“Funarg”) — is an argument which value is a function. 函数式參数(“Funarg”) —— 是指值为函数的參数。
样例:闭包
function exampleFunc(funArg) { funArg(); } exampleFunc(function () { alert('funArg'); });
上述样例中funarg的实际參数事实上是传递给exampleFunc的匿名函数。app
反过来,接受函数式參数的函数称为高阶函数(high-order function 简称:HOF)。还可以称做:函数式函数或者偏数理或操做符。上述样例中。exampleFunc 就是这种函数。ecmascript
此前提到的,函数不只可以做为參数。还可以做为返回值。这类以函数为返回值的函数称为带函数值的函数(functions with functional value or function valued functions)。编程语言
(function functionValued() { return function () { alert('returned function is called'); }; })()();
可以以正常数据形式存在的函数(例如说:当參数传递。接受函数式參数或者以函数值返回)都称做 第一类函数(通常说第一类对象)。在ECMAScript中。所有的函数都是第一类对象。函数式编程
函数可以做为正常数据存在(好比:当參数传递,接受函数式參数或者以函数值返回)都称做第一类函数(通常说第一类对象)。函数
在ECMAScript中。所有的函数都是第一类对象。
接受本身做为參数的函数,称为自应用函数(auto-applicative function 或者 self-applicative function):
(function selfApplicative(funArg) { if (funArg && funArg === selfApplicative) { alert('self-applicative'); return; } selfApplicative(selfApplicative); })();
以本身为返回值的函数称为自复制函数(auto-replicative function 或者 self-replicative function)。
一般,“自复制”这个词用在文学做品中:
(function selfReplicative() { return selfReplicative; })();
自复制函数的当中一个比較有意思的模式是让仅接受集合的一个项做为參数来接受从而取代接受集合自己。
// 接受集合的函数 function registerModes(modes) { modes.forEach(registerMode, modes); } // 使用方法 registerModes(['roster', 'accounts', 'groups']); // 自复制函数的声明 function modes(mode) { registerMode(mode); // 注冊一个mode return modes; // 返回函数自身 } // 使用方法,modes链式调用 modes('roster')('accounts')('groups') //有点相似:jQueryObject.addClass("a").toggle().removClass("b")
但直接传集合用起来相对来讲,比較有效并且直观。
在函数式參数中定义的变量,在“funarg”激活时就行訪问了(因为存储上下文数据的变量对象每次在进入上下文的时候就建立出来了):
function testFn(funArg) { // funarg激活时, 局部变量localVar可以訪问了 funArg(10); // 20 funArg(20); // 30 } testFn(function (arg) { var localVar = 10; alert(arg + localVar); });
然而。咱们从第14章知道,在ECMAScript中,函数是可以封装在父函数中的。并可以使用父函数上下文的变量。这个特性会引起funarg问题。
在面向堆栈的编程语言中,函数的局部变量都是保存在栈上的,每当函数激活的时候,这些变量和函数參数都会压入到该堆栈上。
当函数返回的时候,这些參数又会从栈中移除。这样的模型对将函数做为函数式值使用的时候有很是大的限制(例如说,做为返回值从父函数中返回)。绝大部分状况下,问题会出现在当函数有自由变量的时候。
自由变量是指在函数中使用的。但既不是函数參数也不是函数的局部变量的变量
样例:
function testFn() { var localVar = 10; function innerFn(innerParam) { alert(innerParam + localVar); } return innerFn; } var someFn = testFn(); someFn(20); // 30
上述样例中,对于innerFn函数来讲,localVar就属于自由变量。
对于採用面向栈模型来存储局部变量的系统而言。就意味着当testFn函数调用结束后,其局部变量都会从堆栈中移除。
这样一来。当从外部对innerFn进行函数调用的时候,就会错误发生(因为localVar变量已经不存在了)。
而且。上述样例在面向栈实现模型中,要想将innerFn以返回值返回根本是不可能的。因为它也是testFn函数的局部变量,也会随着testFn的返回而移除。
另外一个问题是当系统採用动态做用域。函数做为函数參数使用的时候有关。
看例如如下样例(伪代码):
var z = 10; function foo() { alert(z); } foo(); // 10 – 使用静态和动态做用域的时候 (function () { var z = 20; foo(); // 10 – 使用静态做用域, 20 – 使用动态做用域 })(); // 将foo做为參数的时候是同样的 (function (funArg) { var z = 30; funArg(); // 10 – 静态做用域, 30 – 动态做用域 })(foo);
咱们看到,採用动态做用域。变量(标识符)的系统是经过变量动态栈来管理的。
所以,自由变量是在当前活跃的动态链中查询的,而不是在函数建立的时候保存起来的静态做用域链中查询的。
这样就会产生冲突。例如说。即便Z仍然存在(与以前从栈中移除变量的样例相反),仍是会有这样一个问题: 在不一样的函数调用中,Z的值究竟取哪一个呢(从哪一个上下文,哪一个做用域中查询)?
上述描写叙述的就是两类funarg问题 —— 取决因而否将函数以返回值返回(第一类问题)以及是否将函数当函数參数使用(第二类问题)。
为了解决上述问题。就引入了 闭包的概念。
闭包是代码块和建立该代码块的上下文中数据的结合。
让咱们来看如下这个样例(伪代码):
var x = 20; function foo() { alert(x); // 自由变量"x" == 20 } // 为foo闭包 fooClosure = { call: foo // 引用到function lexicalEnvironment: {x: 20} // 搜索上下文的上下文 };
上述样例中。“fooClosure”部分是伪代码。相应的,在ECMAScript中,“foo”函数已经有了一个内部属性——建立该函数上下文的做用域链。
“lexical”通常是省略的。上述样例中是为了强调在闭包建立的同一时候,上下文的数据就会保存起来。当下次调用该函数的时候,自由变量就可以在保存的(闭包)上下文中找到了,正如上述代码所看到的,变量“z”的值老是10。
定义中咱们使用的比較广义的词 —— “代码块”,然而。一般(在ECMAScript中)会使用咱们经常用到的函数。固然了,并不是所有对闭包的实现都会将闭包和函数绑在一块儿。例如说,在Ruby语言中,闭包就有多是: 一个过程对象(procedure object), 一个lambda表达式或者是代码块。
对于要实现将局部变量在上下文销毁后仍然保存下来,基于栈的实现显然是不适用的(因为与基于栈的结构相矛盾)。所以在这样的状况下,上层做用域的闭包数据是经过 动态分配内存的方式来实现的(基于“堆”的实现)。配合使用垃圾回收器(garbage collector简称GC)和 引用计数(reference counting)。
这样的实现方式比基于栈的实现性能要低,然而,不论什么一种实现老是可以优化的: 可以分析函数是否使用了自由变量,函数式參数或者函数式值。而后依据状况来决定 —— 是将数据存放在堆栈中仍是堆中。
讨论完理论部分。接下来让咱们来介绍下ECMAScript中闭包究竟是怎样实现的。这里仍是有必要再次强调下:ECMAScript仅仅使用静态(词法)做用域(而诸如Perl这种语言。既可以使用静态做用域也可以使用动态做用域进行变量声明)。
var x = 10; function foo() { alert(x); } (function (funArg) { var x = 20; // 变量"x"在(lexical)上下文中静态保存的,在该函数建立的时候就保存了 funArg(); // 10, 而不是20 })(foo);
技术上说,建立该函数的父级上下文的数据是保存在函数的内部属性 [[Scope]]中的。
假设你还不了解什么是[[Scope]],建议你先阅读第14章, 该章节对[[Scope]]做了很具体的介绍。
假设你对[[Scope]]和做用域链的知识全然理解了的话,那对闭包也就全然理解了。
依据函数建立的算法,咱们看到 在ECMAScript中,所有的函数都是闭包,因为它们都是在建立的时候就保存了上层上下文的做用域链(除开异常的状况) (不管这个函数兴许是否会激活 —— [[Scope]]在函数建立的时候就有了):
var x = 10; function foo() { alert(x); } // foo是闭包 foo: <FunctionObject> = { [[Call]]: <code block of foo>, [[Scope]]: [ global: { x: 10 } ], ... // 其余属性 };
如咱们所说,为了优化目的,当一个函数没有使用自由变量的话,实现可能不保存在反作用域链里。只是,在ECMA-262-3规范里不论什么都没说。所以,正常来讲,所有的參数都是在建立阶段保存在[[Scope]]属性里的。
有些实现中。赞成对闭包做用域直接进行訪问。比方Rhino,针对函数的[[Scope]]属性,相应有一个非标准的 __parent__属性,在第12章中做过介绍:
var global = this; var x = 10; var foo = (function () { var y = 20; return function () { alert(y); }; })(); foo(); // 20 alert(foo.__parent__.y); // 20 foo.__parent__.y = 30; foo(); // 30 // 可以经过做用域链移动到顶部 alert(foo.__parent__.__parent__ === global); // true alert(foo.__parent__.__parent__.x); // 10
这里还要注意的是:在ECMAScript中。同一个父上下文中建立的闭包是共用一个[[Scope]]属性的。也就是说,某个闭包对当中[[Scope]]的变量作改动会影响到其它闭包对其变量的读取:
这就是说:所有的内部函数都共享同一个父做用域
var firstClosure; var secondClosure; function foo() { var x = 1; firstClosure = function () { return ++x; }; secondClosure = function () { return --x; }; x = 2; // 影响 AO["x"], 在2个闭包公有的[[Scope]]中 alert(firstClosure()); // 3, 经过第一个闭包的[[Scope]] } foo(); alert(firstClosure()); // 4 alert(secondClosure()); // 3
关于这个功能有一个很广泛的错误认识。开发者在循环语句里建立函数(内部进行计数)的时候经常得不到预期的结果,而指望是每个函数都有本身的值。
var data = []; for (var k = 0; k < 3; k++) { data[k] = function () { alert(k); }; } data[0](); // 3, 而不是0 data[1](); // 3, 而不是1 data[2](); // 3, 而不是2
上述样例就证实了 —— 同一个上下文中建立的闭包是共用一个[[Scope]]属性的。
所以上层上下文中的变量“k”是可以很是easy就被改变的。
activeContext.Scope = [ ... // 其余变量对象 {data: [...], k: 3} // 活动对象 ]; data[0].[[Scope]] === Scope; data[1].[[Scope]] === Scope; data[2].[[Scope]] === Scope;
这样一来,在函数激活的时候,终于使用到的k就已经变成了3了。
例如如下所看到的。建立一个闭包就可以解决问题了:
var data = []; for (var k = 0; k < 3; k++) { data[k] = (function _helper(x) { return function () { alert(x); }; })(k); // 传入"k"值 } // 现在结果是正确的了 data[0](); // 0 data[1](); // 1 data[2](); // 2
让咱们来看看上述代码都发生了什么?函数“_helper”建立出来以后。经过传入參数“k”激活。
其返回值也是个函数。该函数保存在相应的数组元素中。这样的技术产生了例如如下效果: 在函数激活时,每次“_helper”都会建立一个新的变量对象。当中含有參数“x”,“x”的值就是传递进来的“k”的值。这样一来。返回的函数的[[Scope]]就成了例如如下所看到的:
data[0].[[Scope]] === [ ... // 其余变量对象 父级上下文中的活动对象AO: {data: [...], k: 3}, _helper上下文中的活动对象AO: {x: 0} ]; data[1].[[Scope]] === [ ... // 其余变量对象 父级上下文中的活动对象AO: {data: [...], k: 3}, _helper上下文中的活动对象AO: {x: 1} ]; data[2].[[Scope]] === [ ... // 其余变量对象 父级上下文中的活动对象AO: {data: [...], k: 3}, _helper上下文中的活动对象AO: {x: 2} ];
咱们看到。这时函数的[[Scope]]属性就有了真正想要的值了。为了达到这种目的。咱们不得不在[[Scope]]中建立额外的变量对象。要注意的是,在返回的函数中。假设要获取“k”的值,那么该值仍是会是3。
顺便提下,大量介绍JavaScript的文章都以为仅仅有额外建立的函数才是闭包,这样的说法是错误的。实践得出,这样的方式是最有效的。然而。从理论角度来讲,在ECMAScript中所有的函数都是闭包。
然而,上述提到的方法并不是惟一的方法。
经过其它方式也可以得到正确的“k”的值,例如如下所看到的:
var data = []; for (var k = 0; k < 3; k++) { (data[k] = function () { alert(arguments.callee.x); }).x = k; // 将k做为函数的一个属性 } // 结果也是对的 data[0](); // 0 data[1](); // 1 data[2](); // 2
另一个特性是从闭包中返回。在ECMAScript中,闭包中的返回语句会将控制流返回给调用上下文(调用者)。而在其它语言中。比方,Ruby,有很是多中形式的闭包。对应的处理闭包返回也都不一样,如下几种方式都是可能的:可能直接返回给调用者,或者在某些状况下——直接从上下文退出。
ECMAScript标准的退出行为例如如下:
function getElement() { [1, 2, 3].forEach(function (element) { if (element % 2 == 0) { // 返回给函数"forEach"函数 // 而不是返回给getElement函数 alert('found: ' + element); // found: 2 return element; } }); return null; }
然而,在ECMAScript中经过try catch可以实现例如如下效果:
var $break = {}; function getElement() { try { [1, 2, 3].forEach(function (element) { if (element % 2 == 0) { // // 从getElement中"返回" alert('found: ' + element); // found: 2 $break.data = element; throw $break; } }); } catch (e) { if (e == $break) { return $break.data; } } return null; } alert(getElement()); // 2
这里说明一下,开发者经常错误将闭包简化理解成从父上下文中返回内部函数,甚至理解成仅仅有匿名函数才干是闭包。
再说一下,因为做用域链,使得所有的函数都是闭包(与函数类型无关: 匿名函数,FE,NFE,FD都是闭包)。
这里仅仅有一类函数除外,那就是经过Function构造器建立的函数,因为其[[Scope]]仅仅包括全局对象。
为了更好的澄清该问题。咱们对ECMAScript中的闭包给出2个正确的版本号定义:
ECMAScript中,闭包指的是:
实际使用的时候,闭包可以建立出很优雅的设计。赞成对funarg上定义的多种计算方式进行定制。例如如下就是数组排序的样例。它接受一个排序条件函数做为參数:
[1, 2, 3].sort(function (a, b) { ... // 排序条件 });
相同的样例还有。数组的map方法是依据函数中定义的条件将原数组映射到一个新的数组中:
[1, 2, 3].map(function (element) { return element * 2; }); // [2, 4, 6]
使用函数式參数,可以很是方便的实现一个搜索方法,并且可以支持无限制的搜索条件:
someCollection.find(function (element) { return element.someProperty == 'searchCondition'; });
还有应用函数。比方常见的forEach方法,将函数应用到每个数组元素:
[1, 2, 3].forEach(function (element) { if (element % 2 != 0) { alert(element); } }); // 1, 3
顺便提下,函数对象的 apply 和 call方法,在函数式编程中也可以用做应用函数。 apply和call已经在讨论“this”的时候介绍过了;这里,咱们将它们看做是应用函数 —— 应用到參数中的函数(在apply中是參数列表,在call中是独立的參数):
(function () { alert([].join.call(arguments, ';')); // 1;2;3 }).apply(this, [1, 2, 3]);
闭包还有另一个很重要的应用 —— 延迟调用:
var a = 10; setTimeout(function () { alert(a); // 10, after one second }, 1000);
还有回调函数
//... var x = 10; // only for example xmlHttpRequestObject.onreadystatechange = function () { // 当数据就绪的时候,才会调用; // 这里。不管是在哪一个上下文中建立 // 此时变量“x”的值已经存在了 alert(x); // 10 }; //...
还可以建立封装的做用域来隐藏辅助对象:
var foo = {}; // 初始化 (function (object) { var x = 10; object.getX = function _getX() { return x; }; })(foo); alert(foo.getX()); // 得到闭包 "x" – 10
本文介绍了不少其它关于ECMAScript-262-3的理论知识,而我以为,这些基础的理论有助于理解ECMAScript中闭包的概念。假设有不论什么问题。我回在评论里回复你们。