早期的 JavaScript 变数只能用 var 宣告,後来 ES6 新增 let 、 const 。
这篇不会细讲三个宣告方式的差异,网路上很多大神已经解释得很好。另外,其实我现在已经没在用 var ,但用 var 才能解释宣告提升这个行为。
那麽进入正题,宣告提升 ( Hoisting ) 到底是什麽样的行为?他们怎麽在 JS 中被解析的?
我认为从 JS 的 执行环境 ( execution context ) 机制说起更好理解:
JS 的运作机制是一行一行往下执行的单执行绪,每当解析器碰到一个函式被呼叫, 就会为该函式开启一个 execution context ,我们常称中文为执行环境。
但要记得,第一个执行环境皆是全域执行环境 ( Global Execution Context )会被创建,且即便你有多个 JS 档,他们共用同个全域执行环境。我个人是会把网站的所有 JS 档案,想成被包进同个函式中,当你开启网站页面时就是呼叫这个函式。
就像不同的火锅料丢进同个火锅里,但这些料都是同个汤底(?
这会影响到,你在不同档案的最外层(也就是全域环境)取了同名称变数,解析器就会报错,因为他们是共用同个全域执行环境。
尔後,其他函式被呼叫时都是一个个堆叠於 Global 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」。
但 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,里面储存所有变数名称跟宣告的函式,创建阶段结束後开始逐行跑右边程序码。
执行阶段:
以上统整几个重点
全域执行环境会先被创建
宣告提升发生在执行环境的创建阶段 → 这边要注意全域跟函式的执行环境都有创建阶段,如果你在函式内宣告变数,所有行为都跟文章第一部分讲的一样,会在该函式的作用域内进行 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,如下图红箭头,直到回到最底部的全域执行环境
最後稍微提一下「 暂时性死区 ( 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 课程
前言 今天的文章会再对 Bootstrap 做个 recap, 然後就会稍微对接下来的里程碑 ─ R...
今天意外顺利,把预设要做的东西都有做出来 ^_^ 今日目标 搭建测试场景 建立角色在场景上 搭建场景...
DenseNet201 简介 DenseNet继承了ResNet的短路连线机制,并调整为密集连接机制...
一. Viterbi 演算法 因为若要一条条计算每个path的话会花许多时间,利用Dynamic P...
各位大大好 小论文的主旨在於 用电脑判断路段车辆数依照车辆数去调节红绿灯的秒数的实例 老师说有两种但...