关于闭包

之前上选修课赶作业的时候有遇到过这个问题。
然后为了赶作业…没有具体的学习。
对于闭包的认知一直保留在"与 JavaScript 的作用域以及内存回收机制有关"。

来填一填坑。

2019-08-18更新

参考、阅读、引用的文档:

阿里前端面经
MDN - closures
stackoverflow - How do JavaScript closures work?

( ´・ᴗ・` ) so 上这个问答都已经是发生在十年前了。

词法作用域

可恶!又想起了令人头秃的编译原理。

词法作用域 VS 动态作用域

大部分语言都是基于词法作用域的,和词法作用域相反的呢,就是动态作用域。

然鹅 JavaScript 虽然是基于词法作用域的,但是 JavaScript 有一个大宝贝 this ,this 的机制又很像动态作用域(就是说 this 的域是在运行的时候确定的)。

不过说起来我怎么不记得词法分析这一步会确定作用域。。。(菜鸟发言

JavaScript 里,当我们要使用变量时,引擎总会从最近的一个 Scope 开始向外层查找。

词法作用域
1
2
3
4
5
6
7
8
9
let demo = "outer";
function foo() {
console.log(demo);
}
function bar() {
let demo = "inner";
foo();
}
bar(); // outer

为什么输出是 outer ?

  1. JavaScript 中的作用域就是词法作用域 。 (那函数作用域呢 ? 块级作用域呢 ? 词法作用域是指的变量的可见性
  2. 词法作用域是写代码的时候就静态确定下来的 ,所以函数 foo 的上级 scope 就是 global 。

词法作用域


可以从上图里看到 foo() 函数的上级 scope 是全局环境,因此输出的 demo 是 'outer' 而不是 'inner'。

简单来说,词法作用域关注在何处声明,动态作用域关注在何处调用。

闭包

那认知了一下词法作用域就可以开始了解闭包了。

闭包的定义

查阅到以下的定义:

  1. 闭包是词法作用域的体现。
  2. 闭包是函数和声明该函数的词法环境的组合。
  3. 闭包是 JavaScript 函数作用域的副作用产品,是一种特殊的对象。
  4. 一个持有外部环境变量的函数就是闭包。

组合一下理解应该就差不多了。就是说闭包是一个函数,即可以访问本身自己的局部变量,又能访问定义这个函数时的域里的局部变量。
再举个 MDN 上的例子。下例里的 add5 和 add10 是两个闭包函数,保存了不同的 x 值(定义函数时的域里的局部变量

closures
1
2
3
4
5
6
7
8
9
10
11
function makeAdder(x) {
return function(y) {
return x + y;
};
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

在 JavaScript 里,函数的作用域里的变量都会在函数执行完后被回收(就是最开始的提到的为什么闭包会和内存回收机制有关系),但是创建了闭包之后函数的作用域会一直保存。

经典的循环闭包问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
<div id="div-1">1</div>
<div id="div-2">2</div>
<div id="div-3">3</div>
<div id="div-4">4</div>
<script type="text/javascript">
window.onload = function() {
for (var i = 1; i <= 4; i++) {
document.getElementById(`div-${i}`).onclick = function() {
console.log(i);
};
}
};
</script>
</body>

上面的代码遇到的问题是无论在浏览器里点击哪个 div ,都会在 console 栏里输出 5 。
去年选修课的时候遇到的上面差不多的问题,其实百度一搜一大把一样的问题,所以当时解决方法就跟百度里的一样,用了 IIFE ,但是当时并不知道原理是什么,就只知道"啊,这不就是那个经典的闭包问题嘛!"

解决方案 1 ,利用立即执行函数使每个 div 的监听事件绑定的 i 不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
<div id="div-1">1</div>
<div id="div-2">2</div>
<div id="div-3">3</div>
<div id="div-4">4</div>
<script type="text/javascript">
window.onload = function() {
for (var i = 1; i <= 4; i++) {
(function(i) {
document.getElementById(`div-${i}`).onclick = function() {
console.log(i);
};
})(i);
}
};
</script>
</body>

以及现在如果遇到这种情况也不会去想闭包问题的逃避式解决方法:

解决方法 2 ,利用 ES6 的 let 来划分块作用域使每个 div 的监听事件绑定的 i 不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
<div id="div-1">1</div>
<div id="div-2">2</div>
<div id="div-3">3</div>
<div id="div-4">4</div>
<script type="text/javascript">
window.onload = function() {
for (let i of [1, 2, 3, 4]) {
document.getElementById(`div-${i}`).onclick = function() {
console.log(i);
};
}
};
</script>
</body>

问题的原因

在 for 循环里定义的点击事件进行的操作里用到了变量 i , 但是函数本身作用域里没有定义 i 这个变量,只能从定义这个函数的作用域里去找,存在 i 这个变量,并且在循环结束之后 i 的值为 5 (如果用 for(var i of [1,2,3,4] 则 i 最后为 4),上面的词法作用域里也提到了,js 的引擎要使用变量时,会向上层域里寻找,所以四个 div 绑定的监听事件里引用到的 i 是同一个变量,就是他们在创建时的域里的那个变量 i 。

解决的原因

解决方案 1:

使用了 IIFE 生成一个匿名函数,并把建立点击事件时的 i 作为参数传入这个匿名函数,所以监听事件尽管本身的域里还是没有这个变量 i ,但是四个点击事件已经有了不同的函数作用域,就是每一次循环生成的匿名函数的作用域,所以也就解决了这个问题

解决方案 2:

hmm 这个倒是很简单。

关键字是块级作用域

不过需要注意的是,ES6 之前是没有块级作用域的,而 ES6 里出现的 const/let 关键词声明的变量有自己的作用域块。

所以在 for(let i = 0; i < 4; i++) { 循环体 } 里,相当于表达示里申明了五个 let 块级作用域 {let i = 0}{let i = 1}{let i = 2}{let i = 3}{let i = 4},每一个 let 都有自己的作用域,所以在 for 循环的表达式里使用 let i ,每一个 i 都有属于自己的独立的块作用域。

这个解决方法 1 的区别在于,解决方法 1 使每个点击事件的定义时的域不同,而解决方法 2 里点击事件的定义时的域相同,但是每次循环内部都是一个新的 i。

闭包的用处

虽然对新手不太友好,但是也是很有用处的。

获取函数作用域里的定义

就像在认识闭包前需要认识到词法作用域一样,认识到闭包的这个作用前需要先理解一下函数作用域。

JavaScript 里有 global scope 、函数可以创建 function scope 以及 let/const 的 block scope。

再次强调 词法作用域是指的变量的可见性

在函数外部是没有办法获取到函数内部的变量的,即在 function scope 外,没有办法获取到 function scope 上的变量。
但是闭包可以。
原因就是嵌套的函数可以访问其外部声明的变量。
所以只要获取到某个函数 A 里定义的函数 B ,就可以通过函数 B 来获取函数 A 里定义的变量。

模拟私有函数和变量

也是差不多啦
和上面的那个大同小异,就是利用无法获取到函数作用域里的内容一样,使用闭包来访问私有的函数和变量。
举一个 MDN 的例子

私有定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 */

闭包的坏处

增加 JavaScript 学习难度

影响处理速度和内存消耗

通常来说函数执行完了,其内部定义的变量就会被释放,但是闭包这个异次元通道使得其外部声明的变量会保留在内存里。

MDN 上举的例子是,最好不要在构造函数里定义方法,(就是不要在函数里内嵌函数)
不然会在每次新建一个实例的时候都对方法从新赋值。

hmmm

IE6 的 bug,使用闭包会造成内存泄露(已修复

影响父级函数的变量

上面提到闭包可以模拟 static
但是如果本意不是建立一个私有对象,那么闭包作为父函数的公用方法,有可能会无意识改变父级函数里的变量的值。(情况和全局变量的问题很相似。

0%