结合《你不知道的JavaScript(上)》一书,详解闭包,理清关于闭包的三个问题,彻底认识闭包。

闭包详解

1. 什么是闭包?

闭包的定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 3;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();
-----> '3'

这是一段非常经典的闭包代码,根据网上的说法,闭包的产生有三个条件:1. A函数内返回B函数。2.B函数调用了A函数的变量。3.B()。

很神奇有没有? 我们在外部环境访问了内部环境的变量。并且正常执行了foo的作用域会被销毁掉,垃圾回收机制也会使的释放掉内存空间。显然闭包阻止了这件事情的发生。

2.闭包产生的条件?

由于bar声明的位置,使其拥有涵盖了foo的内部作用域,当bar被调用后,会保持对foo作用域的引用,访问a变量。保持对外部作用域的引用,就会产生闭包,而不是非要返回函数。

1
2
3
4
5
6
function waiting(msg) {
setTimeout(function () {
console.log(msg)
}, 1000)
}
waiting('js')

无论是什么其他的骚操作,只要你将内部函数传递到所在的词法作用域之外,他都会保持对内部作用域的引用,无论在哪里执行内部函数,都会创建闭包。这个例子中setTimeout保持对waiting的引用,在执行时也会创建闭包。

3. 闭包的用途?

有时候,我们希望能够重用一个变量,并且其被保护起来不被污染篡改,就可以使用闭包。这种变量一般被称为私有变量或者局部变量

1
2
3
4
5
6
//一个很经典的题
for(var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i)
}, i* 1000)
}

嗯,我相信大多数搜集资料学习的人,都会清楚答案。 至于为什么,值得再复习一下。

每次循环,我们都会挑出一份i用来输出,但因为setTimeout会在循环完成后执行,每次的i都在同一全局作用域下,于是后来居上,覆盖了前面的i,再由setTimeout执行时,就全是6了。

怎么使得每次循环输出正确呢? 我们只需要将每次的i变成一个私有变量,有独立的作用域,让其不在篡改就OK了。

  • 使用IIFE

这里引入来自MDN的释义。


IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数

1
2
3
(function () {
statements
})();

这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。

第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。


1
2
3
4
5
6
7
8
// IIFE方法
for(var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j)
}, j* 1000)
})(i);
}

这里利用IIFE拥有独立的此法作用域的特性,将变量私有化,这样在setTimeout执行时就会得到正确输出。没错,好像就是利用闭包将每次的变量缓存起来,放在独立的内存中。

  • 使用ES6的let
1
2
3
4
5
6
// let方法
for(let i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i)
}, i* 1000)
}

let会劫持块作用域,“劫持”就是把当前块作用域抢过来变为一个独立的,如果还不明白,请看下面代码。

1
2
3
4
5
6
for(var i = 1; i <= 5; i++) {
let j = i;
setTimeout(function () {
console.log(j)
}, j* 1000)
}

let 遇见{}发生了奇妙的邂逅,于是他们“私奔”了~ 在每次循环中都被声明了一次,所以每次循环中的{}内的变量都不干扰。setTimeout很高兴的执行了。

摇晃一下你的小脑瓜,试想一下,如果我们将这些通过闭包产生的私有变量赋值给一个函数,你会联想到什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var foo = (function myModule() {
var myPublic = 'web工程师的自我修养'
var mywx = ['tangtang1996918'];
function joinMyPublic() {
console.log('join web工程师的自我修养')
}
function pushMywx() {
console.log(mywx.push('your'))
}
return {
joinMyPublic: joinMyPublic,
pushMywx: pushMywx
}
})();

foo.joinMyPublic();
foo.pushMywx();

是不是在项目中很常见? 我靠,这就是模块机制啊,兄弟!!!把一些绝妙的方法都劫持过来放在自己的小空间里面,想拿来用就拿来用,岂不美哉???当然,现代的模块机制肯定没这么简单,嘻嘻。

4. 强大的闭包

认识了闭包,好好想想过去一天中你所写的代码,里面有闭包吗?这是一种日常且强大的模式。

包管理器或者管理模块机制都会将模块定义模块引入进行封装。封装到现在,已经变成了importexport或者其他的语法糖。

评论