JavaScript 中的变量是松散类型的,可以保存任何类型数据,变量只不过是一个名称。JavaScript 中,可以声明变量的关键字有var
、let
和const
。
1. var
使用var
定义变量,可以保存任何类型的值。若不初始化变量,变量会保存undefined
。
1. 函数级作用域
使用var
定义的变量会成为包含它的函数的局部变量。
function func() {
var a = 'hi'; // 局部变量
}
func();
console.log(a); // ReferenceError: a is not defined
变量a
在函数内部使用var
定义,调用函数func
,会创建这个变量并给它赋值。函数执行结束后,变量就会被销毁,所以上述代码最后一行会报错,显示变量a
未定义。
若在函数内部,不使用var
操作符,直接给变量a
赋值,那么a
就成为了全局变量,可以在函数外部访问到。在浏览器环境下,a
成为window
对象的属性。
function func() {
a = 'hi'; // 全局
}
func();
console.log(a); // hi
console.log(window.a); // hi
console.log(window.a === a); // true
2. 变量提升
使用var
声明变量,会自动提升到函数作用域顶部,如下代码:
function func() {
console.log(a);
var a = 1;
}
func(); // undefined
代码没有报错,输出了undefined
,这是因为变量的声明被提升到了函数作用域顶部,等价于如下代码:
function func() {
var a;
console.log(a);
a = 1;
}
func(); // undefined
3. 重复声明
另外,使用var
重复声明同一个变量也可以:
function func() {
var a = 1;
var a = 2;
var a = 3;
console.log(a);
}
func(); // 3
4. 全局变量挂载到 window
浏览器环境中,全局作用域下,使用var
声明的变量,会挂载到window
对象上。
var a = 1;
console.log(window.a === a); // true
2. let
let
也可以声明变量,但和var
操作符有很大的区别。
1. 块级作用域
以下代码会报错,因为let
声明的作用域,具有块级作用域,即被{}
包裹的部分。
if (true) {
let a = 10;
}
console.log(a); // a is not defined
2. 不可重复声明
以下代码,执行到let a = 2
就会报错,因为变量a
在当前块级作用域中已经被声明过了,不能重复声明。
if (true) {
let a = 1;
let a = 2; // SyntaxError: Identifier 'a' has already been declared
let a = 3;
}
另外,如果混用var
和let
声明变量,也是不允许的,下面的代码都会报错:
let a;
var a; // 报错
var a;
let a; // 报错
2. 不存在变量提升(暂时性锁区)
使用let
声明的变量,不能在声明之前访问它。
if (true) {
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 1;
}
实际上,JavaScript 也会注意出现在块后面的let
声明,只不过在此之前不能以任何方式来引用未声明的变量。在let
声明之前的执行瞬间被称为暂时性死区。
3. 全局变量不会挂载到 window
和var
不同,即使在全局作用域下,使用let
声明的变量也不会挂载到window
对象。
var a = 1;
let b = 2;
console.log(window.a === a); // true
console.log(window.b === b); // false
4. 不依赖条件声明
if (typeof name === 'undefined') {
let name;
}
// name 被限制在 if {} 块的作用域内
name = 'Matt'; // 全局赋值
try {
console.log(age); // 如果 age 没有声明过,则会报错
} catch (error) {
let age;
}
// age 被限制在 catch {}块的作用域内
age = 26; // 全局赋值
3. const
const
的特点与let
基本一致,但const
有一些自己的特点。
1. 声明变量时必须同时初始化
以下声明报错,因为声明的同时没有初始化变量。
const a; // Missing initializer in const declaration
2. 不能修改声明后的变量
使用const
定义了一个变量后,不能再更改它的值:
const a = 1;
a = 2; // TypeError: Assignment to constant variable
这里有一个误区,实际上,使用const
声明的变量,不能修改的是内存地址!!
具体规则如下:
- 当
const
定义的常量为基本数据类型时,不能被修改。 - 当
const
定义的常量为引用数据类型时,可以通过其属性进行数据修改。
基本数据类型的值就保存在内存地址中,所以const
定义的基础数据类型不可被改变。 而引用数据类型指向的内存地址只是一个指针,通过指针来指向实际数据,也就是说,不可被改变的是指针,而不是数据,所以const
定义的引用数据类型的常量可以通过属性来修改其数据。
例如,使用const
定义了一个数组,虽然不能更改数据类型,但可以通过push
等方法,修改这个数组中的数据。
const arr = [];
arr.push(1, 2, 3);
console.log(arr); // [ 1, 2, 3 ]
4. 总结及最佳实践
|
|
|
函数级作用域 | 块级作用域 | 块级作用域 |
重复声明 | 不可重复声明 | 不可重复声明 |
变量提升 | 不存在变量提升 | 不存在变量提升 |
值可更改 | 值可更改 | 值不可更改 |
全局变量挂载到 | 全局变量不会挂载到 | 全局变量不会挂载到 |
通常,写 JavaScript 代码时,遵循以下原则:
- 不使用
var
-
const
优先,let
次之
5. 一道面试题
以下代码运行后会打印什么?
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
答案:6 6 6 6 6
虽然每个for
循环中定时器设置的时间都是0
,但由于 JavaScript 是单线程 eventLoop
机制,setTimeout
是异步任务,遇到setTimeout
函数时,JavaScript 会将其放入任务队列中,待同步任务执行完毕后,才执行任务队列中的异步任务。
又因为setTimeout
函数也是一种闭包,往上找它的父级作用域链是window
,而变量i
是用var
声明的,是window
上的全局变量,所以此时变量i
的值已经变成i = 6
了,最后执行setTimeout
时,当然会输出 5 个6
了!
使用let
解决:
利用 JavaScript 的块级作用域,就不用这么麻烦了。如果for
循环使用块级作用域变量关键字,循环就会为每个循环创建独立的变量,从而每次打印都会有正确的索引值。
for (let i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
只介绍了其中一个解决方法,详细分析及其他解决方法见JavaScript经典题 —— 解决循环打印问题。
参考资料:
《JavaScript高级程序设计(第4版)》