在 JavaScript 中,var
、let
和const
是用于声明变量的关键字。
let
和const
是JavaScript里相对较新的变量声明方式。
let
用法类似于var
,但是所声明的变量,只在let
命令所在的代码块内有效。
const
声明一个只读的常量。一旦声明,常量的值就不能改变。
const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值。
对于const
来说,只声明不赋值,就会报错。
示例:
const PI = 3.1415;
console.log(PI) // 3.1415
PI = 3; // Uncaught TypeError: Assignment to constant variable.
const a; // Uncaught SyntaxError: Missing initializer in const declaration
var
、let
和 const
的区别:
重复声明
var
:可以重复声明同一个变量。let
和const
:不允许在同一作用域内重复声明同一个变量。
var a = 1;
var a = 2; // 不会报错
let b = 3;
let b = 4; // Uncaught SyntaxError: Identifier 'b' has already been declared
const c = 5;
const c = 6; // Uncaught SyntaxError: Identifier 'c' has already been declared
变量提升
var
:存在变量提升,即在变量声明之前使用该变量,不会报错,只是值为undefined
。
当函数开始的时候,就会处理var
声明(脚本启动对应全局变量)。
换言之,var
声明的变量会在函数开头被定义,与它在代码中定义的位置无关(这里不考虑定义在嵌套函数中的情况)。let
和const
:不存在变量提升,在声明之前使用会报错。
使用 var
声明,示例:
console.log(a); // 输出 undefined
var a = 10;
// 上面代码的实际执行顺序如下:
// var a;
// console.log(a);
// a = 10
使用var
声明变量 a
,会发生变量提升,即脚本开始运行时,变量 a
已经存在了,但是没有值,所以会输出undefined
。
使用let
、const
声明,示例:
console.log(b); // Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 20;
console.log(c) // Uncaught ReferenceError: Cannot access 'c' before initialization
const c = 30;
message = "message"; // Uncaught ReferenceError: Cannot access 'message' before initialization
let message;
从程序执行进入代码块(或函数)的那一刻起,变量b
、c
就开始进入“未初始化”状态。它一直保持未初始化状态,直至程序执行到相应的 let
、const
语句。
ES6 明确规定,如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
在代码块内,使用 let
、const
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
作用域
var
:函数作用域或全局作用域。在函数内使用var
声明的变量,在函数外部无法访问;在函数外部使用var
声明的变量则是全局变量。let
和const
:块级作用域。即它们在{}
内声明的变量,在{}
外部无法访问。
注意:ES6 的块级作用域必须有大括号{}
,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
由于 var
会忽略代码块,因此我们有了一个全局变量 a
。
if (true) {
var a = 10; // 使用 "var" 而不是 "let"
}
console.log( a ); // 10,变量在 if 结束后仍存在
对于循环,var
声明的变量没有块级作用域也没有循环局部作用域:
for (var i = 0; i < 10; i++) {
var one = 1;
}
console.log(i); // 10,"i" 在循环结束后仍可见,它是一个全局变量
console.log(one); // 1,"one" 在循环结束后仍可见,它是一个全局变量
在函数内部,var 声明的变量的作用域将为函数作用域:
function sayHi() {
if (true) {
var phrase = "Hello";
}
alert(phrase); // 能正常工作
}
sayHi();
console.log(phrase); // ReferenceError: phrase is not defined
使用 let
、const
, 该变量仅在if
内部可见:
if (true) {
let b = 20; // 使用 "let", 该变量仅在if内部可见
const c = 30;
}
console.log(b); // Uncaught ReferenceError: b is not defined
console.log(c); // Uncaught ReferenceError: c is not defined
// 没有{},就没有块级作用域。词法声明不能出现在单语句上下文中
if(true) let a = 1; // Uncaught SyntaxError: Lexical declaration cannot appear in a single-statement context
// 必须要用{},才有块级作用域
if(true) { let a = 1; }
对于 if
,for
和 while
等,使用let
、const
在 {...}
中声明的变量也仅在内部可见:
for (let i = 0; i < 3; i++) {
// 此时,想在这里输出循环条件的i,报错。
// console.log(i); // Uncaught ReferenceError: Cannot access 'i' before initialization
let i = 'abc';
console.log(i); // abc
}
执行第一句console.log(i);
报错是因为在循环体内部再次声明了i
,并且在let
声明之前使用了变量。
for
循环还有一个特别之处:设置循环变量的那部分是一个父作用域,循环体内部是一个单独的子作用域。
值的可修改性
var
、let
:声明的变量的值可以修改。const
:声明的变量的值不能修改,但如果是对象或数组等引用类型,其属性可以修改。
var a = 10;
a = 20; // 可以修改
let b = 30;
b = 40; // 可以修改
const c = 50;
c = 60; // Uncaught TypeError: Assignment to constant variable.
const arr = [1, 2, 3];
// 可以修改数组的元素
arr.push(4);
console.log(arr); // [1, 2, 3, 4]
// 将 arr 指向另一个数组(引用地址被修改),就会报错
arr = [4, 5, 6]; // Uncaught TypeError: Assignment to constant variable.
const obj= {};
// 为 obj 添加一个属性,可以成功
obj.prop = 123;
console.log(obj.prop); // 123
// 将 obj 指向另一个对象(引用地址被修改),就会报错
obj = {}; // Uncaught TypeError: Assignment to constant variable.
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
如果真的想将对象冻结,应该使用Object.freeze
方法。
const obj = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
obj.prop = 123;
常量obj
指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};