浏览器工作原理与实践
李兵
前盛大创新院高级研究员
立即订阅
6167 人已学习
课程目录
已完结 42 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 参透了浏览器的工作原理,你就能解决80%的前端难题
免费
宏观视角下的浏览器 (6讲)
01 | Chrome架构:仅仅打开了1个页面,为什么有4个进程?
02 | TCP协议:如何保证页面文件能被完整送达浏览器?
03 | HTTP请求流程:为什么很多站点第二次打开速度会很快?
04 | 导航流程:从输入URL到页面展示,这中间发生了什么?
05 | 渲染流程(上):HTML、CSS和JavaScript,是如何变成页面的?
06 | 渲染流程(下):HTML、CSS和JavaScript,是如何变成页面的?
浏览器中的JavaScript执行机制 (5讲)
07 | 变量提升:JavaScript代码是按顺序执行的吗?
08 | 调用栈:为什么JavaScript代码会出现栈溢出?
09 | 块级作用域:var缺陷以及为什么要引入let和const?
10 | 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?
11 | this:从JavaScript执行上下文的视角讲清楚this
V8工作原理 (3讲)
12 | 栈空间和堆空间:数据是如何存储的?
13 | 垃圾回收:垃圾数据是如何自动回收的?
14 | 编译器和解释器:V8是如何执行一段JavaScript代码的?
浏览器中的页面循环系统 (6讲)
15 | 消息队列和事件循环:页面是怎么“活”起来的?
16 | WebAPI:setTimeout是如何实现的?
17 | WebAPI:XMLHttpRequest是怎么实现的?
18 | 宏任务和微任务:不是所有任务都是一个待遇
19 | Promise:使用Promise,告别回调函数
20 | async/await:使用同步的方式去写异步代码
浏览器中的页面 (8讲)
21 | Chrome开发者工具:利用网络面板做性能分析
22 | DOM树:JavaScript是如何影响DOM树构建的?
23 | 渲染流水线:CSS如何影响首次加载时的白屏时间?
24 | 分层和合成机制:为什么CSS动画比JavaScript高效?
25 | 页面性能:如何系统地优化页面?
26 | 虚拟DOM:虚拟DOM和实际的DOM有何不同?
27 | 渐进式网页应用(PWA):它究竟解决了Web应用的哪些问题?
28 | WebComponent:像搭积木一样构建Web应用
浏览器中的网络 (3讲)
29 | HTTP/1:HTTP性能优化
30|HTTP/2:如何提升网络速度?
31|HTTP/3:甩掉TCP、TLS 的包袱,构建高效网络
浏览器安全 (5讲)
32 | 同源策略:为什么XMLHttpRequest不能跨域请求资源?
33 | 跨站脚本攻击(XSS):为什么Cookie中有HttpOnly属性?
34 | CSRF攻击:陌生链接不要随便点
35 | 安全沙箱:页面和系统之间的隔离墙
36 | HTTPS:让数据传输更安全
结束语 (1讲)
结束语 | 大道至简
课外加餐 (4讲)
加餐一|浏览上下文组:如何计算Chrome中渲染进程的个数?
加餐二|任务调度:有了setTimeOut,为什么还要使用rAF?
加餐三|加载阶段性能:使用Audits来优化Web性能
加餐四|页面性能工具:如何使用Performance?
浏览器工作原理与实践
登录|注册

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

