浏览器工作原理与实践
李兵
前盛大创新院高级研究员
56402 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 46 讲
浏览器工作原理与实践
15
15
1.0x
00:00/00:00
登录|注册

10 | 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

思考时间
闭包
块级作用域中的变量查找
作用域链
作用域链和闭包

该思维导图由 AI 生成,仅供参考

上一篇文章中我们讲到了什么是作用域,以及 ES6 是如何通过变量环境和词法环境来同时支持变量提升和块级作用域,在最后我们也提到了如何通过词法环境和变量环境来查找变量,这其中就涉及到作用域链的概念。
理解作用域链是理解闭包的基础,而闭包在 JavaScript 中几乎无处不在,同时作用域和作用域链还是所有编程语言的基础。所以,如果你想学透一门语言,作用域和作用域链一定是绕不开的。
那今天我们就来聊聊什么是作用域链,并通过作用域链再来讲讲什么是闭包
首先我们来看下面这段代码:
function bar() {
console.log(myName)
}
function foo() {
var myName = "极客邦"
bar()
}
var myName = "极客时间"
foo()
你觉得这段代码中的 bar 函数和 foo 函数打印出来的内容是什么?这就要分析下这两段代码的执行流程。
通过前面几篇文章的学习,想必你已经知道了如何通过执行上下文来分析代码的执行流程了。那么当这段代码执行到 bar 函数内部时,其调用栈的状态图如下所示:
执行 bar 函数时的调用栈
从图中可以看出,全局执行上下文和 foo 函数的执行上下文中都包含变量 myName,那 bar 函数里面 myName 的值到底该选择哪个呢?
也许你的第一反应是按照调用栈的顺序来查找变量,查找方式如下:
先查找栈顶是否存在 myName 变量,但是这里没有,所以接着往下查找 foo 函数中的变量。
在 foo 函数中查找到了 myName 变量,这时候就使用 foo 函数中的 myName。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

