let 命令
基本用法
用于声明变量,用法类似于var
,但所声明的变量,只在let
命令所在的代码块内有效。
{
let a = 1;
var b = 2;
}
a // ReferenceError: a is not defined.
b // 1
上面代码在代码块之中,分别用let
和var
声明了两个变量。然后在代码块之外调用这两个变量,结果let
声明的变量报错,var
声明的变量返回了正确的值。这表明,let
声明的变量只在它所在的代码块有效。let
常搭配for
循环使用。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
由上述代码可知,var
声明的i
变量在全局范围内有效,所以全局范围内只有一个变量i
。每次循环都会使i
的值+1
,因此循环内数组a
的函数内的console.log(i)
中的i
指向全局的同一个i
。而let
声明的i
变量只在本轮循环有效,每次循环的i
都是一个新的变量(在JavaScript引擎内部会记住上一轮循环的值,初始化本轮变量i
时,就在上一轮循环的基础上进行计算)。
除此之外,for
循环设置循环变量的部分使一个父作用域,而循环体内部使一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = "abc";
console.log(i);
}
// abc
// abc
// abc
在同一个作用域中不可使用let
重复声明同一个变量
// 报错
function foo() {
let a = 10;
var a = 2;
}
// 报错
function bar() {
let a = 1;
let a = 2;
}
不可以在函数内部重新声明参数
function foo(arg) {
let arg;
}
func(); // 报错
function bar(arg) {
{
let arg;
}
}
func(); // 不报错
不存在变量提升
// var
console.log(a); // undefined
var a = 2;
// let
console.log(b); // 报错ReferenceError
let b = 2;
暂时性死区(Temporal Dead Zone, TDZ)
在代码块内,使用let
命令声明变量之前,该变量都是不可用的。
var tmp = 1;
if (true) {
tmp = "a"; // ReferenceError
let tmp;
}
虽然存在全局变量tmp
,但在块级作用域内let
又声明了一个局部变量tmp
,导致后者绑定了该块级作用域,所以在let
声明变量前,对tmp
赋值会报错。
暂时性死区意味着typeof
不再是一个绝对安全的操作。
typeof a; // ReferenceError
let x;
如果一个变量没有声明,使用typeof
反而不会报错
typeof undeclared_variable // "undefined"
举例:一些比较隐蔽的死区
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
在上述代码中,由于bar(x = y, y = 2)
首先读取参数x
,并令参数x
默认值等于另一个参数y
,但此时y
还没有声明,属于死区;与之相对的,若y
的默认值是x
,则不会报错,因为此时x
已经声明。
function bar(x = 2, y = x) {
return [x, y];
}
bar();
由于暂时性死区以下代码也会报错。
var x = x; // undefined
let x = x; // ReferenceError: x is not defined
const 命令
基本用法
const
声明一个只读的常量,该常量的值在声明后不可改变。
const
声明后必须立即初始化
const
作用域与let
相同,只在声明所在的块级作用域内有效
const
同样存在暂时性死区,只能在声明的位置后调用
const
只能在声明后调用
const PI = 3.141592;
PI // 3.141592
PI = 3; // TypeError: Assignment to constant variable
const a; // SyntaxError: Missing initializer in const declaration
if (true) {
const b = 1;
}
b // Uncaught ReferenceError: b is not defined
if (true) {
console.log(c); // ReferenceError
const c = 1;
}
var x = 1;
let y = 2;
const x = 2;
const y = 3; // SyntaxError: Identifier 'a' has already been declared
const 命令的本质
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
上面代码中,常量foo
储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo
指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
下面是另一个例子。
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
上面代码中,常量a
是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a
,就会报错。
如果真的想将对象冻结,应该使用Object.freeze()
方法。
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
上面代码中,常量foo
指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
ES6 的块级作用域
function foo() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
function foo() {
let n = 5;
if (true) {
var n = 10;
}
console.log(n); // SyntaxError: Identifier 'n' has already been declared
}
function foo() {
let n = 5;
if (true) {
n = 10;
}
console.log(n); // 10
}
function foo() {
var n = 5;
if (true) {
var n = 10;
}
console.log(n); // 10
}
上述代码表示外层代码块不受内层代码块的影响。
ES6 允许块级作用域的任意嵌套,并且内层作用域可以定义外层作用域的同名变量
{{{{{
{ let a = 1 }
console.log(a); // 报错
}}}}}
{{{{{
let a = 1;
{ let a = 1 }
}}}}}
块级作用域使得匿名立即执行函数表达式(匿名IIFE)不再必要
// IIFE 写法
(function () {
var tmp = ...;
...
})
// 块级作用域写法
{
let tmp = ...;
...
}
块级作用域与函数声明
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
ES6 规定,块级作用域之中可以声明函数,函数声明语句的行为类似于let
,在块级作用域之外不可引用。
function f() {
console.log('outside');
}
(function () {
if (false) {
function f() { console.log('inside'); }
}
f();
}());
上述代码在 ES5 中运行,会得到"inside",实际的运行代码如下
function f() { console.log('outside'); }
(function() {
function f() { console.log('inside'); }
if (false) {}
f();
}())
但是在 ES6 中,由于块级作用域,理论上在if(false) {}
中声明的f()
不应影响在块级作用域外的f()
,即理论上打印的结果应为"outside·"。
但在实际的 ES6 浏览器中运行该代码会报错
// 浏览器的 ES6 环境
function f() {
console.log('outside');
}
(function () {
if (false) {
function f() { console.log('inside'); }
}
f();
}());
// Uncaught TypeError: f is not a function
上述代码在 ES6 浏览器中,都会报错,原因如下:
如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
-
允许在块级作用域内声明函数。
-
函数声明类似于
var
,即会提升到全局作用域或函数作用域的头部。 -
同时,函数声明还会提升到所在的块级作用域的头部。
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let
处理。
因此,上述代码实际运行如下:
// 浏览器的 ES6 环境
function f() {
console.log('outside');
}
(function () {
var f = undefined; // 将函数声明提升到函数作用域头部
if (false) {
function f() { console.log('inside'); }
}
f();
}());
// Uncaught TypeError: f is not a function
因此,环境导致的结果差距,应避免在块级作用域内声明函数。若需要,也应写成函数表达式形式。
// 块级作用域内的函数声明语句,建议不要使用
{
let a = 'inside';
function f() {
return a;
};
}
// 块级作用域内优先使用函数表达式
{
let a = 'inside';
let f = function() {
return a;
};
}
ES6 的块级作用域必须有大括号,如果无大括号,则 JS 引擎认为不存在块级作用域
if (true) let x = 1; // 没有大括号,报错
if (true) {
let x = 1; // 不报错
}
参考:阮一峰的ES6教程