前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >medium 五万赞好文-《我永远不懂 JS 闭包》

medium 五万赞好文-《我永远不懂 JS 闭包》

作者头像
掘金安东尼
发布2022-09-19 10:27:45
3440
发布2022-09-19 10:27:45
举报
文章被收录于专栏:掘金安东尼

theme: smartblue

正如《你不知道的 JavaScript》书中所言:

闭包就好像从 JavaScript 中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能到达那里!

在实际的开发工作中也确实如此,除了在面试的场景下,或其它几个少数特定的场景下(如“防抖节流”函数),咱意识到了 —— 这就是“闭包”!其它时候基本不用,或者是用了却不知道。

多么可惜!!这么好的东西用了却不自知。

本篇借助 medium 上的五万赞好文 I never understood JavaScript closures 带你一次吃透“闭包”!(吃不透找我,找耶稣也没用,我说的 QAQ)

image.png
image.png

看完本篇,你会惊奇的发现,竟然连以下这段代码都存在着闭包?!

代码语言:javascript
复制
let a = 1
function b(){
    return a
}
console.log(b())

“我永远不懂 JS 闭包,因为它无处不在......”

执行上下文

认知“闭包”前,咱得了解一个重要的概念 —— “执行上下文”

执行上下文分为:

  • 全局执行上下文(Global execution context):当 JS 文件加载进浏览器运行的时候,进入的就是全局执行上下文。全局变量都是在这个执行上下文中。代码在任何位置都能访问。
  • 函数执行上下文(Functional execution context):定义在具体某个方法中的上下文。只有在该方法和该方法中的内部方法中访问。

比如我们在全局执行上下文中调用一个函数的时候,JS 解析的流程大概会是这样的:

  1. JS 创建一个新的函数执行上下文(可以理解为一个临时的“执行上下文”),它有局部可访问的变量集;
  2. 该执行上下文将被放到【执行栈】里去执行(这里将【执行栈】视为一种跟踪程序执行位置的机制);
  3. 当遇到 return} 时,判定为执行结束。
  4. 该执行上下文在执行栈弹出;
  5. 被执行的函数会将返回值发送给调用它的执行上下文,这里就是发送给全局执行上下文;
  6. 函数执行上下文将被销毁,它的变量集将不能再被访问到,这也是为什么它被称为临时的“执行上下文”的原因;

关于执行栈的操作,可见下面的动图理解:

我们再结合下面这段代码举例解析,来看看 JS 一步一步到底是怎么做的:

代码语言:javascript
复制
1: let a = 3
2: function addTwo(x) {
3:   let ret = x + 2
4:   return ret
5: }
6: let b = addTwo(a)
7: console.log(b)
  1. 第一行,在全局执行上下文声明了一个变量 a,赋值为 3;
  2. 第二行到第五行是函数执行上下文。是在全局执行上下文声明了一个 addTwo 的函数,函数内部的代码不做执行,只是存储着以供后面调用;
  3. 第六行,声明了一个变量 b,赋值 b 为 addTwo 函数执行的返回值;
  4. 在全局执行上下文找到 addTwo 函数进行执行,并传入参数 3 ;
  5. 此时,执行上下文会进行切换!创建一个临时的名为 addTwo 的函数执行上下文,推到执行栈当中;
  6. 到第三行,然后是怎样?创建一个变量 ret 吗?不对,实际上,是先创建一个变量 x 然后赋值为 3 ;
  7. 第三行,声明一个变量 ret ,然后赋值为 x + 2 的运算结果。
  8. JS 会找加法的项 x 在哪?原来 x 在 addTwo 这个函数执行上下文就已经有了,它的值是 3 ,接着与 2 执行加法运算,然后赋给 ret。
  9. 然后来到了第四行,将 ret 进行 return 返回;
  10. 第四、第五行,addTwo 函数执行结束,临时的执行上下文被销毁;变量 x 和变量 ret 都会被清除;函数执行上下文将被在调用栈被推出,然后把函数返回给调用它的执行上下文,这里也就是全局执行上下文;
  11. 然后回到第 4 点,将函数返回值赋值给 b;
  12. 第七行,进行打印;

这里虽然是一段非常冗长的解释,且还并未涉及到“闭包”,但是却是我们必须要写在前面并且理解透彻的。

