什么是提升 (Hoisting)?
2023年2月7日
在《在JavaScript 中用`var`, `let`, 以及`const` 有什么差别?什么时候该用哪个?》这篇文章中,曾经提到var
、let
与const
有「提升(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) 并没有被定义在标准规范中的名词解释,但概念上,let
、const
跟 var
同样会有提升 (hoisting) 的行为,不过其中有以下差异:
var
会提升到函式作用域 (function scope),但let
和const
只会提升到区块作用域 (block scope)var
在创建变数与定义变数范围时,会同时将变数值自动初始化为undefined
; 但当let
在提升变数到区块作用域(block scope) 范围时,并不会初始化此变数,这个状态可以称之为uninitialized,也有另一个常见的说法是,let
和const
定义的变数目前存在于暂时死区(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
后再拿到不同值的设计会不符合规范。因此,设计了暂时死区的错误,避免这种情况发生。