你以为你真的理解 Closure 吗

用户头像
大导演
关注
发布于: 2020 年 07 月 18 日
你以为你真的理解 Closure 吗

闭包(closure),作为前端面试中老生常谈的话题,经久不衰。今天我们就一起来深入理解一下闭包吧!

要理解闭包,首先得理解作用域链。那我们就从作用域开始咯。

作用域(scope)



作用域就是变量或者函数的可访问范围。JavaScript 中有全局作用域、函数作用域以及 ES6 中增加的块级作用域。我们看一下下面的代码:



function bar() {
console.log(name);
}
function foo() {
var name = 'foo';
bar();
}
var name = 'global';
foo();



相信你已经有自己的答案了,这里打印的是 'global';当执行到 bar 函数内部的时候,调用栈的状态如下图:



bar 函数执行时的调用栈



variable enviroment:变量环境,当声明变量时使用。var 声明的变量(能穿透 iffor 语句)会被存入变量环境。

lexical enviroment:词法环境,当获取变量或者 this 值时使用。letconst 声明的变量会被存入词法环境。函数内部声明的变量与函数内部块声明的变量,存放在不同的内存,可以理解为词法环境也是一个栈型结构。为了方便理解,请参照下面的代码:



function fn() {
var a = 1;
let b = 2;
{
var c = 3;
let b = 4;
let d = 5;
console.log('inner', a, b, c, d); // inner, 1,4,3,5
}
console.log('outer', a, b, c); // outer, 1,2,3
}
fn();



fn 执行时,调用栈情况如下图:



fn 函数执行时的调用栈



好了,回到我们上面的问题,不知道你有没有疑惑过,bar 函数是在 foo 函数里面调用的,为什么不是打印 'foo' 呢?其实,问题的关键就在于,执行到 bar 函数内部时,是找全局作用域的 name,还是 foo 函数作用域的 name。那要解释清楚这个问题,首先要理解作用域链。

作用域链



每个执行上下文的变量环境中,都包含一个外部引用,指向外部的执行上下文,我们把这个外部引用称为 outer



当 JavaScript 引擎需要使用一个变量时,会首先在当前环境(即当前执行上下文)去找,如果找不到,会去找 outer 指向的外部执行上下文。当然,如果找到 global 也找不到,那就是 undefined 了。为了方便理解,请参照下图:



带有外部引用 outer 的调用栈



至此,我们可以给作用域链下一个定义了:

JavaScript 引擎通过 outer 查找变量的这个链条就被称为 作用域链



看到这里,你是否会有第二个疑问呢?bar 函数是在 foo 函数内部调用的,为什么 bar 函数的 outer 指向的是全局执行上下文,而不是 foo 函数执行上下文呢?这里就涉及到 词法作用域 的知识了。在 JavaScript 执行过程中,其作用域链是由词法作用域决定的。下面,就来了解一下词法作用域。

词法作用域



词法作用域 就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。



那一开始那段代码,它的词法作用域链就是这样的:

foo 函数作用域 -> 全局作用域

bar 函数作用域 -> 全局作用域



根据词法作用域,bar 函数,foo 函数的上级作用域都是全局作用域。所以,当访问 bar 函数内部没有的变量时,就会去全局作用域查找。这也就解释了为什么 bar 函数的 outer 指向的是全局执行上下文而不是 foo 函数执行上下文。



最后,词法作用域是在词法分析阶段就已经确定了,与函数调用没有什么关系。换言之,词法作用域只关心变量、函数声明定义的位置。而动态作用域才关心函数是何处调用的,即函数调用是由运行时(runtime)确定的。



有了这些前置知识之后,我们再来聊一聊 闭包

闭包(closure)



闭包在 JavaScript 中可以说是史诗级的存在,但在 JavaScript 标准 中却找不到有关闭包的定义。这个事情也是很神奇。让我们结合下面这段代码来理解闭包吧!



var name = 'global';
function foo() {
var name = 'foo';
var bar = function() {
console.log(name);
}
return bar;
}
var f = foo();
f();



想必聪明的你已经知道答案了,这里会打印 'foo';不知你是否会有同样的疑问,foo 函数执行完成后就被销毁了呀,那么,是怎么访问到 foo 函数内部的 name 的呢?那我们一起来分析一下调用栈的情况:



foo 函数执行时的调用栈



当 foo 函数执行完后,bar 函数返回到外部被 f 函数保存,f 函数执行时调用栈的情况如下:



bar 函数执行时的调用栈



JavaScript 中,根据词法作用域,内部函数总是可以访问外部函数的变量的。当 bar 函数被返回到外部时,即使内部函数已经执行完毕,但内部函数引用外部函数的变量仍然保存在内存中,我们就将这些变量的集合称之为闭包。我们可以在 Chrome devtools 看到闭包的情况,你也可以自己动手去尝试一下。



Closure



好了,弄清楚闭包是如何产生的之后,那闭包有哪些用途呢?

  • 可以访问内部函数的变量

  • 让变量始终保存在内存



闭包使用不当会导致内存泄漏,所以,在实际开发中正确的使用闭包尤为重要。在使用闭包时,应该时刻记住,如果该闭包不是一直使用,且占用内存又比较大,那么应该设计成局部变量持有的闭包。这样, 在函数执行完毕销毁后,JavaScript 引擎会在下次垃圾回收时判断闭包是否已经不再使用,JavaScript 引擎就会回收这块内存。



好了,介绍到这里,想必你对闭包已经有更深刻的理解了。如果觉得有帮助,或者想帮助到更多的人,欢迎点赞分享~



参考

极客时间 · 浏览器工作原理与实践专栏

极客时间 · 重学前端专栏

winter · 如何写技术文章方法论



发布于: 2020 年 07 月 18 日 阅读数: 1413
用户头像

大导演

关注

导演出品,必属精品 2019.01.15 加入

github:https://github.com/directorcn,欢迎 star ⭐

评论 (1 条评论)

发布
用户头像
可以,不错
2020 年 07 月 21 日 10:23
回复
没有更多了
你以为你真的理解 Closure 吗