解释 JavaScript 中的“hoisting”(变量提升)概念
TL;DR
Hoisting(变量提升)是一种 JavaScript 机制,在编译阶段,变量和函数声明会被“提升”到其包含作用域的顶部。
- 变量声明 (
var
):声明被提升,但未初始化。如果在使用前访问变量,则变量的值为undefined
。 - 变量声明 (
let
和const
):声明被提升,但未初始化。访问它们会导致ReferenceError
,直到遇到实际的声明。 - 函数表达式 (
var
):声明被提升,但未初始化。如果在使用前访问变量,则变量的值为undefined
。 - 函数声明 (
function
):声明和定义都被完全提升。 - 类声明 (
class
):声明被提升,但未初始化。访问它们会导致ReferenceError
,直到遇到实际的声明。 - 导入声明 (
import
):声明被提升,并且在执行其余代码之前,会执行导入模块的副作用。
以下行为总结了在声明变量之前访问变量的结果。
声明 | 在声明之前访问 |
---|---|
var foo | undefined |
let foo | ReferenceError |
const foo | ReferenceError |
class Foo | ReferenceError |
var foo = function() { ... } | undefined |
function foo() { ... } | 正常 |
import | 正常 |
Hoisting(变量提升)
Hoisting(变量提升)是一个术语,用于解释 JavaScript 代码中变量声明的行为。
使用 var
关键字声明或初始化的变量将在编译期间将其声明“移动”到其包含作用域的顶部,我们将其称为变量提升。
只有声明被提升,初始化/赋值(如果存在)将保留在原来的位置。请注意,声明实际上并没有移动——JavaScript 引擎在编译期间会解析声明,并了解变量及其作用域,但是通过将声明可视化为“提升”到其作用域的顶部,更容易理解这种行为。
让我们用几个代码示例来解释。请注意,这些示例的代码应该在模块作用域内执行,而不是像浏览器控制台那样逐行输入到 REPL 中。
使用 var
声明的变量的变量提升
在这里可以看到变量提升的实际效果,即使在第一个 console.log()
之后才声明和初始化 foo
,第一个 console.log()
也会将 foo
的值打印为 undefined
。
console.log(foo); // undefinedvar foo = 1;console.log(foo); // 1
您可以将代码可视化为:
var foo;console.log(foo); // undefinedfoo = 1;console.log(foo); // 1
使用 let
、const
和 class
声明的变量的变量提升
通过 let
、const
和 class
声明的变量也会被提升。但是,与 var
和 function
不同,它们没有被初始化,在声明之前访问它们将导致 ReferenceError
异常。该变量处于一个“暂时性死区”,从块的开始到声明被处理。
y; // ReferenceError: Cannot access 'y' before initializationlet y = 'local';
z; // ReferenceError: Cannot access 'z' before initializationconst z = 'local';
Foo; // ReferenceError: Cannot access 'Foo' before initializationclass Foo {constructor() {}}
函数表达式的提升
函数表达式是以变量声明的形式编写的函数。由于它们也是使用 var
声明的,因此只有变量声明被提升。
console.log(bar); // undefinedbar(); // Uncaught TypeError: bar is not a functionvar bar = function () {console.log('BARRRR');};
函数声明的提升
函数声明使用 function
关键字。与函数表达式不同,函数声明同时提升声明和定义,因此即使在声明之前也可以调用它们。
console.log(foo); // [Function: foo]foo(); // 'FOOOOO'function foo() {console.log('FOOOOO');}
这同样适用于生成器 (function*
)、异步函数 (async function
) 和异步函数生成器 (async function*
)。
import
语句的提升
Import 声明被提升。import 引入的标识符在整个模块范围内都可用,并且它们产生的副作用在模块的其余代码运行之前产生。
foo.doSomething(); // Works normally.import foo from './modules/foo';
幕后
实际上,JavaScript 在尝试执行代码之前,会在当前作用域中创建所有变量。使用 var
关键字创建的变量的值将为 undefined
,而使用 let
和 const
关键字创建的变量将被标记为 <value unavailable>
。因此,在初始化之前访问它们将导致 ReferenceError
,从而阻止您在初始化之前访问它们。
在 ECMAScript 规范中,let
和 const
声明解释如下:
当它们包含的 Environment Record 被实例化时,变量被创建,但在变量的 LexicalBinding 被求值之前,可能无法以任何方式访问它们。
但是,对于 var
关键字,此语句略有不同:
当它们包含的 Environment Record 被实例化时,Var 变量被创建,并在创建时被初始化为
undefined
。
现代实践
在实践中,现代代码库避免使用 var
,并且仅使用 let
和 const
。建议在包含范围/模块的顶部声明和初始化变量和 import 语句,以消除跟踪何时可以使用变量的心理负担。
ESLint 是一个静态代码分析器,可以发现违反此类情况,使用以下规则:
no-use-before-define
: 当遇到对尚未声明的标识符的引用时,此规则将发出警告。no-undef
: 当遇到对尚未声明的标识符的引用时,此规则将发出警告。