本文将介绍一个在JavaScript常常会拿来讨论的话题 —— 闭包(closure)。闭包其实已是个老生常谈的话题了; 有大量文章都介绍过闭包的内容,尽管如此,这里仍是要试着从理论角度来讨论下闭包,看看ECMAScript中的闭包内部到底是如何工做的。程序员
正如在此前文章中提到的,这些文章都是系列文章,相互之间都是有关联的。所以,为了更好的理解本文要介绍的内容, 建议先去阅读下第四章 - 做用域链和 第二章 - 变量对象。算法
众所周知,在函数式语言中(ECMAScript也支持这种风格),函数便是数据。就比方说,函数能够保存在变量中,能够当参数传递给其余函数,还能够当返回值返回等等。 这类函数有特殊的名字和结构。编程
以下例子:数组
function exampleFunc(funArg) {
funArg();
}
exampleFunc(function () {
alert('funArg');
});
复制代码
上述例子中funarg的实参是一个传递给exampleFunc的匿名函数。bash
反过来,接受函数式参数的函数称为 高阶函数(high-order function 简称:HOF)。还能够称做:函数式函数 或者 偏数理的叫法:操做符函数。 上述例子中,exampleFunc 就是这样的函数。闭包
此前提到的,函数不只能够做为参数,还能够做为返回值。这类以函数为返回值的函数称为 _带函数值的函数(functions with functional value or function valued functions)。app
(function selfApplicative(funArg) {
if (funArg && funArg === selfApplicative) {
alert('self-applicative');
return;
}
selfApplicative(selfApplicative);
})();
复制代码
以本身为返回值的函数称为 自复制函数(auto-replicative function 或者 self-replicative function)。 一般,“自复制”这个词用在文学做品中:ecmascript
(function selfReplicative() {
return selfReplicative;
})();
复制代码
在函数式参数中定义的变量,在“funarg”激活时就可以访问了(由于存储上下文数据的变量对象每次在进入上下文的时候就建立出来了):编程语言
function testFn(funArg) {
// 激活funarg, 本地变量localVar可访问
funArg(10); // 20
funArg(20); // 30
}
testFn(function (arg) {
var localVar = 10;
alert(arg + localVar);
});
复制代码
然而,咱们知道(特别在第四章中提到的),在ECMAScript中,函数是能够封装在父函数中的,并可使用父函数上下文的变量。 这个特性会引起 funarg问题。ide
当函数返回的时候,这些参数又会从堆栈中移除。这种模型对将函数做为函数式值使用的时候有很大的限制(比方说,做为返回值从父函数中返回)。 绝大部分状况下,问题会出如今当函数有 自由变量的时候。
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量
以下所示:
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 // 对函数的引用
lexicalEnvironment: {x: 20} // 查询自由变量的上下文
};
复制代码
上述例子中,“fooClosure”部分是伪代码。对应的,在ECMAScript中,“foo”函数已经有了一个内部属性——建立该函数上下文的做用域链。
这里“lexical”是不言而喻的,一般是省略的。上述例子中是为了强调在闭包建立的同时,上下文的数据就会保存起来。 当下次调用该函数的时候,自由变量就能够在保存的(闭包)上下文中找到了,正如上述代码所示,变量“z”的值老是10。
定义中咱们使用的比较广义的词 —— “代码块”,然而,一般(在ECMAScript中)会使用咱们常常用到的函数。 固然了,并非全部对闭包的实现都会将闭包和函数绑在一块儿,比方说,在Ruby语言中,闭包就有多是: 一个程序对象(procedure object), 一个lambda表达式或者是代码块。
对于要实现将局部变量在上下文销毁后仍然保存下来,基于堆栈的实现显然是不适用的(由于与基于堆栈的结构相矛盾)。 所以在这种状况下,上层做用域的闭包数据是经过 动态分配内存的方式来实现的(基于“堆”的实现),配合使用垃圾回收器(garbage collector简称GC)和 引用计数(reference counting)。 这种实现方式比基于堆栈的实现性能要低,然而,任何一种实现老是能够优化的: 能够分析函数是否使用了自由变量,函数式参数或者函数式值,而后根据状况来决定 —— 是将数据存放在堆栈中仍是堆中。
var x = 10;
function foo() {
alert(x);
}
(function (funArg) {
var x = 20;
// funArg的变量 "x" 是静态保存的,在该函数建立的时候就保存了
funArg(); // 10, 而不是 20
})(foo);
复制代码
从技术角度来讲,建立该函数的上层上下文的数据是保存在函数的内部属性 [[Scope]]中的。 若是你还不了解什么是[[Scope]],建议你先阅读第四章, 该章节对[[Scope]]做了很是详细的介绍。若是你对[[Scope]]和做用域链的知识彻底理解了的话,那对闭包也就彻底理解了。
根据函数建立的算法,咱们看到 在ECMAScript中,全部的函数都是闭包,由于它们都是在建立的时候就保存了上层上下文的做用域链(除开异常的状况) (无论这个函数后续是否会激活 —— [[Scope]]在函数建立的时候就有了)
var x = 10;
function foo() {
alert(x);
}
// foo is a closure
foo: <FunctionObject> = {
[[Call]]: <code block of foo>,
[[Scope]]: [
global: {
x: 10
}
],
... // other properties
};
复制代码
正如此前提到过的,出于优化的目的,当函数不使用自由变量的时候,实现层可能就不会保存上层做用域链。 然而,ECMAScript-262-3标准中并未对此做任何说明;所以,严格来讲 —— 全部函数都会在建立的时候将上层做用域链保存在[[Scope]]中。
有些实现中,容许对闭包做用域直接进行访问。好比Rhino,针对函数的[[Scope]]属性,对应有一个非标准的 __parent__属性,在第二章中做过介绍:
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
复制代码
var firstClosure;
var secondClosure;
function foo() {
var x = 1;
firstClosure = function () { return ++x; };
secondClosure = function () { return --x; };
x = 2; // 对AO["x"]产生了影响, 其值在两个闭包的[[Scope]]中
alert(firstClosure()); // 3, 经过 firstClosure.[[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”是能够很容易就被改变的。
以下所示:
activeContext.Scope = [
... // higher variable objects
{data: [...], k: 3} // activation object
];
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标准的退出行为以下:
function getElement() {
[1, 2, 3].forEach(function (element) {
if (element % 2 == 0) {
// 返回给函数"forEach",
// 而不会从getElement函数返回
alert('found: ' + element); // found: 2
return element;
}
});
return null;
}
alert(getElement()); // null, 而不是 2
复制代码
然而,在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
复制代码
ECMAScript中,闭包指的是:
[1, 2, 3].sort(function (a, b) {
... // 排序条件
});
复制代码
一样的例子还有,数组的map方法(并不是全部的实现都支持数组map方法,SpiderMonkey从1.6版本开始有支持),该方法根据函数中定义的条件将原数组映射到一个新的数组中
[1, 2, 3].map(function (element) {
return element * 2;
}); // [2, 4, 6]
复制代码
使用函数式参数,能够很方便的实现一个搜索方法,而且能够支持无穷多的搜索条件
someCollection.find(function (element) {
return element.someProperty == 'searchCondition';
});
复制代码
还有应用函数,好比常见的forEach方法,将funarg应用到每一个数组元素:
[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, 一秒钟后
}, 1000);
复制代码
也能够用于回调函数:
...
var x = 10;
// only for example
xmlHttpRequestObject.onreadystatechange = function () {
// 当数据就绪的时候,才会调用;
// 这里,不管是在哪一个上下文中建立,变量“x”的值已经存在了
alert(x); // 10
};
..
复制代码
还能够用于封装做用域来隐藏辅助对象:
var foo = {};
// initialization
(function (object) {
var x = 10;
object.getX = function _getX() {
return x;
};
})(foo);
alert(foo.getX()); // get closured "x" – 10
复制代码
重学JavaScript深刻理解系列(一)
重学JavaScript深刻理解系列(二)
重学JavaScript深刻理解系列(三)
重学JavaScript深刻理解系列(四)
重学JavaScript深刻理解系列(五)