李兵 2019-08-27
上一篇文章中我们讲到了什么是作用域,以及 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/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《浏览器工作原理与实践》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(55)

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

    作者回复: 分析的没问题

    2019-08-27
    2
    13
  • pyhhou
    思考题,最后输出的都是 “极客邦”,这里不会产生函数闭包,解释如下:

    1. bar 不是一个函数,因此 bar 当中的 printName 其实是一个全局声明的函数,bar 当中的 myName 只是对象的一个属性,也和 printName 没有联系,如果要产生联系,需要使用 this 关键字,表示这里的 myName 是对象的一个属性,不然的话,printName 会通过词法作用域链去到其声明的环境,也就是全局,去找 myName

    2. foo 函数返回的 printName 是全局声明的函数,因此和 foo 当中定义的变量也没有任何联系,这个时候 foo 函数返回 printName 并不会产生闭包

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

    2019-08-27
    9
  • 李懂
    本来对这篇文章充满期待,看完后还是有很多疑惑
    又翻看了一下小红书
    有以下疑问:

    1. 最后的分析图是不是有问题,全局上下文中变量环境怎么会有myName
    foo上下文中的innerBar是对象,用了函数?

    2.闭包是存在调用栈里的,现在的模块化存在大量闭包,那不是调用栈底部存在大量闭包
    很容易栈溢出吧
    3.看了下chrome中函数对应的[[Scopes]]是个List集合包含了闭包模块,这个是不是文章中的outer

    4.闭包是包含了整个变量环境和词法环境,还是只是包含用到的变量

    作者回复: 第一个我的疏忽,图明天改正过来。

    第二个问题:当闭包函数执行结束之后,执行上下文都从栈中弹出来,只不过被内部函数引用的变量不会被垃圾回收,这块内容要到讲v8 GC那节来讲了。

    第三个没明白意思

    第四个是 只包含用到的变量,这是因为在返回内部函数时,JS引擎会提前分析闭包内部函数的词法环境,有引用的外部变量都不会被gc回收。

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

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

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

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

    2019-08-27
    1
    3
  • Andy Jiang
    Local–>Closure(foo)–>Global,是不是表示setName执行时的执行上下文中的outer是指向闭包的?闭包中是否有outer指向全局执行上下文?
    2019-10-11
    2
  • YBB
    有几个问题,还想请老师解惑:
    1. 返回的内部函数在运行时,由于其外部函数的执行上下文已经出栈,其执行上下文中的outer指向何处?
    2. 如果函数嵌套产生多个闭包,是否也是类似于作用域链一样的机制,提供内部函数按序在闭包中查找变量?
    2019-09-18
    2
  • 忘忧草的约定
    老师我想请教一个问题:函数执行上下文是在函数执行前的编译阶段存入执行栈的、那么执行上下文中的outer也是在编译阶段通过分析函数声明的位置来赋值的吗?

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

    2019-08-29
    2
  • Marvin
    请问
    console.log(a)
    {
      function a(){}
    }
    为何会log一个undefined?目测function的变量提升会受到块的影响,这是标准浏览器的特性造成的,还是IE6时代就是这样呢?

    作者回复:
    这个问题我在前面回答过一次了,重新贴下:

    ES规定函数只不能在块级作用域中声明,
    function foo(){
        if(true){
            console.log('hello world');
            function g(){ return true; }
        }
    }
    也就是说,上面这行代码执行会报错,但是个大浏览器都没有遵守这个标准。

    接下来到了ES6了,ES6明确支持块级作用域,ES6规定块级作用域内部声明的函数,和通过let声明变量的行为类似。

    规定的是理想的,但是还要照顾实现,要是完全按照let的方式来修订,会影响到以前老的代码,所以为了向下兼容,个大浏览器基本是按照下面的方式来实现的:

    function foo(){
        if(true){
            console.log('hello world');
            var g = function(){return true;}
        }
    }

    这就解释了你的疑问,不过还是不建议在块级作用域中定义函数,很多时候,简单的才是最好的。

    2019-08-27
    2
    2
  • 江南逰子
    请老师帮忙答疑解惑,下面是我的代码,虽然问题好解决,但是不太明白:
    import { message } from 'antd'
    export const generateRemark = (skus, message) => {
      let remark = ''
      skus.forEach(v => {
        if (v.a && v.p) remark = remark + `${v.a}*${v.p};`
      })
      remark = remark + (message || '')
      return remark
    }
    当我调用generateRemark(skus, ''),返回是[object Object],message被解析成了antd的message组件。
    2019-11-25
    1
  • gigot
    var a=0;
    if(true){
     a = 1;
     function a(){};
     a=21;
     console.log(a)
    }
    console.log(a)
    老师,在 chrome 中,这里的 if 块语句有 a 函数,形成块级作用域了,所以函数 a 的声明被提升到 a = 1 之前;但是接下执行 function a() {} 的时候,全局的 a 的值却改变成 1 了, 导致最终输出为 21 和 1。想问下老师,这里面全局 a 是怎么改变的
    2019-10-30
    1
  • 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
    1
  • ChaoZzz
    不会产生闭包,函数在被创建的时候它的作用域链就已经确定了,所以不论是直接调用bar对象中的printName方法还是在foo函数中返回的printName方法他们的作用域链都是[自身的AO, 全局作用域],自身的AO中没有myName就到全局中找,找到了全局作用域中的myName = ' 极客邦 ',所以两次打印都是“极客邦”啦~

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

    2019-08-27
    1
  • ytd
    不会产生闭包,都打印极客邦。printName函数定义时的执行上下文是全局,所以会在全局词法环境和变量环境下找myName。

    作者回复: 嗯,词法作用域是关键

    2019-08-27
    1
  • mfist
    1. _printName是一个全局函数,执行的话 不会访问到内部变量。输出全局变量的myName 极客邦
    2. bar.printName 同样输出是极客邦

    随着专栏的推进,发现看一遍文章的时间一直在增长。发现了很多的知识盲区,很多内容只是知道,不知道底层原理。

    今日得到:作用域链如何选择,闭包如何形成

    作者回复: 加油,词法作用域影响到作用域链,这点很关键

    2019-08-27
    1
  • 程力辛
    所以变量环境是动态的,根据函数调用关系。词法环境是静态的,根据函数定义时的状态?

    作者回复: 都是静态的,动态绑定的this下节内容讲,this系统和作用域链是两套不一样的系统

    2019-08-27
    1
    1
  • Geek_East
    如果从定义拓展来看,可不可以说闭包是在作用域层面的,因为对象本身并不构成作用域,所以最后留的问题,其实是产生了闭包,不过变量是来自于global scope的。
    2019-12-08
  • Geek_East
    看了很多评论,有些疑惑的地方:
    闭包之所以没有产生是因为,闭包的界定在内部函数与外部函数直接的,而且闭包本身是在scope范围内的,跟执行上下文没有关系;在返回bar.printName的时候,myName的查找首先肯定不会是time.geekbang,com, 因为对象本身并不能成为scope,再往上一层查找的时候,根部不会查找到foo里面的极客时间,因为scope查找是静态的,根据代码位置决定的,所以就找打了global scope里面的极客邦。

    最关键的一点是:
    执行上下文的机制遵循stack LIFO原则,是动态的,是运行时的东西;标识符的查找则是通过代码字面上的位置进行查找,是静态的,是编译时的东西;这两种机制搞在一起,确实容易难倒很多人;但是只要分的开,就好多了;

    另外想问一个问题是:scope的这种寻找机制貌似是通过outer引用机制实现的,而且会从词法环境找到变量环境再横跨不同的执行上下问,这样理解可以吗?
    2019-12-08
  • Geek_East
    词法作用域的查找机制,可以不可以说是通过outer引用来实现的;而执行上下文的运行机制,则是由调用栈来实现的。
    2019-12-08
收起评论
55
返回
顶部