词法作用域

接着“执行上下文”,还有一个概念我们需要理解,它就是 “词法作用域”

来看下面这段代码示例:

代码语言:javascript
复制
1: let val1 = 2
2: function multiplyThis(n) {
3:   let ret = n * val1
4:   return ret
5: }
6: let multiplied = multiplyThis(6)
7: console.log('example of scope:', multiplied)

你能参照上一节的解释对这段代码进行描述吗?

  1. 第一行,在全局执行上下文声明了一个变量 vall,赋值为 2;
  2. 第二行至第五行声明一个 multiplyThis 函数执行上下文,内部代码不做执行,存储以供调用;
  3. 第六行,声明一个变量 multiplied,赋值为 multiplyThis 函数执行的返回值;
  4. 在全局执行上下文找到 multiplyThis 函数进行执行,并传入参数 6 ;
  5. 此时,执行上下文会进行切换!创建一个临时的名为 multiplyThis 的函数执行上下文,推到执行栈当中;
  6. 到第三行,在函数执行上下文声明一个变量 n ,并赋值为 6;
  7. 第三行,声明一个变量 ret ,然后赋值为 n 与 vall 做乘法的运算结果。n 为 6,vall 呢?在当前函数执行上下文并未找到 vall!
  8. 此时,JS 会去调用 multiplyThis 函数的全局执行上下文寻找 vall!找到了!它的值是 2,6 * 2 = 12。赋给 ret;
  9. 然后来到了第四行,将 ret 进行 return 返回;
  10. 第四、第五行,multiplyThis 函数执行结束,临时的执行上下文被销毁,变量 n 和变量 ret 都会被清除,但是 vall 没有被销毁,因为它存在于全局函数执行上下文;
  11. 回到第六行,将返回值 12 赋给变量 multiplied;
  12. 最后打印输出;

这段描述中,置灰的步骤就是和上一节的描述基本一致,未置灰的是

最重要的是:JS 在当前执行上下文寻找变量的时候,如果找不到,就会去调用这个执行上下文的上一执行上下文去寻找。

这并不难理解,这样链式查找变量的过程,就是 JS 的【作用域链】。

函数返回函数

函数可以返回任何东西,当然也就包括返回另一个函数了。

让我们再按照以上解析一遍以下代码:

代码语言:javascript
复制
 1: let val = 7
 2: function createAdder() {
 3:   function addNumbers(a, b) {
 4:     let ret = a + b
 5:     return ret
 6:   }
 7:   return addNumbers
 8: }
 9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)
  1. 第一行,在全局执行上下文声明了一个变量 val,赋值为 7;
  2. 第二行至第八行声明一个 createAdder 函数执行上下文,内部代码不做执行,存储以供调用;
  3. 第十一行,声明一个变量 adder,赋值为 createAdder 函数执行的返回值;
  4. 在全局执行上下文找到 createAdder 函数进行执行,它在第二行,ok,调用;
  5. 来到第二行,创建一个新的临时的函数执行上下文,将其推倒执行栈;
  6. 由于 createAdder 函数没有传参,直接进入函数体内,这里声明了一个函数 addNumbers,只做声明,不做执行;
  7. 来到第七行,将 addNumbers 返回到全局执行上下文,它是一个函数;然后将临时 createAdder 函数执行的上下文推出执行栈;
  8. createAdder 函数执行上下文被销毁,此时 addNumbers 也将不存在;
  9. 再到第十行,在全局执行上下文声明一个变量 sum,它的值为 adder(val, 8) 执行的返回值;
  10. 我们再到全局执行上下文寻找 adder ,找到了,它正好是一个函数,我们可以调用它;
  11. 他有两个传参,第一个是 val,它在第一行声明了并复制了,为 7,第二个参数是 8;
  12. 然后我们来到第三行到第五行,创建两个变量 a 和 b ,为他们赋值分别为 8 和 7;
  13. 第四行,声明一个变量 ret ,赋值为 8 + 7,为 15;
  14. ret 变量被 return ,临时的函数执行上下文被销毁,a ,b ,ret 变量都将被销毁;
  15. adder 函数执行的返回值赋给 sum 变量;
  16. 最后打印输出;

主角闭包!!!

看了以上三段的具体步骤详细分析,我相信再给你一段类似的调用代码,你也一定能通晓其中端倪,作出类似的解析!

