本文详细介绍了JavaScript中的变量声明方式、作用域规则和生命周期管理。通过实例讲解var、let和const的使用场景和注意事项,帮助开发者编写更安全、可维护的代码。
JavaScript变量
ECMAScript变量是松散的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命令占位符。目前有三个关键字可以声明变量:var/const/let
,var
在任何ESMCScript版本都可以使用。const/let
只能在ECMAScript6及更晚的版本中使用。
1.var声明
要定义变量,可以使用var
操作符,后面跟变量名
var message;
这行代码定义了一个名为message
的变量,可以用它保存任何类型的值(不初始化的情况下,变量会保存一个特殊值undefined)。
1.1 var声明作用域
使用var
操作符定义的变量会变成包含它的函数的局部变量。比如,使用var
在一个函数内部定义一个变量,就意味着该变量将在函数退出时销毁。
function test(){
var message = 'hello world';
}
test();
console.log(message); // 报错
这里,message
变量是在函数内部使用var
定义的。函数是test
,调用它会创建这个变量并给它赋值。调用之后变量随即被销毁。不过,在函数内定义变量时省略var
操作符,可以创建一个全局变量。
function test() {
message = 'hello world';
}
test();
console.log(message); // hello world
去掉var
操作符之后,message
就变成了全局变量。只要调用一个函数test()
,就会定义这个变量,并且在函数外部可以访问到。
注意
虽然可以通过剩余 var
操作符定义全局变量,但不推荐这么做。在局部作用域中定义的全局变量很难维护,也会造成困惑。在严格模式下,给未声明的变量复制,则会报错 ReferenceError
。
1.2 var声明提升
使用var
时,下面代码不会报错。这是因为使用这个关键字声明的变量会自动提升到函数作用域顶部
function foo () {
console.log(age);
var age = 18;
}
foo(); // undefined
之所以不报错,是因为ECMAScript
运行时把它等加成如下代码
function foo() {
var age;
console.log(age);
age = 18;
}
foo(); // undefined
这就是所谓的提升
,也就是把所有变量声明都拉到函数作用域的顶部。此外,反复多次使用var
声明同一个变量也是没有问题。
var age = 8;
var age = 18;
console.log(age); // 18
2.let声明
let
跟var
的作用差不多,但有着非常重要的区别。最明显的是,let
声明的范围是块作用域,而var
声明的范围是函数作用域。
function bar() {
if(true) {
var name = 'Tom';
console.log(name); // Tom
}
console.log(name) // Tom
if(true) {
let age = 18;
console.log(age); // 18
}
console.log(age); // ReferenceError: age没有定义
}
在这里,age
变量之所以不能在if
块外部被引用,是因为它的作用域仅限于块内部。块作用域是函数作用域的子集,因此适用于var
的作用域限制同样适用于let
。
let
不允许在一个块中出现冗余声明,这样会导致报错。
var name;
var name;
let age;
let age; // SyntaxError age已经被声明过了
JavaScript引擎会记录用于变量声明的标识符以及所在的块作用域,一次嵌套使用相同的标识符不会报错,这是因为在同一个块中没有重复声明
var name = 'Tom';
console.log(name); // Tom
if(true){
var name = 'Jerry';
console.log(name); // Jerry
}
let age = 30;
console.log(age); // 30
if(true) {
let age = 18;
console.log(age); // 18
}
对于声明冗余报错
不会因混用let
和var
受影响。这两个关键字声明并不是不同类型的变量,它们只是指出变量在相关作用域如何存在。
var name;
let name; // SyntaxError
let age;
var age; // SyntaxError
2.1暂时性死区
let
与var
的另一个重要的区别,就是let
声明的变量不会在作用域中被提升。
// name会提升
console.log(name);
var name = 'Tom';
// age 不会被提升
console.log(age); // ReferenceError
let age = 18;
在解析代码时,JavaScript引擎会注意出现在块后面的let
声明,只不过在此之前不能以任何方式来引用未声明变量。在let
声明之前的执行瞬间被称为暂时性死区
,在此阶段引用任何后面才声明的变量都会抛出异常。
2.2全局声明
与var
关键字不同,使用let
在全局作用域中声明的变量,不会成为window
对象的属性(var声明变量则会)。
var name = 'Tom';
console.log(window.name); // Tom
let age = 18;
console.log(window.age); // undefined
不过,let
声明仍然是在全局作用域中发生的,变量会在页面的生命周期内存在。
2.3条件声明
在使用var
声明变量时,由于声明会被提升,JavaScript引擎会自动将多余的声明在作用域顶部合并为一个声明。因为let
的作用域是块,所以不可能检查前面是否已经使用let
声明过同名变量,一旦在同一个块作用域内重复使用let声明同名变量,就会抛出SyntaxError错误。
即使你想使用try/catch
或typeof
操作符也不能解决,因为条件块中let
声明的作用域仅限于改块。
if(typeof name === undefined) {
let name;
}
// 因为name被限制在if块中,因此下面的赋值形同全局赋值
name = 'Tom';
try{
console.log(age); // 如果age没有被声明过,则会报错
}catch(error) {
let age;
}
age = 18;
对于let
声明不能依赖条件声明模式,var
则可以。
提示
不能使用let
进行条件声明是好事,因为条件声明是一种反模式,让程序变得更难理解。
2.4for循环中的let
在let
出现之前,for
循环定义的迭代变量会渗透到循环体外部。
for(var i = 0; i < 5; i++) {
}
console.log(i); // 5
改成let
之后,这个问题就没有了,因为迭代变量的作用域仅限于for
循环块内部
for(let i = 0; i < 5; i++) {
}
console.log(i); // ReferenceError i没有定义
在使用var的时候,最常见的问题就是迭代变量的修改
for(var i = 0; i < 5; i++) {
setTimeout(() => {console.log(i)}, 0);
}
可能会认为输出 0、1、2、3、4
但结果输出的是 5、5、5、5、5
之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5。在之后执行超时任务时,所有的i
都是同一个变量。
在使用let
声明迭代变量时,JavaScript引擎会为每个迭代循环声明一个新的迭代变量。每个setTimeout
引用的都是不同的变量实例。
for(let i = 0; i < 5; i++) {
setTimeout(() => {console.log(i)}, 0); // 0、1、2、3、4
}
3.const声明
const
的行为与let
基本相同,唯一一个重要的区别是const
声明变量时必须同时初始化变量的值,且在尝试修改这个变量时会导致报错---const
声明的是常量。
const age = 18;
age = 20; // TypeError 不能给常量赋值
const
声明的限制只适用于它指向的变量引用。如果const
变量引用的是一个对象,那么修改这个对象的属性是允许的。
4.声明风格及最佳实践
1.不使用var
有了let/const
,大多数开发者会发现不在需要var
了。限制自己使用let/const
有助于提高代码的质量,因为变量有了明确的作用域、声明的位置、以及不变的值。
2.const优先,let次之
使用const
声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。因此只有在提前知道未来会修改时再使用let
。
💬 欢迎评论!请确保您已登录 GitHub。