本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
引言
在开发过程中,变量的使用是不可或缺的,准确声明变量不仅可以更精确地声明作用域和语义,还可以提升性能。所以你真的用对了嘛 ?
ECMAScript 变量是松散类型的,是指变量可以保存任何类型的数据。有 3 种方式可以声明变量,var、const 和 let。其中 var 可以在 ECMAScript 的所有版本中使用,而 const 和 let 只能在 ESCMAScript 6 以及之后的版本中使用。
var 声明
通过 var 声明的变量有以下特点:
1. 函数作用域
使用 var 声明变量,变量会被自动添加到最接近的上下文中。在全局作用域中声明的变量会变成 window 对像的属性。
var message = 'hello world';
console.log(window.message) // 'hello world'
在函数中,最近的上下文就是函数的局部上下文,在函数退出之后,就访问不到变量了。
function test(){
var message = 'hello world'; // 局部变量
console.log(message);
}
test();
console.log(message) // 报错:message是局部变量,在函数之外访问不到
如果变量未被声明就被初始化了,那么就会被自动添加到全局上下文中(或者对于声明在任何函数外的变量来说是全局)。
function test(){
message = 'hello world'; // 全局变量
}
test();
console.log(message) // 'hello world'
console.log(window.message) // 'hello world'
虽然可以通过省略
var操作符来定义全局变量,但不推荐这么做,在局部作用域中定义的全局变量很难维护,也会造成困惑。因为不能一下子断定省略var是不是有意为之。
2. 变量提升
使用 var 声明的变量会被拿到函数或者全局作用域的顶部,位于作用域中所有代码的前面。这种现象叫做提升(hosting)。提升让同一作用域中的代码不必考虑是否声明就可以直接使用。
var message = 'hello world';
// 等价于
message = 'hello world';
var message;
通过声明之前打印变量,可验证变量提升。声明的提升意味着在声明之前打印变量是 undefined 而不是 Reference Eorror。
console.log(message) // undefined
var message = 'hello world';
由于声明被提升,JavaScript 引擎自动会在作用域顶部将多余的声明合并为一个声明,所以反复使用 var 声明同一个变量也没有问题。
var message = 'hello world';
var message = '你好';
var message = 2021;
console.log(message) // 2021;
let 声明
let 和 var 的作用差不多,但有着非常重要的区别。最明显的一个区别就是,var 声明的范围是函数作用域,let 声明的范围是块作用域。块作用域是 ECMAScript 6 新增的,由 {} 包括,块作用域是函数作用域的子集。
1. 暂时性死区
let 和 var 的另外一个重要的区别是,let 声明的变量也有变量提升,不过不是初始化提升,也就是不能在声明之前使用。在声明之前的执行瞬间被叫做暂时性死区(temporal dead zone) ,在此阶段引用后面才声明的变量都会抛出 ReferenceEorror 。
console.log(message) // 报错:ReferenceEorror,因为没有初始化变量提升
let message = 'hello world';
- 因为
let声明的变量没有提升,JavaScript 引擎也不会进行合并,所以let不允许同一个块作用域中出现冗余声明,JavaScript 引擎会记录声明变量的标识符和作用域。
var message;
var message;
let name;
let name; // 报错:SyntaxError 标识符 name 已经声明过
- 声明冗余报错不会因为混用
let和var而受影响,这两个关键字的声明的是同类型的变量,只是指出作用域所在。
var message;
let message; // 报错:SyntaxError
// 或者
let message;
var message; // 报错:SyntaxError
2. 全局声明
与 var 不同,用 let 在全局作用域中声明的变量不会成为 window 对象的属性,但是作用域还是全局作用域,会存在于页面的整个生命周期内。
let message = 'hello world';
console.log(window.message) // undefined
3.条件声明
因为 let 声明的作用域是块,所以 let 不能依赖条件声明模式。但这是件好事,因为条件声明是一种反模式,它会让程序变得更难理解,如果你正在用这种模式,那一定会有更好的替代方式。
4.for 循环中的 let 声明
在 ECMAScript 6 之前,for 循环中定义的迭代变量用 var 定义,因为是函数作用域,所以会出现一些问题。
- 迭代变量会渗透到循环的外部。
for(var i = 0; i < 10; ++i){
// 循环逻辑
}
console.log(i) // 10
- 在上下文中,只有一个变量
i,每次循环i发生变化,示例如下:
for(var i = 0; i < 5; ++i){
setTimeout(() => console.log(i),0);
}
// 你可能会以为输出 0、1、2、3、4
// 实际输出 5、5、5、5、5
出现 let 之后这些问题就被解决了,因为迭代变量的作用域仅限于循环块中,所以 JavaScript 引擎会为每次循环都声明一个新的迭代变量,每个 setTimeout 引用的都是不同的变量,输出就是我们期望的每次循环的结果。
for(let i = 0; i < 5; ++i){
setTimeout(() => console.log(i),0);
}
// 实际输出 0、1、2、3、4
这种每次迭代声明一个独立变量实例的行为适用于所有风格的 for 循环,包括 for-in 和 for-of 循环。
const 声明
const 的行为和 let 差不多,声明的作用域也是块作用域,唯一一个重要的区别是用它声明的变量必须同时初始化,而且一经声明,在其声明周期的任何时候都不能被重新赋值。
const message // SyntaxError: 常量声明时没有初始化
const name = 'Starry';
console.log(name) // Starry
name = 'Alex' // TypeError: 给常量赋值
const声明的限制只适用于它指向的变量的引用,比如说,如果它的引用是个对象,那么修改对象内部的属性并不违反const的限制。
const person = { name:'Starry' }
person.name = 'Alex' // ok
如果想让整个对象都不能修改,可以使用 Object.freeze() ,这样再给属性赋值虽然不会报错,但会静默失败。
const person = Object.freeze({ name:'Starry' });
person.name = 'Alex';
console.log(person.name) // Starry
由于 const 声明的变量是单一变量切不可修改,JavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查询。 谷歌的 V8 引擎就执行这种优化。
最佳实践
ECMAScript 6 新增的 const 和 let 从客观上为这门语言更精确地声明作用域和语义提供更好的了支持,新的有助于提升代码质量的最佳实践也逐渐显现。
- 不使用 var
限制自己只使用 const 和 let 有助于提升代码质量,因为变量有了更明确的作用域、声明位置,以及不变的值。
- const 优先,let 次之
使用 const 可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的操作。当提前知道变量未来会有修改时,才使用 let。
- 通过 const 和 let 声明提升性能
因为 const 和 let 的作用域都是块作用域,所以相比于使用 var 能更早的让垃圾回收程序介入,更早的回收应该回收的。在块作用域比函数作用域更早终止的情况下,这种情况就有可能发生。
参考资料
《JavaScript 高级程序设计》