执行环境 Execution Context、宣告提升 Hoisting

早期的 JavaScript 变数只能用 var 宣告,後来 ES6 新增 let 、 const 。
这篇不会细讲三个宣告方式的差异,网路上很多大神已经解释得很好。另外,其实我现在已经没在用 var ,但用 var 才能解释宣告提升这个行为。

那麽进入正题,宣告提升 ( Hoisting ) 到底是什麽样的行为?他们怎麽在 JS 中被解析的?

我认为从 JS 的 执行环境 ( execution context ) 机制说起更好理解:

JS 的运作机制是一行一行往下执行的单执行绪,每当解析器碰到一个函式被呼叫, 就会为该函式开启一个 execution context ,我们常称中文为执行环境。

但要记得,第一个执行环境皆是全域执行环境 ( Global Execution Context )会被创建,且即便你有多个 JS 档,他们共用同个全域执行环境。我个人是会把网站的所有 JS 档案,想成被包进同个函式中,当你开启网站页面时就是呼叫这个函式。
就像不同的火锅料丢进同个火锅里,但这些料都是同个汤底(?
https://ithelp.ithome.com.tw/upload/images/20210914/20141763i4QhJoL0Zd.png

这会影响到,你在不同档案的最外层(也就是全域环境)取了同名称变数,解析器就会报错,因为他们是共用同个全域执行环境。

尔後,其他函式被呼叫时都是一个个堆叠於 Global Execution Context 之上。

好咧!接着要细讲 Execution Context 的机制

Execution Context 执行环境

Execution Context 整个过程分为两个阶段 - 创建阶段、执行阶段,且 Execution Context 还分成全域执行环境以及函式执行环境。

第一阶段:创建阶段 Creation Phase

  • 若是全域执行环境,会建立 global object

  • 皆会创建 this ,而 全域的 this 就是 window 物件,函式的 this 则会依据多种情况做绑定

  • 每个执行环境,都会将宣告的变数名称以及函式放入记忆体的一个专属空间中
    ( ! ) 这边有个大坑,以下面例子说明

    console.log(idol);
    sing('I');
    var idol = 'Taeyeon';
    function sing(song){ console.log(song)};
    console.log(idol);
    

在创建阶段,记忆体仅会放入的是变数 idol 这个名称以及 sing 函式的全部程序码,就是所谓的「宣告提升 Hoisting」。

https://ithelp.ithome.com.tw/upload/images/20210914/201417638dInEzzgUq.png

但 console.log(idol) 为什麽是 undefined?因为 JS 第一阶段就会把所有变数名称先写在记忆体中,此时它会给所有变数赋予 undefined,直到第二阶段开始逐行执行时,有赋值才给值,这也是为什麽第五行 console.log(idol); 就能得到我们想要的值,因为我们在第三行赋值了。

but !! sing('I')为什麽可以成功执行?因为 JS 在创建阶段对於函式的宣告,会把整个函式包含程序码( Function Statement )都放进记忆体。

这是一般变数跟函式在宣告提升上的差别!变数的宣告提升是不包含赋值这件事。

再看一个例子:

```javascript
cheer(idol); // error
var idol = 'Taeyeon';
var cheer = function(idol){ 
  console.log(`cheer for ${idol}`)
};
cheer(idol); // cheer for Taeyeon
```

跟之前不同的是,cheer 我们用运算子的方式去赋值一个匿名函式,而非直接宣告成函式。这会导致放进整个记忆体的依然仅有 cheer 变数名称,不包含匿名函式的程序码,因此第一行呼叫 cheer(idol) 会报错,系统没办法知道 cheer 是一个 function。

第二阶段:执行阶段 Execution Phase

当创建阶段完成後,就会进入到执行阶段,也就是逐行 run 程序码的时候
执行阶段的核心观念在执行堆叠 Execution Stack ( 也称为 Call Stack )
Stack 是一种资料结构,特色为後进先出,也就是最後进来的会优先执行

例子讲解

我们现在有一个 concert 档,从右边可以看到我们演唱会表演嘉宾的值以及他即将做哪些表演。当 concert 执行时,左边最下层会先创建一个 global execution context,里面储存所有变数名称跟宣告的函式,创建阶段结束後开始逐行跑右边程序码。

https://ithelp.ithome.com.tw/upload/images/20210914/20141763zXdMnymVLr.png

执行阶段:

  1. 首先变数 idol 得到值 Taeyeon
  2. 接下来两个都是函式宣告,不用执行
  3. 呼叫 sing() 函式,於是创建一个属於 sing()的「新 execution context」,并堆叠在 global 之上
  4. 开始执行 sing 的程序码,遇到呼叫 dance() 函式,又创建属於 dance()的「新 execution context」,并堆叠在 sing 的 context 之上
  5. 当 dance() 执行完後,dance 的 context 会跳出 ( pop off )整个 execution stack,并回到 sing()
  6. 当 sing() 执行完後同样也会跳出 stack,最终回到 global execution context 继续执行下面的 code ( concert 例子到 sing() 就结束了,但正常来讲一份 js 还会有很多要执行的东西 )

以上统整几个重点

  • 全域执行环境会先被创建

  • 宣告提升发生在执行环境的创建阶段 → 这边要注意全域跟函式的执行环境都有创建阶段,如果你在函式内宣告变数,所有行为都跟文章第一部分讲的一样,会在该函式的作用域内进行 Hoisting喔!

    var idol = 'Taeyeon';
    function sing(singer){
      console.log(idol) // undefined  因为下一行 var idol; 已经被放进记忆体内 但还没执行到赋值的动作
                        // 即便外层全域也有 idol,但被函式 exection context 里的同名称 idol 变数给取代
      var idol = 'Key'
      console.log(idol) // "Fine" 上一行赋值後就能成功取得值
    }
    
    sing(idol);
    
  • JS 的单执行绪机制让程序同个时间只能做一件事,做完才接下一个

  • JS 的执行 stack 采後进先出

  • 每个执行环境 execution context 执行完後会跳离 stack,如下图红箭头,直到回到最底部的全域执行环境

https://ithelp.ithome.com.tw/upload/images/20210914/2014176378qGkaznkI.png

最後稍微提一下「 暂时性死区 ( temporal dead zone,简称TDZ )

为什麽开头会说必须用 var 才能说明 hoisting呢?

这是因为 let 跟 const 的宣告提升会被 TDZ 盖过去,TDZ 是一个时间点的概念,并非空间。

就我的认知上,我不觉得 let、const 不具有 hoisting 的效果,而是他们宣告提升後被自身特性给限制住。倘若已经宣告但尚未赋值,JS 也不会主动把它设为 undefined (更不用说 const 规定宣告时一定要有值 ),所以想取值就会直接报错,错误好像会因浏览器不同而定,我比较常见的是 "ReferenceError: Cannot access 'idol' before initialization

console.log(idol); // 受 TDZ 影响 会报错
let idol = 'Taegu';
let idol2;
console.log(idol2) // undefined

不过这件事有多派理解,大家就记得 let 、 const 在赋值前取用的话会报错,而不是 undefined。

一起养成先宣告好再取用变数的习惯~


参考资料

Huli 大神 https://blog.techbridge.cc/2018/11/10/javascript-hoisting/
https://medium.com/itsems-frontend/javascript-execution-context-and-call-stack-e36e7f77152e
Udemy 课程


<<:  【Side Project】 序

>>:  Day14 javascript 错误

[Day8] 学 Bootstrap 是为了走更长远的路 ~ 下一站 ‧ Reactstrap

前言 今天的文章会再对 Bootstrap 做个 recap, 然後就会稍微对接下来的里程碑 ─ R...

[Day27] 测试场景与角色

今天意外顺利,把预设要做的东西都有做出来 ^_^ 今日目标 搭建测试场景 建立角色在场景上 搭建场景...

DAY25:模型训练DenseNet201

DenseNet201 简介 DenseNet继承了ResNet的短路连线机制,并调整为密集连接机制...

[Day8] 词性标注(三)-Viterbi 演算法

一. Viterbi 演算法 因为若要一条条计算每个path的话会花许多时间,利用Dynamic P...

用电脑判断路段车辆数->控制红绿灯 小论文求解(急

各位大大好 小论文的主旨在於 用电脑判断路段车辆数依照车辆数去调节红绿灯的秒数的实例 老师说有两种但...