浏览器工作原理与实践
李兵
前盛大创新院高级研究员
立即订阅
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?
浏览器工作原理与实践
登录|注册

09 | 块级作用域:var缺陷以及为什么要引入let和const?

李兵 2019-08-24
在前面《07 | 变量提升:JavaScript 代码是按顺序执行的吗?》这篇文章中,我们已经讲解了 JavaScript 中变量提升的相关内容,正是由于 JavaScript 存在变量提升这种特性,从而导致了很多与直觉不符的代码,这也是 JavaScript 的一个重要设计缺陷
虽然 ECMAScript6(以下简称 ES6)已经通过引入块级作用域并配合 let、const 关键字,来避开了这种设计缺陷,但是由于 JavaScript 需要保持向下兼容,所以变量提升在相当长一段时间内还会继续存在。这也加大了你理解概念的难度,因为既要理解新的机制,又要理解变量提升这套机制,关键这两套机制还是同时运行在“一套”系统中的。
但如果抛开 JavaScript 的底层去理解这些,那么你大概率会很难深入理解其概念。俗话说,“断病要断因,治病要治根”,所以为了便于你更好地理解和学习,今天我们这篇文章会先“探病因”——分析为什么在 JavaScript 中会存在变量提升,以及变量提升所带来的问题;然后再来“开药方”——介绍如何通过块级作用域并配合 let 和 const 关键字来修复这种缺陷。

作用域(scope)

