什么是提升 (Hoisting)?

2023年2月7日

💎 加入 E+ 成長計畫 與超過 300+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

《在JavaScript 中用`var`, `let`, 以及`const` 有什么差别?什么时候该用哪个?》这篇文章中,曾经提到varletconst 有「提升(hoisting) 的差别」,而这篇文章会更详细的回答提升(hoisting) 究竟是什么。

什么是提升 (hoisting) ?

大部分人应该都曾经在写 JavaScript 代码时,在宣告函式之前就使用它,如下方代码:

sayHello(); // Hello

function sayHello() {
  console.log("Hello");
}

但执行这样的代码并不会报错,其原因就是因为提升 (Hoisting)。

提升(Hoisting) 并非ECMAScript® 2015 Language Specification 中的一个正式定义,但它用来形容 JavaScript 编译阶段将变数和函式的宣告存入记忆体的概念。

这个特性会使函式和变量的宣告被提升到作用域的顶部,即使他们的实际定义位置在下面。但要注意,JavaScript 引擎并不会将代码实际移到顶部,而只是这个概念的形容。

变数与函式的提升

var 提升 (hoisting)

var 的提升 (hoisting) 是指在编译阶段,JavaScript 引擎会将所有的 var 变数宣告提升到该函式作用域的顶端。虽然变数宣告被提升了,但并不会赋值,如下方代码,提早呼叫 name 的结果会是 undefined 而非 Tom

console.log(name); // undefined
var name = "Tom";

let 提升 (hoisting)

许多人会误以为 let 不会提升 (hoisting),因为我们如果是在宣告 let 前就使用,会出现以下错误。

console.log(greeting); // Uncaught ReferenceError: greeting is not defined
let greeting = "hi there";

上方的代码之所以会抛出错误,并不是因为 let 没有 hoisting。虽然提升 (hoisting) 并没有被定义在标准规范中的名词解释,但概念上,letconstvar 同样会有提升 (hoisting) 的行为,不过其中有以下差异:

  1. var 会提升到函式作用域 (function scope),但 letconst 只会提升到区块作用域 (block scope)
  2. var 在创建变数与定义变数范围时,会同时将变数值自动初始化为undefined; 但当let 在提升变数到区块作用域(block scope) 范围时,并不会初始化此变数,这个状态可以称之为uninitialized,也有另一个常见的说法是,letconst 定义的变数目前存在于暂时死区(TDZ,Temporal dead zone)

函式提升

函式宣告也有提升,与 var 提升的差异为,函式提升也会创建好函式物件,因此可以在宣告前呼叫。

foo(); // 1
function foo() {
  console.log(1);
}

但函式提升要注意的是,如果是函式表达式,提升行为会与其宣告的变数一样,如下方代码,用 var 宣告的foo 函式,在宣告前使用时,当时值会是 undefined,因此呼叫undefined 会报错。

foo(); // Uncaught TypeError: foo is not a function
var foo = function () {};

用 let 宣告的 foo 函式,在宣告前使用时,此时 foo 在暂时死区,因此呼叫 foo 会报错。

foo(); // Uncaught ReferenceError: foo is not defined
let foo = function () {};

为什么有暂时死区 (TDZ, Temporal dead zone)错误?

许多文章中提到,暂时死区(TDZ, Temporal dead zone) 出现好处是可以避免我们在变数在还没有被宣告前就能拿来使用,但其实,还有另外一个最主要的设计概念。

You Don't Know JS 的作者在曾经在这门课中提出给出很好的解释:暂时死区(TDZ, Temporal dead zone) 错误其实是为了const 所设计的。试想一下,如果const 的提升行为与var 相同,因此我们在宣告前访问到const 变量时,会拿到undefined 的值,但我们也知道const 是*常数 *,同个作用域中值不应该变动,因此如果先拿到undefined 后再拿到不同值的设计会不符合规范。因此,设计了暂时死区的错误,避免这种情况发生。

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們