代码语言:javascript
复制
 1:let counter
 2: function createCounter() {
 3:   let counter = 0
 4:   const myFunction = function() {
 5:     counter = counter + 1
 6:     return counter
 7:   }
 8:   return myFunction
 9: }
 10: const increment = createCounter()
11: const c1 = increment()
12: const c2 = increment()
13: const c3 = increment()
14: console.log('example increment', c1, c2, c3)

来试试吧?

  1. 第一行,声明一个变量 counter ,赋值为 undefined ;
  2. 第二行至第九行,在全局执行上下文声明了一个函数 createCounter ,不做执行,存储以供调用;
  3. 第十行,在全局执行上下文声明了一个变量 increment ,赋值为 createCounter 函数执行的返回值;
  4. 调用 createCounter 函数,它在第 2 步已经声明了,执行它!
  5. 第二到九行,创建一个新的临时的函数执行上下文;
  6. 在这个临时的函数执行上下文声明一个变量 counter,赋值为 0;
  7. 第四到七行,声明一个 myFunction 函数,不做执行,存储以供调用;
  8. 将 myFunction 进行返回,赋给变量 increment。createCounter 函数执行上下文被销毁,myFunction 和 counter 都将被销毁;
  9. 此时的全局执行上下文没有 myFunction 函数了,只有 increment 函数;
  10. 第十一行,声明一个变量 c1 , 赋值为 increment 函数执行的返回值;
  11. increment 函数是 createCounter 函数执行的返回值,它在第四行到第七行被定义;
  12. 创建一个新的函数执行上下文,没有传参,直接进入函数内部进行执行;
  13. 第五行,counter = counter + 1,counter 变量在新创建的函数执行上下文找的到吗?找不到!只得回到调用它的全局执行上下文去寻找,在全局执行上下文 counter 为 undefined,所以执行 counter = undefined + 1
  14. 第六行,返回 counter,值为 1,销毁新函数执行上席文;
  15. 回到第十一行,c1 赋值为 1;
  16. 第十二行,重复步骤第 10 到第 14 步,c2 同理赋值为 1;
  17. 第十三行,重复步骤第 10 到第 14 步,c3 同理赋值为 1;
  18. 最终打印输出;

究竟是这样的吗? 我们在控制台打印:

image.png
image.png

结果竟然与我们分析的期望打印相悖!

发生了什么?

一定还有一个神秘的东西在作用!没错,它就是 “闭包”!the missing piece !

它的原理是这样的:

当我们声明一个函数时,存储以供调用,存储的不仅仅是这个函数的定义,同时还有这个函数的“闭包”,闭包包括了这个函数执行上下文所有变量的词法作用域。这些在函数被创建时就已经确定下来了。

做个不太恰当的比喻,把函数理解为一个人,当这人生下来的时候(函数创建时),也附赠了一个背包(闭包),这个背包包括了家庭环境(词法作用域)。

所以我们上一段的逐步分析是错的!

我们再来进行一次正确的分析!

代码语言:javascript
复制
 1:let counter
 2: function createCounter() {
 3:   let counter = 0
 4:   const myFunction = function() {
 5:     counter = counter + 1
 6:     return counter
 7:   }
 8:   return myFunction
 9: }
 10: const increment = createCounter()