为什么 JavaScript 中会存在变量提升这个特性,而其他语言似乎都没有这个特性呢?要讲清楚这个问题,我们就得先从作用域讲起。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《浏览器工作原理与实践》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(50)

  • 山里娃
    【最终打印结果】:VM6277:3 Uncaught ReferenceError: Cannot access 'myname' before initialization
    【分析原因】:在块作用域内,let声明的变量被提升,但变量只是创建被提升,初始化并没有被提升,在初始化之前使用变量,就会形成一个暂时性死区。
    【拓展】
    var的创建和初始化被提升,赋值不会被提升。
    let的创建被提升,初始化和赋值不会被提升。
    function的创建、初始化和赋值均会被提升。

    作者回复: 很好,这个答案大家可以参考下

    2019-08-24
    8
    90
  • 这篇真的是神作啊。 有一个疑问,在abcd那个例子里,第一步<编译并创建执行上下文>的图里并没有块级作用域的b=undefined; d=undefined。而在第二步里<继续执行代码>的图中才出现b=undefined; d=undefined。想问下这个块级作用域的b=undefined; d=undefined是不是应该在第一步的编译阶段里就有。还是说在执行阶段像函数那样,块级作用域会有一个自己的编译阶段

    作者回复: 执行函数时才有进行编译,抽象语法树(AST)在进入函数阶段就生成了,并且函数内部作用域是已经明确了,所以进入块级作用域不会有编译过程,只不过通过let或者const声明的变量会在进入块级作用域的时被创建,但是在该变量没有赋值之前,引用该变量JavaScript引擎会抛出错误---这就是“暂时性死区”

    2019-08-24
    2
    18
  • YBB
    有个问题,在一个块级作用域中,let和const声明的变量是在编译阶段被压入栈中还是执行阶段被压入栈中?在文中的表述来看,第一个let声明的变量是在编译阶段就压入栈中的,但是后面的变量又感觉是在执行是压入栈中,有点混乱。

    作者回复: 对的,你的理解没错

    函数只会在第一次执行的时候被编译,所以编译时变量环境和词法环境最顶层数据已经确定了。

    当执行到块级作用域的时候,块级作用域中通过let和const申明的变量会被追加到词法环境中,当这个块执行结束之后,追加到词法作用域的内容又会销毁掉。

    2019-08-26
    10
  • 李懂
    执行上下文是在编译时创建的,在执行代码的时候已经有词法环境了,而且变量已经默认初始化了undefiend,为什么还会存在暂时性死区,老师解答一下!

    作者回复: 暂时性死去是语法规定的,也就是说虽然通过let声明的变量已经在词法环境中了,但是在没有赋值之前,访问该变量JavaScript引擎就会抛出一个错误。

    2019-08-24
    3
    5
  • William
    第二步,继续执行代码。 这张图我觉得有错误,当进入foo函数内部的代码块之后,并没有了编译阶段,此时,新创建的栈顶块级作用域的内容为空,而并没有 b = undefined 和 d = undefined 两项内容。 执行完 let b = 3 之后,分配内存,块级作用域出现 b = 3 一项。 执行 let d = 5 之后,为d分配内存,栈顶块级作用域增加一项 d = 5。

    作者回复: 使用let/const声明的变量,伴随着词法环境被创建,但只有在变量的词法绑定(LexicalBinding)已经被求值运算后,才能够被访问。

    你也可以在let b声明之前断点下,看看scope中的值有没有,你会看scope中的值已经存在了。

    2019-08-24
    4
    4
  • …Lucky
    老师,按照最后的思考题。let,const会在编译阶段创建,但不赋值。但是上面几个图中都是直接赋值的undefined。这是否矛盾
    2019-09-04
    2
    3
  • 晓小东
    在ES3开始,try /catch 分句结构中也具有块作用域。补充……

    作者回复: 赞

    2019-08-25
    3
  • 爱吃锅巴的沐泡
    对文中foo()函数的分析和一些问题:
               我调试了一下,①断点打在 let b = 2,此时的scope中只有local:a = 1,b = undefined,c = undefined;并没有block,这应该说明js是解释性语言,一句一执行的。
               ②当断点走到 let b = 3时,这时进入了作用域,scope中有了block:b = undefined,d = undefined,这应该说明在进入作用域之前AST已经生成,并确定了作用域的范围。
               问题:1、老师提到在进入作用域时let声明的变量被创建,结合断点可以证明,那么是不是说 let声明的变量在该作用域内提升了,但没有提升赋值语句?因为在②处已经有了d = undefined。
               问题:2、把foo()中的作用域变形如下:
                               {
                                      let b = 3
                                      console.log(d)
                                      var c = 4
                                      let d = 5
                                      console.log(a)
                                      console.log(b)
                                 }
                       当断点走到 let b = 3处,scope的block中只有b = undefined,并没有d = undefined,是因为“暂时性死区”是js在语法上的设置,防止访问声明前的变量,而在进入作用域之前就会有语法树的生成,所以在编译到console.log(d)时,遇到错误,所以没有在词法环境中创建变量d。这样分析是否正确?
    2019-09-01
    2
  • if(0){ var myname = " 极客邦 "} 这段代码里的if条件是false很有意思。是说编译阶段不管if会不会执行。里面的代码都会编译,因此这里的myname变量提升,从而导致上面的console.log(myname)输出undefined吗?
    另外let 声明的变量会提升吗?

    作者回复: 对的,第一个分析的没问题
    第二个let不会产生变量提升

    2019-08-24
    2
  • Kyeon
    老师好,看到您在评论中说let并不会变量提升。那么,课后题浏览器报Cannot access 'myname' before initialization,看起来像是引擎已经知道有myname这么个变量了。但是如果按照let变量不会提升的话,那引擎是怎么知道myname这个变量尚未初始化的呢?谢谢~
    2019-09-17
    1
    1
  • 无名
    1<script type="text/javascript">
    2 let myname = "outer name";
    3 {
    4 let myage = 10;
    5 let myclazz = "1(4)班";
    6 console.log(myname);
    7 let myname = 'inner name';
    8 }
    9</script>

    老师,对于以上代码,我有些疑惑:
    1、断点到2的位置时:
    我的理解:myname 应该在【词法环境】中创建了【Script区】,里面:myname=undefined
    实际上:没有看到myname=undefined,执行完2时,才在右边的【Script区】中显示myname="outer name";

    2、断点到4的位置时:
    我的理解:myage、myclazz、myname应该在【词法环境】中创建了【Block区】,里面:myage=undefined、myclazz=undefined、myname=undefined;然后执行完4时,myage=10;然后执行完5时,myclazz = "1(4)班";然后执行到6时,报Cannot access 'myname' before initialization (原因是【暂时性死区】);
    实际上:在【词法环境】中是创建了【Block区】,但只看到了myage=undefined、myclazz=undefined;没有看到myname=undefined。
    2019-08-30
    1
  • 蓝配鸡
    let myname= '极客时间'
    {
      console.log(myname)
      let myname= '极客邦'
    }

    编译过程:
    生成执行上下文压入栈
    变量环境为空
    词法环境中myname=undefined压入栈

    执行过程:
    词法环境中myname=极客时间
    新开一个 myname =undefined 压入词法环境栈
    查找myname并输出undefined
    赋值当前栈头上myname=极客邦
    pop栈头

    结束

    作者回复: 还要考虑暂时性死区的问题,这个我在文中没介绍,可以自行搜索下。可以参考下其它评论。

    2019-08-24
    2
    1
  • 爱吃锅巴的沐泡
    有个疑问:
    在思考题中,
    1、执行到console.log(myname)这句话时,编译阶段已经完成,那么词法环境中的栈顶 是不是已经有了该作用域块了,let myname =‘极客邦’ 是不是也已经在栈顶的作用域快中了?
    2、执行到console.log(myname)这句话时,是按着从词法环境栈顶到栈底到变量环境的顺序查找,栈底已经存在了函数级的 let myname了,那为什么还是会报错呢?

    作者回复: 暂时性死区,你可以参考下其它评论

    2019-08-24
    1
  • mfist
    1. 在块级作用域中,从{开始到let myname= '极客邦' 代码之间会形成一个暂时性死区,如果中间去访问变量myname,会报初始化之前不能访问myname的错误。Uncaught ReferenceError

    2. 另外上面的一个foo函数也会报d没有定义吧,d在块级作用域中声明,在外面是访问不到的
    function foo(){
        var a = 1
        let b = 2
        {
          let b = 3
          var c = 4
          let d = 5
          console.log(a)
          console.log(b)
        }
        console.log(b)
        console.log(c)
        console.log(d)
    }
    foo()

    作者回复: 对的,你的分析没问题,这两行都会报错

    2019-08-24
    1
  • Geek_East
    在变量环境中,是不是也是一个小型栈结构呢?看spec好像变量环境也是一个词法环境。
    2019-12-08
  • Geek_East
    我想,理解execution context和scope的区别是理解这个问题的一个关键;很多时候执行上下文和作用域都混着说

    作者回复: 这是两样不同的东西,一个表示一个表示函数运行时的上下文,一个表示词法作用域!

    我会在下篇介绍V8的专栏中详细分析这块内容!

    2019-12-06
  • -_-_aaa
    也就是编译时,块级作用域不编译。只有在执行时会为块级作用域临时创建词法环境。
    2019-12-03
  • -_-_aaa
    我印像中词法环境是那种树形结构的对象,查找时好像是遍历树。这里说是栈结构,疑惑。
    2019-12-03
  • Bence Zhu
    for (let i = 0; i < 7; i++) {}
    老师,为什么每次循环,i都有一个新域 ?
    2019-12-02
    1
  • vianem
    词法环境跟词法作用域是不是一个东西?
    2019-11-25
    1
收起评论
50
返回
顶部