JavaScript ES6“变量”详细解说:var,let,const到底有啥区别?该用哪个?【重嗑JS系列-Basic】
2021.07.075 min read
原创声明:未经允许,禁止转载
这次我们来讲讲var,let,const的区别,虽然基础,但是真正搞透彻的人很少,看看有没有你不知道的
可变意味着任何可以改变的东西。 JavaScript变量用来保存数据值,它可以随时更改。
ECMAScript 变量是松散类型的,这意味着变量可以保存任何类型的数据。每个变量只是一个值的命名占位符。可以使用三个关键字来声明变量:
- var,在所有 ECMAScript 版本中都可用
- const 和 let,在 ECMAScript 6 中引入
var 关键字声明
用 var定义的变量名可以储存任何类型的值。如果不初始化的话(即是仅仅声明 var x;
),则会被赋予一个特殊值:undefined
即是被初始化为字符串,也并不意味着该变量就是字符串类型,而是仅仅意味着它被赋予了一个值而已。它也可以被重新赋予其他类型的值,比如下面:
var message = "你好";
message = 100 ; // 合法,但不推荐;
var 变量的作用域
var 声明的变量默认是局部作用域,在调用方法以外无法被识别:
function test() {
var message = "hi"; // 局部作用域
}
test();
console.log(message); // error!
可以通过去掉 var 操作符的方式来声明全局变量
function test() {
message = "hi"; // 全局变量,不推荐。严格模式(strict mode)下会__报错__
}
test();
console.log(message); // "hi"
但是因为不容易维护和容易造成代码逻辑混乱等原因,这种写法并不推荐。因为往往一眼看上去并不是立刻能识别没有写var运算符是因为忘掉的还是故意的。
严格模式(strict mode)下会报错。
如果你想一次性声明一个以上的变量,那么你可用下面的简单写法,用逗号分隔变量名:
var message = "hi",
found = false,
age = 29;
这样,三个不同的变量就一次性被定义了。因为ECMAScript是松散类型的,所以用不同数据类型初始化的变量可以被整合到一个声明语句中去。
即使像上面代码中的换行和字符锁进并不是必须的,但这都增加了代码的可读性。
严格模式(strict mode
)下,你不能用 eval
或 arguments
来定义变量名,这会造成语法错误。
var声明提升
使用var声明变量的时候可能会发生下面的情况,
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined
并没有报错,因为使用var 关键字声明的变量被提升到函数作用域的顶部,其实背后ECMAScript 运行时在技术上是这样处理的:
function foo() {
var age; // 声明提升
console.log(age);
age = 26;
}
foo(); // undefined
并且,即使重复声明同一个变量名的变量也不会报错(会被当作一个变量重复处理)。
var 就讲到这里,下面我们来康康大家熟知的ES6中新增加的let
let 关键字声明
let 和 var 用法基本一致, 但是有下面这些很重要的不同:
1. let
的作用于是 块,而 var
的作用域是 函数。
if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name);// Matt
----------------------------------
if (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age is not defined
块作用域严格来说是方法(函数)作用域的一个子集,因此任何适用于 var 声明的作用域限制也将适用于 let 声明。
可见let的作用域更小,严格到块。let 声明也不像var那样在同一个块中允许重复(冗余)声明。如果这么做的话会报错:
var name;
var name; // 不报错
let age;
let age; // SyntaxError; identifier 'age' has already been declared (“age”标识符已被声明)
嵌套声明则不会出问题,因为作用域不同:
var name = 'Nicholas';
console.log(name); // 'Nicholas'
if (true) {
var name = 'Matt';
console.log(name); // 'Matt'
}
let age = 30;
console.log(age); // 30
if (true) {
let age = 26;
console.log(age); // 26
}
声明冗余报错和顺序无关,即使 let 与 var 混合,也不会受到影响。不同的关键字并不是声明不同类型的变量——它们只是指定变量在相关范围内的存在方式不同。
var name;
let name; // SyntaxError
let age;
var age; // SyntaxError
2. 另一个let
和 var
的不同特征是,用 let
定义的变量不能被声明提升:
// name 被声明提升
console.log(name); // undefined
var name = 'Matt';
// age is not hoisted
console.log(age); // ReferenceError: age is not defined
let age = 26;
3. 全局声明
和var
不同, 用let
在全局语境下定义的变量,变量不会像var
那样被附属到 window
对象上。
var name = 'Matt';
console.log(window.name); // 'Matt'
-----------------
let age = 26;
console.log(window.age); // undefined
但是 let的声明依然会在页面的生命周期中持续出现在全局范围,因而要注意不能重复(冗余)定义。
4. 有条件声明(Conditional Declaration)
不像 var
关键字,因为声明会被提升,所以JavaScript引擎会开心地把多余的声明整合到作用域顶部的一个单一的声明中去,let
声明的作用域是 块,所以不可能去检查一个用let
声明的变量是否之前在别的地方被声明过了,然后看看如果没被声明就去有选择地声明之。
<script>
var name = 'Nicholas';
let age = 26;
</script>
---------------------
<script>
// 假设此脚本不确定页面中是否有什么变量是已经被声明过的。
// 它会假设任何变量都尚未被声明.
var name = 'Matt'; // 这里没毛病,因为这会被作为一个提升的声明来处理。不用管它是否已经被定义过。
let age = 36; // 如果‘age’被在别的地方定义过了,这里就会报错。
</script>
即使用 try/catch
或者typeof
操作符也不行,因为let
声明 将会局限于该条件块中。
<script>
let name = 'Nicholas';
let age = 36;
</script>
<script>
// 假设此脚本不确定页面中是否有什么变量是已经被声明过的。
// 它会假设任何变量都尚未被声明.
if (typeof name !== 'undefined') {
let name; // 'name' 被局限于if {} 块作用域中
}
name = 'Matt'; //这个 name会被作为全局变量声明
try (age) {
// 如果‘age’没有被定义,这里就会报错
} catch(error) {
let age; // 'age' 被局限于catch {} 块作用域中
}
age = 26; //这个 age会被作为全局变量声明
</script>
因为以上的原因,在使用let
这个在ES6里面新定义的关键词的时候,你不能再像用var
的时候一样依靠有条件的声明模式,
这其实反而是一件好事情,因为有条件地声明其实是一个坏的编码范式。它让程序流程看起来复杂难懂。如果某天你发现你陷入了这种范式,我敢说一定会有更好的解决办法。
5. 在循环体中的let
声明
在let出现以前,for 循环语句中的迭代器变量的作用域会溢出到循环体之外。
for ( var i = 0; i < 5 ; ++i ) {
// do loop things
}
console.log(i); // 5
改为用 let
以后,就再也没这个问题了,因为迭代器变量的作用域仅仅被局限在循环体中。
for (let i = 0; i < 5; ++1){
// do loop things
}
console.log(i); // ReferenceError: i is not defined
在使用 var
的时候,一个经常出现的问题是“迭代器变量的单一声明和修改”的问题:
for (var i = 0; i < 5; ++i){
setTimeout(()=> console.log(i), 0)
}
// 你可能希望的结果是 console.log 0, 1, 2, 3, 4
// 但实际上是 console.log 5, 5, 5, 5, 5
出现上面代码里情况的原因是,在循环结束的时候迭代器变量 i 的值始终停留在导致循环结束的那个值上:5,因此,当之后的timeouts
被执行的时候,i 始终被指向的是同一个值,因此不断输出的都是最终值5。
然而当用let
声明循环迭代变量的时候,背后发生的事情是:JavaScript引擎会在每次循环迭代的时候都声明一个新的迭代器变量。所以每次setTimeout
调用的都是单独的实例,因此console.log可以得到预期的值:每次循环迭代被执行的时候的迭代器变量的值。
for (let i = 0; i < 5; ++i){
setTimeout(() => console.log(i), 0)
}
// console.log 0, 1, 2, 3, 4
这种单次迭代声明行为适用于所有类型的循环体,包括 for-in
和 for-of
循环
最后我们来康康const关键字
const 关键字声明
const
的行为和 let
一致,但是有一个重要的区别——它必须在初始化的时候被赋值,并且赋值以后不能改变。尝试改变一个const变量会导致一个运行时错误。
const age = 26;
age = 36; // TypeError: assignment to a constant
// const 也不允许冗余定义
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError
// const 的作用域也是块
const name = 'Matt';
if(true){
const name = 'Nicholas'; //不会报错,和块作用域外面的'name'没有冲突
}
console.log(name); // Matt
const 声明仅仅对它所指向的变量的引用具有强制性,如果一个const变量指向的是一个对象,那么修改对象里面的的属性并不违反const的限制规定。比如:
const person = {};
person.name = 'Matt'; // OK
虽说JavaScript引擎会对每一次for循环的let
迭代器变量创建一个实例,并且即使const
变量的动作和let
很一致,你也不能用const
来声明循环迭代变量。
for (const i = 0; i < 10; ++i){} // TypeError: assignment to const variable
然而,如果你想创建一个不希望被更改的循环变量,那当然是可以用const的——正是因为每次迭代都会声明一个新的变量。这与 for-of
和 for-in
循环尤其相关:
let i = 0;
for (const j = 7; i < 5; ++i){
console.log(j);
}
// 7, 7, 7, 7, 7
for (const key in {a: 1, b: 2}){
console.log(key);
}
// a, b
for (const value of [1,2,3,4,5]){
console.log(value);
}
// 1, 2, 3, 4, 5
那么现在问题来了: var
, let
, const
这些关键字到底怎么用呢? 其实大家在工作中可能早已知道了,但是知道了以上这么多关于 var, let, const的本质的不同以后,再来看看使用这些关键字的使用最佳实践可能就会觉得很不一样😉。
声明风格和最佳实践
let
和 const
在ES6中的引入客观上使得JavaScript成为了一种更好的语言工具,因为这使得声明的范围和语义上的精确度都得到了提升。var 变量声明所带来的各种迷惑代码行为多年来给JS社区所造成了无比的困扰,这早就不是啥秘密了。而因为这些新关键词的引入,一些提升代码质量的常见模式也不断涌现。
1. 不要用var
来进行变量声明
有了let
和const
以后,很多开发者会发现他们的代码库再也不需要var
了。只使用let
和const
进行变量声明的严格限制模式可以提升代码库的质量,因为这使得变量作用域、声明位置和const正确性都可以得到仔细的管理。
2. 比起用let
,优先使用const
使用const
声明允许浏览器运行时强制执行常量变量,并且使得静态代码分析工具得以预见非法的重新分配操作。因此,很多开发人员觉得,除非他们将来什么时候会想要重新赋值,否则默认用const
来声明变量对他们是有好处的。这也促使开发人员具体分析哪些值他们知道是永远不会改变的,并且也能够快速监测到代码尝试进行意外赋值操作的行为。
参考/翻译自:Matt Frisbie - Professional JavaScript for Web Developers-Wrox Press