闭包就是能够读取其他函数内部变量的函数。—— 阮一峰
在JavaScript中,函数A内部的局部变量不能被A以外的函数访问到,只能被A内部的子函数B访问到,当在函数A外调用函数B时,便可通过B访问A中局部变量,那么子函数B就是闭包。(注意,是子函数哦~)
举个栗子:
function outer() {
var a = 100; // outer的局部变量
function inner() { // 闭包
console.log(a);
}
return inner; // 没有这条语句,闭包起不到在outer外部访问变量a的作用~
}
console.log(a); // 在外部直接访问a出错,Uncaught ReferenceError: a is not defined
var test = outer(); // outer运行完返回inner,赋值给test
test(); // 100,执行test(),相当于执行inner(),这样就可以访问到outer内部的a了
简单来说,闭包就是在另一个作用域中保存了一份它从上一级函数或作用域取得的变量(键值对),而这些键值对是不会随上一级函数的执行完成而销毁。周爱民说得更清楚,闭包就是“属性表”,闭包就是一个数据块,闭包就是一个存放着“Name=Value”的对照表。就这么简单。但是,必须强调,闭包是一个运行期概念。—— 司徒正美
根编译原理知识,编程语言主要分为两类:
{
"编译形": c/c++,
"解释形": javascript
}
“编译形(C/C++、java )” 是全部代码必须整体正确通过编译后才可以运行.。[代码整体为单位完全编译]、[先编译后运行]
“解释形(javascript)”是一边编译一边运行,代码是逐行解释运行。[代码按最小单位:表达式|语句解释运行]、[编译运行先同时]
说原理可能有些大,但这部分确实是要从内部机制的角度,解释下“为什么上面的栗子中通过inner就可以在outer外部访问outer内部的a
javascript是没有块级作用域的 但是具有全局作用和函数内部运行时作用域
在JS中,有两个链很重要:原型链 和 作用域链。通过 原型链 可以实现继承,而与 闭包 相关的就是 作用域链。
还是上面的栗子,假设outer定义在全局作用域中:
1 function outer() {
2 var a = 100;
3 function inner() {
4 console.log(a);
5 }
6 return inner;
7 }
8 var test = outer();
9 test();
当执行到1处,outer函数定义,其作用域链中只有一个全局对象。
然后执行8,运行outer()。此时,outer的作用域链中有两个对象:outer的活动对象–>全局对象。
运行outer(),会执行outer里面的3,定义inner(),此时inner的作用域链是:outer的活动对象–>全局对象。
执行9,其实就是执行inner函数,此时其作用域链是:inner的活动对象–>outer的活动对象–>全局对象。
因为inner的作用域链中有outer的活动对象,所以它可以访问到outer的局部变量a。
常理来说,一个函数执行完毕,其执行环境的作用域链会被销毁。但是outer执行完毕后,其活动对象仍然保留在内存中,因为inner的作用域链仍在引用着这个活动对象。所以此时,outer的作用域链虽然销毁了,但是其活动对象仍在内存中。直到test执行完毕,outer的活动对象才被销毁。
也正因为如此,闭包只能取得包含函数中任何变量的最后一个值,即包含函数执行完毕时变量的值。改改之前的栗子:
function outer() {
var a = 100;
function inner() {
console.log(a);
}
a = a + 50; // 改变a的值
return inner;
}
var test = outer();
test(); // 150,取得的a是150,而不是100
正是因为作用域链,只能里面的访问外面的,外面的不能访问里面的。也是基于作用域链,聪明的大师们想出了闭包,使得外面的可以访问里面的。掌握作用域链很关键啊。
通过对上边概念的理解,来理解下边这句话:
最典型的场景如下:
var tasks = [];
for (var i = 0; i < 5; i++) {
tasks[tasks.length] = function () {
console.log('Current cursor is at ' + i);
};
}
var len = tasks.length;
while (len--) {
tasks[len]();
}
以上代码对 tasks 中的函数的执行均会输出 Current cursor is at 5,往往不符合预期。(可以在适当位置插入console.log()运行测试)
此现象称为 Lift 效应 。解决的方式是通过额外加上一层闭包函数,将需要的外部变量作为参数传递来解除变量的绑定关系:
var tasks = [];
for (var i = 0; i < 5; i++) {
// 注意有一层额外的闭包
tasks[tasks.length] = (function (i) {
return function () {
console.log('Current cursor is at ' + i);
};
})(i);
}
var len = tasks.length;
while (len--) {
tasks[len]();
}
创建闭包最常见的方式就是在一个函数里面创建一个函数,之前举得栗子就是。
最常见的3种闭包实现:
(function(){
//函数闭包
})();
with(obj){
//这里是对象闭包
}
try{
//...
} catch(e) {
//catch闭包 但IE里不行
}
function outer() {
var a = 100;
function inner() {
console.log(a++);
}
return inner;
}
var test = outer();
test(); // 100
test(); // 101
test(); // 102
栗子中a一直在内存中,每执行一次test(),输出值加1。因为test在全局执行环境中,所以a一直在内存中,如果test在其他执行环境中,当这个执行执行环境销毁的时候,a就不会再在内存中了。
function Person(){
var name = 'XiaoMing';
return {
setName : function(theName){
name = theName;
},
getName : function(){
return name;
}
};
}
var person = new Person();
console.log(person.name); // undefined
console.log(person.getName()); // XiaoMing
//*************闭包uniqueID*************
uniqueID = (function(){
var id = 0;
return function() {
return id++; // 返回,自加
};
})();
document.writeln(uniqueID()); //0
document.writeln(uniqueID()); //1
document.writeln(uniqueID()); //2
document.writeln(uniqueID()); //3
document.writeln(uniqueID()); //4
//*************闭包阶乘*************
var a = (function(n) {
if (n<1) {
alert("invalid arguments");
return 0;
}
if(n==1) {
return 1;
}
else {
return n * arguments.callee(n-1);
}
})(4);
document.writeln(a);
//*************闭包阶乘*************
var a = (function(n) {
if (n<1) {
alert("invalid arguments");
return 0;
}
if(n==1) {
return 1;
}
else {
return n * arguments.callee(n-1);
}
})(4);
document.writeln(a);
在 JavaScript 中,无需特别的关键词就可以使用闭包,一个函数可以任意访问在其定义的作用域外的变量。需要注意的是,函数的作用域是静态的,即在定义时决定,与调用的时机和方式没有任何关系。
闭包会阻止一些变量的垃圾回收,对于较老旧的JavaScript引擎,可能导致外部所有变量均无法回收。
首先一个较为明确的结论是,以下内容会影响到闭包内变量的回收:
Chakra、V8 和 SpiderMonkey 将受以上因素的影响,表现出不尽相同又较为相似的回收策略,而JScript.dll和Carakan则完全没有这方面的优化,会完整保留整个 LexicalEnvironment 中的所有变量绑定,造成一定的内存消耗。
由于对闭包内变量有回收优化策略的 Chakra、V8 和 SpiderMonkey 引擎的行为较为相似,因此可以总结如下,当返回一个函数 fn 时:
如果 fn 的 [[Scope]] 是ObjectEnvironment(with 表达式生成 ObjectEnvironment,函数和 catch 表达式生成 DeclarativeEnvironment),则:
如果是 V8 引擎,则退出全过程。
如果是 SpiderMonkey,则处理该 ObjectEnvironment 的外层 LexicalEnvironment。
获取当前 LexicalEnvironment 下的所有类型为 Function 的对象,对于每一个 Function 对象,分析其 FunctionBody:
如果 FunctionBody 中含有 直接调用eval,则退出全过程。
否则得到所有的 Identifier。
对于每一个 Identifier,设其为 name,根据查找变量引用的规则,从 LexicalEnvironment 中找出名称为 name 的绑定 binding。
对 binding 添加 notSwap 属性,其值为 true。
检查当前 LexicalEnvironment 中的每一个变量绑定,如果该绑定有 notSwap 属性且值为 true,则:
如果是V8引擎,删除该绑定。
如果是SpiderMonkey,将该绑定的值设为 undefined,将删除 notSwap 属性。
对于Chakra引擎,暂无法得知是按 V8 的模式还是按 SpiderMonkey 的模式进行。
如果有 非常庞大 的对象,且预计会在 老旧的引擎 中执行,则使用闭包时,注意将闭包不需要的对象置为空引用。