js 闭包

JS 闭包–一点点心得体会

相信熟悉 js 的童鞋都了解、经常使用闭包了吧?但对于我来说,也只停留在熟悉、使用上,要问我为什么会产生闭包,我也只能笼统地说:内层作用域可以访问到外层作用域的变量。究其根本也不甚了解。今天有时间来分享一下这一两天的相关学习。

起因

万事有果皆有因,要问我为什么突然要学习 js 闭包,其实是下面一个问题:

1
2
3
4
5
6
7
8
9
10
11
function a() {
var variableInA = 'A variable in [[scope A]]';

new Promise((res, rej) => {
setTimeout(() => {
res('pass after setTimeout with 1000ms');
}, 1000);
}).then((data) => {
console.log('data in then: ', data, 'variableInA is:', variableInA);
});
}

从函数的角度来说,执行 a () 是一个同步的运行方式,运行完 new Promise ().then () 之后就会将 a () 创建的环境销毁(没有闭包的情况下)。我之前所理解的闭包是函数 a 运行返回一个函数 b,函数 b 中保持了 a 中的变量引用,那么函数 a 所创建的环境就不会被销毁(我也不太清楚是被引用的变量不会被销毁,还是说所有都不会 更新:2018-11-01 17:26:28:只有被引用了的才会产生闭包,应该是在执行前解释的时候决定的。但是讲道理只保留被引用的变量能最大程度地节省内存,不过对 gc 的要求就更多,需要去判断哪些被引用,哪些被间接地引用等等,chrome 的 V8 貌似属于只保留引用变量的那种)

b b

从上面两图来看,前图是只引用了一个变量 a,所以闭包中只有 a;后图中 a 是一个函数,引用了变量对象中的 b,所以 fn 函数的闭包中既有 a 又有 b。验证成功。大家可自行把玩 chrome 的 source 面板。

回到主题,这里的 Promise 并不是被返回的,而只是一个执行的步骤,但确实保留了对 a 创建的 variableInA 的引用,成为了所谓的闭包。不解。so 开始学习。

经过

首先了解了几个概念:变量对象、作用域链、调用栈。

变量对象

变量对象就是创建一个新的块级作用域时会先产生的一个对象,如下图:

b

在匿名调用 foo 函数后,进入了 foo 函数(此时还没开始执行代码),然后看红框,Local,这个可以理解成变量对象,此时函数内所有声明的变量、函数都会被列出来,var 有变量提升,funciton 有优先解释的权利,所以 Local 中 a、b 被提前声明,但值未赋,function 被提至最前。随着代码执行,逐一被赋值。这个就是变量对象。

作用域链

借上图,红框上面有 5 个大字(母),Scope,对,就是这个东西,作用域链。就是查找一个变量、函数啥的时候,由里到外,由上到下(针对 chrome 浏览器给出的这种 Scope 的顺序)地查找。当在某一层查找到了,就不往外再查找了;都查不到就返回 undefined。这就是作用域链。and,还有一个 problem,就是查得越深,就越慢。需要 notice 一下。

调用栈

还借上图,红框上面的上面有 9 个大字(母),call stack,对,就是这个东西,调用栈。它表示了函数的调用顺序。本例中是 windows 全局中匿名调用 foo。当调用开始或者结束的时候,call stack 会将当前环境压入或者弹出。

闭包

有了上面三个概念,这个就好说了:闭包就是 - 函数 - 保留有 - 其定义所在位置的(词法作用域)- 变量对象 - 的引用。没错,是这样的!为了方便断句和理解,我加了中划线。在前面的图中,由于 fn 指向了 baz 函数,导致 baz 函数在执行时,保留了 foo 函数执行时的变量对象(中的属性 a)的引用。

结果

上面一句话把闭包总结出来了,其实不知道这么说严不严格,但这对我来说很好理解。也让我想起了平时中一些经常使用但没细想的东西,打个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function a() {
var inputEle = document.getElementById('inputName');

setTimeout(() => {
console.log('Now start in timeout');
inputEle.value = 666;
}, 0);
console.log('Now end a().');
}
a(); // 执行a
/*
这里setTimeout就是在执行a()时创建(定义)的,所以在执行setTimeout中的匿名回调函数(注意,这是个函数,所以)保留了a执行时的活动对象,其中保留了对inputEle属性的引用,这就是闭包的使用。
*/

再次强调,闭包中的引用,就是 js 中真正意义上的引用。对于原始值是简单地复制,对于引用对象就是直接引用。所以利用闭包也可以方便地实现单例模式。