JavaScript中的作用域链和闭包是本文的重点讨论内容。通过分析代码执行流程和调用栈的状态图,文章解释了作用域链是如何决定变量查找顺序的。词法作用域的概念被用来解释为什么函数的外部引用是全局执行上下文而不是调用函数的执行上下文。此外,文章还介绍了块级作用域中变量的查找过程。通过图示和代码分析,读者可以深入理解JavaScript中作用域链和闭包的工作原理。文章内容深入浅出,适合帮助读者快速了解并掌握这些技术特点。 在闭包的讨论中,文章通过具体的代码示例和调用栈状态图展示了闭包的产生过程,以及闭包中变量的访问和修改过程。此外,文章还简要介绍了闭包的回收机制,提醒读者在使用闭包时要注意内存泄漏的问题。总结部分回顾了作用域链和闭包的核心概念,并提出了思考题,引导读者深入思考和理解闭包的应用场景。 通过本文的阅读,读者可以深入理解JavaScript中的作用域链和闭包的概念,以及它们在实际开发中的应用。同时,文章通过具体的代码示例和图示,帮助读者直观地理解这些概念,为进一步深入学习和应用JavaScript提供了良好的基础。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《浏览器工作原理与实践》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(118)

  • 最新
  • 精选
  • 蓝配鸡
    var bar = { myName:"time.geekbang.com", printName: function () { console.log(myName) } } function foo() { let myName = " 极客时间 " return bar.printName } let myName = " 极客邦 " let _printName = foo() _printName() bar.printName() 全局执行上下文: 变量环境: Bar=undefined Foo= function 词法环境: myname = undefined _printName = undefined 开始执行: bar ={myname: "time.geekbang.com", printName: function(){...}} myName = " 极客邦 " _printName = foo() 调用foo函数,压执行上下文入调用栈 foo函数执行上下文: 变量环境: 空 词法环境: myName=undefined 开始执行: myName = " 极客时间 " return bar.printName 开始查询变量bar, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找outer词法环境(没有)-> 查找outer语法环境(找到了)并且返回找到的值 pop foo的执行上下文 _printName = bar.printName printName()压bar.printName方法的执行上下文入调用栈 bar.printName函数执行上下文: 变量环境: 空 词法环境: 空 开始执行: console.log(myName) 开始查询变量myName, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找outer词法环境(找到了) 打印" 极客邦 " pop bar.printName的执行上下文 bar.printName() 压bar.printName方法的执行上下文入调用栈 bar.printName函数执行上下文: 变量环境: 空 词法环境: 空 开始执行: console.log(myName) 开始查询变量myName, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找outer词法环境(找到了) 打印" 极客邦 " pop bar.printName的执行上下文

    作者回复: 分析步骤很详细 👍

    2019-08-27
    21
    213
  • 许童童
    思考题: 这道题其实是个障眼法,只需要确定好函数调用栈就可以很轻松的解答,调用了foo()后,返回的是bar.printName,后续就跟foo函数没有关系了,所以结果就是调用了两次bar.printName(),根据词法作用域,结果都是“极客邦”,也不会形成闭包。 闭包还可以这样理解:当函数嵌套时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局作用域下可访问时,就形成了闭包。

    作者回复: 分析的没问题

    2019-08-27
    13
    89
  • pyhhou
    思考题,最后输出的都是 “极客邦”,这里不会产生函数闭包,解释如下: 1. bar 不是一个函数,因此 bar 当中的 printName 其实是一个全局声明的函数,bar 当中的 myName 只是对象的一个属性,也和 printName 没有联系,如果要产生联系,需要使用 this 关键字,表示这里的 myName 是对象的一个属性,不然的话,printName 会通过词法作用域链去到其声明的环境,也就是全局,去找 myName 2. foo 函数返回的 printName 是全局声明的函数,因此和 foo 当中定义的变量也没有任何联系,这个时候 foo 函数返回 printName 并不会产生闭包

    作者回复: 分析的没问题 很赞

    2019-08-27
    2
    71
  • 李艺轩
    关于闭包的概念: 老师提出的概念:内部函数引用外部函数的变量的集合。 高级程序设计中的概念:闭包是指有权访问另一个函数作用域中的变量的函数。 MDN上的概念:闭包是函数和声明该函数的词法环境的组合。 所以到底哪个是对的。。MDN = 老师 + 高程

    作者回复: 很高兴终于有人提这个问题了,我的观点是不要太纠结于概念,因为如何定义闭包不会影响到实际的使用,了解闭包是如何产生的,这才是本质的东西。

    2019-09-04
    7
    55
  • 李懂
    本来对这篇文章充满期待,看完后还是有很多疑惑 又翻看了一下小红书 有以下疑问: 1. 最后的分析图是不是有问题,全局上下文中变量环境怎么会有myName foo上下文中的innerBar是对象,用了函数? 2.闭包是存在调用栈里的,现在的模块化存在大量闭包,那不是调用栈底部存在大量闭包 很容易栈溢出吧 3.看了下chrome中函数对应的[[Scopes]]是个List集合包含了闭包模块,这个是不是文章中的outer 4.闭包是包含了整个变量环境和词法环境,还是只是包含用到的变量

    作者回复: 第一个我的疏忽,图明天改正过来。 第二个问题:当闭包函数执行结束之后,执行上下文都从栈中弹出来,只不过被内部函数引用的变量不会被垃圾回收,这块内容要到讲v8 GC那节来讲了。 第三个没明白意思 第四个是 只包含用到的变量,这是因为在返回内部函数时,JS引擎会提前分析闭包内部函数的词法环境,有引用的外部变量都不会被gc回收。

    2019-08-27
    5
    13
  • 早起不吃虫
    作为一名前端虽然这些都是早已熟悉的,但是老师讲的确实是好呀,深入浅出,逻辑清晰,期待后面的课程!

    作者回复: 我争取把每篇内容讲的通俗易懂,不过这相当有挑战。

    2019-08-27
    11
  • hzj.
    首先两个函数都会打印 : 极客邦 社区中对闭包的定义: 函数执行产生私有作用域, 函数内部返回一个调用的函数, 由于外部会拿到内部函数的返回值, 所以内部函数不会被垃圾回收, 这个私有作用域就是闭包. 闭包的作用有两点: 1. 保护私有变量 2. 维持内部私有变量的状态 但是在 sicp (计算机程序的构造与解释) 中认为: 只要函数调用, 那么就会产生闭包. 所以, 我认为是会产生闭包的 _printName() 输出 极客邦, 因为 _printName拿到了bar.printName, 打印上面的 myName即可. bar.printName() 输出 极客邦, 因为会直接打印全局的 myName. 最后, 只有在 foo() 函数中有 log, 才会输出 "极客时间", 因为 这个值是在 foo 函数的私有作用域中的!!!

    作者回复: 分析的没问题,你把闭包的概念外延扩大也没问题,分析思路很赞

    2019-08-27
    3
    9
  • oc7
    function foo() { var myName = " 极客时间 " let test1 = 1 const test2 = 2 var innerBar = { getName:function(){ console.log(test1) return myName }, setName:function(newName){ myName = newName } } return innerBar } var bar = foo() bar.setName(" 极客邦 ") bar.getName() console.log(bar.getName()) 有个问题没搞明白 在return innerBar的时候 bar.setName(" 极客邦 ")和bar.getName()这两个函数还没有执行 为什么会执行词法作用域的分析 之前不是说只有函数调用时才创建这个函数的执行作用域和可执行代码

    作者回复: 这是预分析过程,主要是查看内部函数是否引用了外部作用域变量,用来判断是否要创建闭包,所以预分析过程并不是编译过程!

    2019-09-04
    7
  • 忘忧草的约定
    老师我想请教一个问题:函数执行上下文是在函数执行前的编译阶段存入执行栈的、那么执行上下文中的outer也是在编译阶段通过分析函数声明的位置来赋值的吗?

    作者回复: 是的 编译阶段就确定了

    2019-08-29
    3
    6
  • ChaoZzz
    不会产生闭包,函数在被创建的时候它的作用域链就已经确定了,所以不论是直接调用bar对象中的printName方法还是在foo函数中返回的printName方法他们的作用域链都是[自身的AO, 全局作用域],自身的AO中没有myName就到全局中找,找到了全局作用域中的myName = ' 极客邦 ',所以两次打印都是“极客邦”啦~

    作者回复: 分析没问题,不过es6已经不用ao了,这块知识可以更新下了

    2019-08-27
    4
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部