JavaScript 闭包
闭包 = 函数 + 外部变量的引用 是指 一个函数可以访问其外部作用域的变量,即使这个外部作用域已经执行结束。闭包让函数“记住”它诞生时的作用域环境。
例:定义一个方法,方法的返回值为一个函数,返回函数的内部引用方法内部变量
function outer(){
var count = 1;
function inner(){
return count;
}
return inner;
}
var f = outer();
console.log(f()); // 1
console.log(f()); // 2
outer() 执行后,其作用域本应被销毁,但 inner() 仍然引用 count,所以 count 变量仍然存在,形成闭包。
为什么要这么写
首先闭包可以实现在函数外部获取函数内部的变量的值; 难道闭包就是为了这个?当然不是了。闭包最主要的目的是为了能够保存函数的运行状态,间接的实现了私有变量。
function outer(){
var a = 1;
function inner(){
return a++;
}
return inner;
}
var f = outer();
console.log(f());// 1
console.log(f());// 2
console.log(f());// 3
这样outer方法内部变量a就可保存状态了,每调用一次就自增1;
外部方法无法访问到变量a,实现了数据私有化
闭包为什么可以保存状态
闭包的这种特性和 JavaScript 的内存回收机制有关,JavaScript 会对定时进行垃圾回收,例如:
// 这时obj和对象之间存在着引用关系
var obj = {
a: 1,
b: 2,
};
obj = null; // 这时obj对对象之间的引用关系就不存在了,对象就会被JavaScript引擎当作垃圾消耗
函数在执行完成后函数内部变量也会被回收,只保留全局变量;
在闭包中函数outer执行完成后将内部函数变量inner作为返回值返回;
这样全局变量 f 和闭包函数inner形成引用关系,inner函数在outer函数内部;
这样 JavaScript 引擎因为这之间的引用关系不会进行垃圾回收,就可以到达保存允许状态的目的。
- 闭包的优点:不污染全局变量的情况下实现运行状态的保留
- 闭包的缺点:使用不当会造成内存泄漏;
- 使用场景:个人认为都可以使用 OOP 来替代,闭包实在让人难以理解控制;
深入闭包&内存回收
闭包的本质还是一个函数
- 这个函数引用了自由变量
- 即使创建这个函数的上下文已经销毁,这个函数仍然存在
var scope = "global scope";
function checkScope(){
var scope = "local scope";
function foo(){
return scope;
}
return foo;
}
var foo = checkScope();
foo();
我们来分析一下上面代码的执行过程:
- 创建全局执行上下文,压入执行栈底
- 初始化全局执行上下文
globalExecutionContext,执行代码 - 执行到
checkScope函数,创建checkScope函数执行上下文checkScopeContext,并压入执行栈 - 初始化
checkScopeContext执行上下文,继续执行代码 checkScope函数执行完成,将checkScopeContext从执行栈中弹出- 执行
foo函数,创建foo函数执行上下文fooContext,并压入执行栈 - 初始化
fooContext,继续执行代码 foo执行完成,将fooContext从执行栈中弹出- 完成
现象:在foo执行的时候,checkScopeExecutionContext已经出栈,但是仍然能够获取scope变量。
原理:在变量作用域&执行栈中介绍过标识符解析(即变量获取)是在作用域链中进行。
fooExecutionContext的作用域链是在初始化的时候生成的,如下:[AO, checkScopeContext.AO, globalContext.VO],因为函数foo引用了checkScopeContext.AO所以 JavaScripts 不会销毁checkScopeContext.AO,foo函数通过这个作用域链即使checkScopeContext已经被销毁但是仍然可以获得变量scope。
fooContext = {
Scope: [AO, checkScopeContext.AO, globalContext.VO],
}
例题
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
以上代码for循环执行完成之后的的全局上下文结构如下
globalContext = {
VO: {
data: [...],
i: 3
}
}
在执行data[0]()时,data[0]的作用域链为:
data[0]Context = {
Scope: [AO, globalContext.VO]
}
在data[0]Context.AO中并没有变量i,需要从globalContext.VO中获取,所以获取的i始终为3。
改成闭包后
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();
执行上下文结构如下:
data[0]Context = {
Scope: [AO, 匿名函数Context.AO, globalContext.VO]
}
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}