ECMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。有 3 个关键字可以声明变量:var、const 和 let。其中,var 在 ECMAScript 的所有版本中都可以使用,而 const 和 let 只能在 ECMAScript 6 及更晚的版本中使用。

var

1
2
3
4
5
6
7
// 不初始化的情况下,变量会保存一个特殊值 undefined,
var message;
console.log(message);
message = "h1";
console.log(message);
// 执行到当前位置时,变量保存值的类型会发生改变
message = 100; // 合法,但不推荐

var 的作用域

使用 var 在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁

1
2
3
4
5
function test() {
var message = "hi"; // 局部变量
}
test();
console.log(message); // 报错:Uncaught ReferenceError: message is not defined
1
2
3
4
5
6
7
8
/*
虽然可以通过省略 var 操作符定义全局变量,但不推荐这么做。在局部作用域中定义的全局变量很难维护,也会造成困惑。这是因为不能一下子断定省略 var 是不是有意而为之。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出 ReferenceError。
*/
function test() {
message = "hi"; // 省略var会声明一个全局变量
}
test();
console.log(message);

如果需要定义多个变量,可以在一条语句中用逗号分隔每个变量(及可选的初始化):

1
2
3
4
5
6
/*
这里定义并初始化了 3 个变量。因为 ECMAScript 是松散类型的,所以使用不同数据类型初始化的变量可以用一条语句来声明。插入换行和空格缩进并不是必需的,但这样有利于阅读理解。在严格模式下,不能定义名为 eval 和 arguments 的变量,否则会导致语法错误。
*/
var message = "hi",
found = false,
age = 29;

声明提升

使用 var 时,下面的代码不会报错。这是因为使用这个关键字声明的变量会自动提升到函数作用域顶部:

1
2
3
4
5
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined

之所以不会报错,是因为 ECMAScript 运行时把它看成等价于如下代码:

1
2
3
4
5
6
function foo() {
var age;
console.log(age);
age = 26;
}
foo();

这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。此外,反复多次使用 var 声明同一个变量也没有问题:

1
2
3
4
5
6
7
function foo() {
var age = 16;
var age = 26;
var age = 36;
console.log(age);
}
foo(); // 36

let

let 跟 var 的作用差不多,但有着非常重要的区别。最明显的区别是,let 声明的范围是块作用域,而 var 声明的范围是函数作用域。

1
2
3
4
5
if (true) {
var name = "Matt";
console.log(name); // Matt
}
console.log(name); // Matt
1
2
3
4
5
if (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // Uncaught ReferenceError: age is not defined

在这里,age 变量之所以不能在 if 块外部被引用,是因为它的作用域仅限于该块内部。块作用域是函数作用域的子集,因此适用于 var 的作用域限制同样也适用于 let。

let 也不允许同一个块作用域中出现冗余声明。这样会导致报错:

1
2
3
4
var name;
var name;
let age;
let age; // Uncaught SyntaxError: Identifier 'age' has already been declared

当然,JavaScript 引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标识符不会报错,而这是因为同一个块中没有重复声明:

1
2
3
4
5
6
let age = 30;
console.log(age); // 30
if (true) {
let age = 26;
console.log(age); // 26
}

暂时性死区

1
2
3
4
5
6
/*
在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方式来引用未声明的变量。在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone)
*/
// age 不会被提升
console.log(age); // Uncaught ReferenceError: Cannot access 'age' before initialization
let age = 26;

全局声明

与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声明的变量则会)。

1
2
3
4
var name = "Matt";
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined

条件声明

在使用 var 声明变量时,由于声明会被提升,JavaScript 引擎会自动将多余的声明在作用域顶部合并为一个声明。因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同时也就不可能在没有声明的情况下声明它。

在页面中定义<script>脚本再次进行声明

1
2
3
4
5
6
// 假设脚本不确定页面中是否已经声明了同名变量
// 那它可以假设还没有声明过
var name = "Nicholas";
// 这里没问题,因为可以被作为一个提升声明来处理
// 不需要检查之前是否声明过同名变量
let age = 26; // Uncaught SyntaxError: Identifier 'age' has already been declared
1
2
3
4
5
6
/*
使用 try/catch 语句或 typeof 操作符也不能解决,因为条件块中 let 声明的作用域仅限于该块。
*/
if (typeof name === "undefined") {
let name;
}

for 循环中的 let 声明

在 let 出现之前,for 循环定义的迭代变量会渗透到循环体外部:

1
2
3
4
for (var i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // 5

改成使用 let 之后,这个问题就消失了,因为迭代变量的作用域仅限于 for 循环块内部:

1
2
3
4
for (let i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // Uncaught ReferenceError: i is not defined

在使用 var 的时候,最常见的问题就是对迭代变量的奇特声明和修改:

1
2
3
4
5
for (var i = 0; i < 5; ++i) {
// 页面中所有由setTimeout定义的操作,都将放在同一个队列中依次执行。而这个队列的执行时间需要等到函数调用栈执行完毕后才会执行,也就是在循环结束之后,才会轮到setTimeout执行其内部操作,同时,var是函数级作用域,因此在循环结束时i的最终值会更新为5。
setTimeout(() => console.log(i), 0);
}
// 输出 5、5、5、5、5
1
2
3
4
5
6
// 在使用 let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。每个 setTimeout 引用的都是不同的变量实例,所以 console.log 输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。
for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0);
}

// 输出0、1、2、3、4

const

const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误。

1
2
const age = 26;
age = 36; // Attempt to assign to const or readonly variable
1
2
3
// const 也不允许重复声明
const name = "Matt";
const name = "Nicholas"; // Duplicate declaration
1
2
3
4
5
6
// const 声明的作用域也是块
const name = "Matt";
if (true) {
const name = "Nicholas";
}
console.log(name); // Matt

const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象,那么修改这个对象内部的属性并不违反 const 的限制

对于声明方式的建议

  1. 不使用 var
    有了 let 和 const,大多数开发者会发现自己不再需要 var 了。限制自己只使用 let 和 const 有助于提升代码质量,因为变量有了明确的作用域、声明位置,以及不变的值。

  2. const 优先,let 次之
    使用 const 声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。因此,很多开发者认为应该优先使用 const 来声明变量,只在提前知道未来会有修改时,再使用 let。这样可以让开发者更有信心地推断某些变量的值永远不会变,同时也能迅速发现因意外赋值导致的非预期行为。