前言
闭包就是能够读取其他函数内部变量的函数。在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。首先理解什么是自由变量?
正文
什么是自由变量?
指在当前作用域中使用,却不属于当前作用域的变量
例:
'use strict';
function foo() {
var a = 1;
function bar() {
console.log(a);
}
bar();
}
复制代码
在foo函数中,a变量相对了bar函数就是自由变量。
闭包
首先需要知道几个概念:
执行上下文初始化的时候包含:
- this
- 活动对象(Active Object),在全局的时候即(Global Object)
- 作用域链([[scope]]),存在函数执行上下文中
每个函数内部都有一个[[scope]]的属性(这个属性仅供js引擎使用),当函数被创建的时候,[[scope]]属性会保存创建该函数的作用域中的所有对象(Variable Obejct)。
具体通过下面代码的执行过程来看闭包:
'use strict';
function foo() {
var a = 1;
return function() {
return ++a;
};
};
var add = foo();
console.log(add()); // 2
复制代码
该代码的具体执行过程如下:
- 创建全局执行上下文环境(Global Execution context,下面简称globalEC ),将全局执行上下文压入执行上下文栈(Execution context stack,下面简称ECStack)中。
// 伪代码,全局执行上下文压栈
ECStack = [
globalEC
]
复制代码
- 进入预解析阶段,变量提升和函数提升
function foo() { /... };
var add;
add = foo();
console.log(add());
复制代码
- 全局执行上下文初始化
// 伪代码
globalEC = {
[global]: [global],
this: [global],
GO: {
foo: function() { //... },
add: undefined
}
}
复制代码
- 当执行foo函数的时候,创建foo函数的执行上下文环境(foo Execution context,下面简称fooEC),并压入执行上下文栈中
// 伪代码,创建foo函数的执行上下文并压栈
ECStack = [
fooEC
]
复制代码
- foo函数执行上下文初始化,并处于活动状态
// 伪代码
fooEC = {
this: undefined, // 严格模式下为undefined,非严格模式下为window
AO: {
arguments: {
length: 0
},
a: undefined
},
[[scope]]: [fooEC.AO, globalEC.GO]
}
复制代码
- foo函数开始执行
// 伪代码,对变量a赋值
fooEC = {
this: undefined,
AO: {
arguments: {
length: 0
},
a: 1
},
[[scope]]: [fooEC.AO, globalEC.GO]
}
复制代码
- foo函数执行完毕,foo函数的执行上下文出栈,foo函数的执行上下文销毁,但此时foo函数中的变量a(对于add函数来说,a就是自由变量)还被add函数引用,此时就形成了闭包,add函数的作用域链中保存了foo函数中的活动对象,所以导致foo函数的活动对象不能被销毁。
// 伪代码,foo函数的执行上下文出栈,进入全局执行上下文环境
ECStack = [
globalEC
]
复制代码
- 当执行到add函数的时候,创建add函数的执行上下文环境(add Execution context,下面简称addEC),并压栈
//伪代码,add函数的执行上下文压栈
ECStack = [
addEC,
globalEC
]
复制代码
- add函数执行上下文初始化,并处于活动状态
// 伪代码,add函数执行上下文初始化
addEC = {
this: undefined,
AO: {
aruments: {
length: 0
}
},
[[scope]]: [[addEC.AO, fooEC.AO, globalEC.GO]]
}
复制代码
可以看出,此时add函数的作用域链中还保存着foo函数的活动对象。
- add函数执行完毕,add函数的执行上下文出栈,并被销毁,进入全局执行上下文环境
// 伪代码
ECStack = [
globalEC
]
复制代码
至此,整个执行过程就结束了,但是由于函数的内部的[[scope]]属性是在函数被创建的时候就存在的,里面保存了定义该函数时候的作用域链,在上面的示例代码中,即:add函数中保存了foo函数的作用域链。所以导致foo函数作用域链中的活动对象还存在引用,不能得到释放,占用了内存,这也就是为什么闭包会导致内存溢出的原因。
所以当不再需要使用add函数的时候,将add函数的赋值为null,即解除了add函数的引用,也就释放add函数占用的内存。
代码如下:
'use strict';
function foo() {
var a = 1;
return function() {
return ++a;
};
};
var add = foo();
console.log(add()); // 2
//如果后面不再使用add函数,即将其赋值为null来释放内存
add = null;
复制代码
面试常考题
示例代码:
'use strict';
for(var i=0; i<3; i++) {
setTimeout(function() {
console.log(i);
});
}
复制代码
结果:上面的代码会在控制台打印3,打印3次。
解析:
- 进入全局上下文环境,压栈,声明变量a
- 当每次进入for循环的时候,会将 console.log(i) 放入异步队列中
- 当主线程的代码执行完成,就会去执行异步队列中的代码
- 此时for循环已经执行完成,此时i的值为3
- 因为setTimeout函数此时处于全局执行上下文中,所以当要查找变量 i 的时候,会去全局对象中找,i的值已经变为3。
解决方案: 只要setTimeout函数处于块作用域中,就能保存每次循环i的值。
- 闭包
'use strict';
for(var i=0; i<3; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
});
})(i);
}
复制代码
此时,每次for循环的时候,将i的值传递进来,保存到自执行函数的arguments里面,这样就形成了一个函数作用域,那么每次寻找变量i的时候,就先从当先的作用域中查找。
- 可以使用let声明,使用let可以产生一个块作用域,也能保存 i 的值
'use strict';
for(let i=0; i<3; i++) {
setTimeout(function() {
console.log(i);
});
}
复制代码
当然还可以有别的方法实现,但重要的是搞清楚原理。
结语: 上面的过程是我看了多篇关于闭包的文章之后自己的理解,如有不对的地方,还请指正~~~///(^v^)\~~~