本文深入解析 JavaScript 中 var 声明的变量提升机制,区分可访问性与可用性,并对比 ES6 引入的 let 和 const 如何消除变量提升及引入暂时性死区,帮助开发者规避常见陷阱并养成规范编码习惯。
JavaScript 变量提升机制概述
JavaScript 中存在变量提升(Hoisting)机制,这通常被视为语言设计上的遗留问题或实现副作用。该机制允许变量在未声明的情况下被访问,或者在声明之前就被使用。这一特性常令新手困惑,甚至许多资深开发者也易产生误解。然而,随着 ES6 标准引入 let 和 const 关键字,变量提升现象在块级作用域中已不再存在。
var 声明的变量提升行为
一、未声明变量直接使用
function test() {
alert(notDefined);
}
test(); // ?
此时程序会直接报错,这是符合预期的行为。

二、变量声明置于代码末尾
function test() {
alert(declaredButNotAssigned); // undefined
var declaredButNotAssigned;
}
test();
运行结果输出 undefined。虽然代码不再报错并能继续执行,但变量的值往往并非程序员所预期的结果。
三、变量声明置于末尾并同时赋值
function test() {
alert(declaredAndAssigned); // undefined
var declaredAndAssigned = 1;
}
test();
结果与二一致,显然赋值操作不会导致输出 1。
上述第二、三种情况均涉及变量提升(Hoisting),现对其进行简要定义。
变量提升是指:在指定作用域内,尽管从代码书写顺序上看是“先使用后声明”,但在运行时,变量的“可访问性”会被提升至当前作用域的顶部,其初始值为 undefined,此时变量并不具备“可用性”。
此处强调“代码顺序”与“运行顺序”的区别,因为大多数代码是顺序执行的,两者通常一致,这也符合人类的思维逻辑。例如,拥有 C 语言经验的程序员
#include <stdio.h>
int main() {
int x = 1;
printf("%d, ", x); // 1
}
编写如下代码:先声明整型变量 x 并赋值为 1,随后输出。此时代码顺序与运行顺序一致,程序正常运行。
如果将顺序颠倒,
#include <stdio.h>
int main() {
printf("%d, ", x); // error
int x = 1;
}
在 C 语言中,编译器将无法通过检查并报错。但在 JavaScript 中,如前文第二、三例所示,这种颠倒是被允许的。
因此,具备 C 语言经验的程序员深知变量必须“先声明后使用”,否则会导致错误。而在 JavaScript 中,由于存在变量提升现象,允许“先使用后声明”,这导致 C 语言的经验迁移到 JS 开发时会产生困惑。
函数表达式与函数声明的提升差异
四、函数表达式同样存在变量提升
function test() {
alert(func); // undefined
var func = function() {};
}
test();
然而,如果试图调用该函数,则无法实现。
function test() {
alert(func); // undefined
func(); // 报异常
var func = function() {};
}
test();
结果显示 func 为 undefined,调用 func 会抛出异常。这对应前文提到的“可访问性”与“可用性”概念,具体表现如下。
可访问性:执行 alert(func) 会输出 undefined,代码可以运行,说明 func 是可访问的。
可用性:执行 func() 会抛出异常,无法正常调用,说明 func 不具备可用性。
第二、三、四种情况均使用 var 声明变量。在 JavaScript 中,函数声明也会发生提升,但这里的“变量”较为特殊,其类型为 function(可作为函数、方法或构造器)。其标识符名称同样会被提升至当前作用域的顶部。
五、函数声明的名称也会提升至当前作用域顶部
function test() {
alert(f1); // function
f1(); // "called"
function f1() {
alert('called');
}
}
test();
可以看到,尽管函数 f1 的声明位于代码末尾,但在使用前调用 alert(f1) 和 f1() 均能正常执行,这表明 f1 同时具备了可访问性和可用性。
变量提升的面试题解析
前文已述,变量提升(Hoisting)并无实际益处,属于语言设计中的缺陷,良好的编码习惯应是“先声明后使用”。这一特性也常出现在各大公司的技术面试题中。
面试题 1:
// 写出以下代码的运行结果
var a = 1;
function fn() {
if (!a) {
var a = 2;
}
alert(a); // ?
}
fn();
面试题 2:
// 写出以下代码的运行结果
var a = 1;
function fn() {
a = 2;
return;
function a() {}
}
fn();
alert(a); // ?
ES6 let/const 对变量提升的终结
然而,随着 ES6 标准中 let 和 const 的引入,这一切已发生改变。在 ES6 及后续版本中,除全局变量外,其他变量均应使用 let 或 const 声明。将 var 替换为 let 后,变量提升现象便不复存在。
function test() {
alert(declaredButNotAssigned1); // 报异常
alert(declaredButNotAssigned2); // 报异常
alert(func); // 报异常
let declaredButNotAssigned1;
let declaredButNotAssigned2 = true;
let func = function() {};
}
test();
这强制程序员养成良好习惯,变量必须“先声明再使用”,否则将直接报错。
以下摘录自 MDN 关于 let 不再发生变量提升的描述:
代码如下:
In ECMAScript 6, let does not hoist the variable to the top of the block. If you reference a variable in a block before the let declaration for that variable is encountered, this results in a ReferenceError, because the variable is in a "temporal dead zone" from the start of the block until the declaration is processed.
使用 let 声明变量后,typeof 操作符也不再安全。
if (condition) {
alert(typeof num); // Error!
let num = 100;
}
过去可以使用 typeof == 'undefined' 来判断是否引入了某个库,例如 jQuery。
// 判断jQuery是否引入了
if (typeof $ !== 'undefined') {
// do something
}...
如果未引入 jQuery,变量 $ 未声明,该判断语句不会报错且不影响后续代码执行;但如果使用 let 声明变量,则会抛出错误。
以上即为本文的全部内容,希望对大家有所帮助。