什么是闭包
在《Javascript 高级程序设计》中是这么解释的
闭包是指有权访问另一个函数作用域中的变量的函数。
创建闭包
创建闭包最常见的方式,就是一个函数内部创建另一个函数
function outFunc() {
var scope = 'out';
function innerFunc() {
console.log(scope);
}
return innerFunc;
}
var foo = outFunc();
foo(); // out
复制代码
以上就是一个简单的闭包例子,在函数 outFunc 中创建了另一个函数 innerFunc,在 innerFunc 中可以访问到另一个函数 outFunc 中定义的变量 scope,即形成了闭包。我们还可以在 innerFunc 内修改 scope 值
function outFunc() {
var scope = 'out';
function innerFunc() {
scope = 'inner'
console.log(scope);
}
return innerFunc;
}
var foo = outFunc();
foo(); // inner
复制代码
闭包产生的原因
闭包形成的原因与函数执行是的作用域链有关,理解函数执行时是如何创建作用域链以及作用域链有什么作用,对彻底理解闭包至关重要。
执行上下文
执行上下文(execution context)是 Javascipt 中最为重要的一个概念,执行上下文定义了变量或者函数有权访问的数据范围。每个执行上下文都有一个与之关联的变量对象(varible object),上下文中定义的所有变量和函数都会保留在这个对象中。
一段 Javascript 代码在执行过程中一般会创建多个执行上下文,Javascript 引擎会通过创建执行上下文栈(Execution context stack)来管理各个执行上下文,当某个执行上下文被创建后都会被压入执行上下文栈中。
全局执行上下文是程序执行中最外围的一个执行上下文,我们可以用 globalContext 来表示全局执行上下文,全局执行上下文直到应用程序退出时才会被销毁,因此在执行上下文栈的最底部永远存在一个 globalContext,除非程序执行完毕。
每个函数都有自己的执行上下文,当执行流进入一个函数时,函数的执行上下文就会被压入执行上下文栈中,而在函数执行后,栈将其上下文弹出,把控制权返回给之前的执行环境。
我们通过刚才举例的闭包代码来进行分析
function outFunc() {
var scope = 'out';
function innerFunc() {
console.log(scope);
}
return innerFunc;
}
var foo = outFunc();
foo(); // out
复制代码
- 创建全局上下文 globalContext,压入执行上下文栈。
ECStack.push(globalContext)
复制代码
- 执行到
var foo = outFunc()
前,创建 outFunc 函数执行上下文,压入全局上下文栈中
ECStack.push(<outFunc> functionContext)
复制代码
- 执行函数 outFunc,执行完毕后,将其上下文从执行上下文栈中弹出。
ECStack.pop() // outFunc 执行完毕
复制代码
- 此时 foo 即为内部函数 innerFunc,执行到
foo()
前,创建 innerFunc 函数执行上下文,压入上下文栈中。
ECStack.push(<innerFunc> functionContext)
复制代码
- 执行函数 innerFunc,执行完毕后,将其上下文从执行上下文栈中弹出。
ECStack.pop() // innerFunc 执行完毕
复制代码
- 程序结束,销毁全局上下文
ECStack.pop() // globalContext 从栈中弹出
复制代码
通过以上分析,我们可以发现在函数 innerFunc 执行时,函数 outFunc 其实已经执行完毕,其执行上下文也已经从栈中销毁,那为什么我们在 innerFunc 中还能访问到函数 outFunc 内部定义的变量呢?这就要说到另一个关键概念了,作用域链。
作用域链
当函数执行时,在查找变量时,会先从当前函数的执行上下文的变量对象中查找,如果没有找到对应变量,则会继续在其父级执行上下文的变量对象中查找变量,直到变量找到或者已达最上层执行上下文。这样多个执行上下文的变量对象就叫做作用域链。
值得一提的是,函数的作用域链在函数创建时就已经确定,当函数创建时,就会保存其父级的变量对象在其中。 且作用域前端,始终是当前执行的代码所在上下文的变量对象。这样保证了执行上下文有权访问的所有变量和函数的有序访问。
我们来继续分析一下这段代码
function outFunc() {
var scope = 'out';
function innerFunc() {
console.log(scope);
}
return innerFunc;
}
var foo = outFunc();
foo(); // out
复制代码
可以发现,当函数 outFunc 执行时,会创建函数 innerFunc,innerFunc 在创建时会将其父级函数执行上下文的变量对象保存在自己的作用域链中。根据作用域链查询变量的规则,当函数 innerFunc 执行时,从当前上下文的变量对象查询变量 scope,未找到将会继续从其父级函数执行上下文的变量对象中查询,此时即可返回了 父函数中 scope 的值。
所以闭包形成的原因为:函数在创建时,会将其父类执行上下文的变量对象保留在其作用域链中,即使在其父类执行上下文已经被销毁,但该函数作用域链中仍然保留有父级上下文相关的变量对象,因而仍然可以继续访问到父类执行上下文中的变量。
闭包的作用
- 创建需要多次执行的函数的公共变量
什么时候我们需要创建多次执行的函数的公共变量?当一个函数的执行结果受前一次执行结果的影响时,我们就可以将这个执行结果通过变量放在父类函数中,将该函数在父函数中定义为子函数返回,以此形成闭包。
举个例子,用过节流函数(throttle)的同学应该可以发现,不管我们怎么触发函数,函数只有到达一定的时间节点后才会进行执行。我们可以用闭包来简单的实现一个节流函数。
function throttle(func, wait) {
// 记录前一次函数执行的时间
var previous = ;
return function() {
var now = +new Date(); // +new Date() 获取当前的时间戳
// 时间间隔大于设定的等待时间即执行函数
if (now - previous > wait) {
func();
previous = now; // 将 previous 设置为本次函数的执行的时间
}
}
}
复制代码
可以看到函数每次执行与否受上次一次函数的执行时间的影响,每次我们执行函数都可以访问 previous 这个公共变量,并且修改 previous 的值来控制下一次函数执行的结果。
- 函数柯里化(curry)
接触过函数式编程的童鞋对柯里化一定不陌生
curry 的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
实际上柯里化就是运用了闭包的特性
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add();
var addTen = add();
increment(); // 3
addTen(); // 12
复制代码
通过闭包“记住了”函数的第一个参数
使用闭包的注意点
- 由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存空间。过度使用闭包可能会导致内存占用过多,因此我们只在绝对必要的时候再考虑使用闭包。
- 闭包只能取得父类函数中任何变量的最终值。
function foo() {
var result = new Array();
for (var i = ; i < ; i++) {
result[i] = function() {
return i;
};
}
return result;
}
result[](); // 3
result[](); // 3
result[](); // 3
复制代码
以上每一个函数执行返回的都将会是 3,因为每一个函数的作用域链中都保存着 foo 函数的变量对象,当函数 foo 执行完毕后,其变量对象中 i 的值为 3;所以在内部匿名函数中获取到的都会是 3。
我们可以通过在内部的匿名函数中再创建一个函数,并且每次自执行匿名函数来避免这个问题
function foo() {
var result = new Array();
for (var i = ; i < ; i++) {
result[i] = (function(num) {
return function() {
return i;
}
})(i);
}
return result;
}
result[](); // 0
result[](); // 1
result[](); // 2
复制代码
可以看到在 foo 的内部匿名函数中再次定义了一个函数,以此形成闭包,在每次循环中进行自调用,“记录住了”每次 i 的值。