11: const c1 = increment()
12: const c2 = increment()
13: const c3 = increment()
14: console.log('example increment', c1, c2, c3)
  1. 第一行,声明一个变量 counter ,赋值为 undefined ;(同上)
  2. 第二行至第九行,在全局执行上下文声明了一个函数 createCounter ,不做执行,存储以供调用;(同上)
  3. 第十行,在全局执行上下文声明了一个变量 increment ,赋值为 createCounter 函数执行的返回值;(同上)
  4. 调用 createCounter 函数,它在第 2 步已经声明了,执行它!(同上)
  5. 第二到九行,创建一个新的临时的函数执行上下文;(同上)
  6. 在这个临时的函数执行上下文声明一个变量 counter,赋值为 0;(同上)
  7. 第四到七行,声明一个 myFunction 函数,同时还会创建一个闭包,包括这个函数执行上下文所有变量的词法作用域。此例中就是 counter,通过作用域链查找,它的值为 0。这里同样不做执行,存储以供调用;
  8. 将 myFunction 和它的闭包 一起进行返回,赋值给 变量 increment。createCounter 函数执行上下文被销毁,myFunction 和 counter 都将被销毁;
  9. 此时的全局执行上下文没有 myFunction 函数了,只有 increment 函数;(同上)
  10. 第十一行,声明一个变量 c1 , 赋值为 increment 函数执行的返回值;(同上)
  11. increment 函数是 createCounter 函数执行的返回值,它在第四行到第七行被定义;(同上)
  12. 创建一个新的函数执行上下文,没有传参,直接进入函数内部进行执行;(同上)
  13. 第五行,counter = counter + 1,counter 变量在新创建的函数执行上下文找的到吗?找不到!我们要去它的“闭包”里面找一找!,竟然是有的!它的值为 0 ,那么就可以执行 counter = 0 + 1,等于 1;
  14. 第六行,返回 counter,值为 1,销毁新函数执行上席文;(同上)
  15. 回到第十一行,c1 赋值为 1;(同上)
  16. 第十二行,重复步骤第 10 到第 14 步,我们再去“闭包”里找的时候,因为经过上一步的操作 counter 已经变为 1 了,所以再执行加法运算,counter 结果为 2
  17. 第十三行,重复步骤第 10 到第 14 步,c3 同理赋值为 3;
  18. 最终打印输出;

噢~原来是这样!

当一个函数声明的时候不单单只做了声明,后面还带着个“闭包”呢!闭包里装的是这个函数执行上下文所有变量的词法作用域!

原来这就是闭包!

你可能会疑问:只要一个函数进行了声明,就包含了闭包,那全局函数是不是也有闭包呢?

答案是:YES !

全局函数声明时,也有闭包!只不过它的变量词法作用域就是全局的,所以不是“闭”的不是很明显。

当一个函数返回另外一个函数时,“闭包”是最明显的!返回的函数的变量词法作用域可以访问非全局范围的变量,它们仅放在其闭包中!

测验

这里再放几道闭包题目,供大家自查测验:

代码语言:javascript
复制
// 题目一
let c = 4
function addX(x) {
  return function(n) {
     return n + x
  }
}
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)


// 题目二

function multiply(number1, number2) {
  if (number2 !== undefined) {
    return number1 * number2;
  }
  return function doMultiply(number2) {
    return number1 * number2;
  };
}

multiply(4, 5); 
const double = multiply(2);
double(5);

// 题目三

function showBiBao() {
    for (var i = 0; i < 5; i++) {
      setTimeout( timer => () {
          console.log(i);
      }, 1000 );
    }
    console.log(i)
}

showBiBao()

function showListNumber() {
    for(var i = 0; i < 5; i++) {
        let ret = function(i) {
            setTimeout(function timerr() {
                console.log(i)
            }, 1000)
        }
        ret(i)
    }
    console.log(i)
}
showListNumber()

我相信如果你从头至尾认真跟着分析步骤来了,这一定不算啥大问题~

欢迎讨论 ~

总结

通过本篇我们知道了:只要有函数申明的地方就有闭包!只不过有时候“闭”的不那么明显。

为什么题目说《我永远不懂 JS 闭包》呢?其实你也看到了,开发工作中,即使你没有用到闭包或者根本不认识闭包也一样摸鱼打卡上下班。但问题的关键是闭包吗?

不!

你以为本篇是在讲闭包?错!本篇是在讲执行上下文!

你以为本篇是在讲执行上下文?错!本篇是在讲词法作用域!

你以为本篇是在讲词法作用域?错!本篇是在讲 JS 的动态语言特性!

你一问本篇是在讲 JS 的动态语言?错!本篇是在讲 JS 运行的单线程特性!

......

没错,哎,就是玩儿~

v2-cc922db944e651867447654f0858d44f_b.webp
v2-cc922db944e651867447654f0858d44f_b.webp

撰文不易✍,点赞鼓励👍,你的反馈💬,我的动力🚀

我是掘金安东尼,关注公众号【掘金安东尼】,关注前端技术,也关注生活,持续输出ing!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-04-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • theme: smartblue
  • 执行上下文
  • 词法作用域
  • 函数返回函数
  • 主角闭包!!!
  • 测验
  • 总结
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档