本来写这篇闭包总结都快写结束了,参考了几篇大佬的文章,结果写着写着发现大佬们对闭包原理的解释好像不太一致。而且阮一峰大神的文章虽然写的很简单容易理解,但是在知乎看到大佬朴灵并不是很赞成阮一峰的看法 (链接)。之后我又翻阅了几篇文章,陷入了迷茫。想想还是决定原理这部分每个人都有不同的看法,还是基于语言定义来看吧。
闭包是什么
理论意义上的闭包
我们先看下wiki百科对闭包的定义: 链接在此
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。
其中重点是:闭包是一个函数和其关联环境共同组成的,关联环境中包含了约束变量(该函数内部绑定的符号)和自由变量(在函数外部定义但在函数内被引用)。
啊?闭包是这样的吗?这样一想不是很多函数都是闭包了吗?
确实,我们老看下 MDN 对闭包的定义:链接在此
闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。闭包将函数与其所操作的某些数据(环境)关联起来。
《JavaScript权威指南》也说到:从技术的角度讲,所有的 JavaScript 函数都是闭包。
ECMAScript 中,闭包指的是:
- 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
- 从实践角度:以下函数才算是闭包:
I. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
II. 在代码中引用了自由变量
所以,理论上,JavaScript 里所有的函数都是闭包。 不过一般来说,嵌套的 function 所产生的闭包更为强大,也是大部分时候我们所谓的 “闭包”。
实践意义上的闭包
I. 即使创建它的上下文已经销毁(弹出上下文栈),它仍然存在(比如,内部函数从父函数中返回)
II. 在代码中引用了自由变量
function a() {
var i = 0;
function b() {
alert(++i);
}
return b;
}
var c = a();
c();
这段代码有两个特点:函数 b 嵌套在函数 a 内部;函数 a 返回函数 b。
这样在执行完 var c = a() 后,变量 c 实际上是指向了函数 b,b 中用到了变量 i,再执行 c() 后就会弹出一个窗口显示 i 的值。这段代码其实就创建了一个闭包,为什么?
解析:如果返回的 b 不被自由变量 c 引用,只是被 a 所引用,而此时 a 也只会被 b 引用,因此函数 a 和 b 互相引用但又不被被外界引用,函数 a 和 b 就会被 GC 回收。但是此题中 i 值被保存下来累加。因为此时的上下文已经弹出栈了,但是闭包的存在使其保存在内存中。
b 的执行上下文维护了一个作用域链
b作用域链 : { b函数变量及参数, a函数变量及参数, 全局作用域变量 }
因为这个作用域链,b 依然可以读取到 a函数变量及参数 的值,即使 a函数变量及参数 被销毁了,但是 JavaScript 依然会让 a函数变量及参数 活在内存中,b 依然可以通过作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
当函数 a 的内部函数 b 被 return 出来,被函数 a 外的一个自由变量引用的时,就创建了一个我们通常所谓的“闭包”。
闭包的作用
- 可以读取函数内部的变量。看上面代码就是 c 可以操作 i
- 让这些变量的值始终保持在内存中。i 一直在内存里
- 避免变量污染全局
- 把变量存到独立的作用域,作为私有成员存在
- 模拟私有方法,模块封装
缺点
- 增大内存消耗。闭包里的引用无法被垃圾回收,增大内存用量,使用不当会导致内存泄漏。
- 对处理速度有影响。闭包的层级决定了引用在查找时经过的作用域链长度。
- 可能获取到意外的值(captured value)
应用场景
应用场景一
典型应用是模块封装,在各模块规范出现之前,都是用这样的方法防止变量污染全局。
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
对于模块封装有两个要点:
- 使用立即执行函数包裹,该函数必须被至少调用一次
- 必须返回至少一个函数形成闭包
应用场景二
在循环中创建闭包,防止取到意外的值
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]();
锻炼题目
参考文章:
wiki 闭包 (计算机科学)
MDN 闭包
深入理解 JavaScript闭包
闭包
JavaScript深入之闭包
谢谢观看,不足的地方还有很多,请多多指教。