第二章 Let 和 const 命令
第二章分别讲解了 let
和 const
两个用于声明变量的命令。并介绍了他们与 var
之间的异同点。下面来分别依次介绍。
let 命令
书中从 基础用法、变量提升、暂时性死区、重复声明 四个方面做了讲解。
基础用法
let
用于声明变量,与 var
相比,let
声明的变量只在代码块内有效;var
声明的变量在全局范围内都有效。这是一个作用域的概念,这个在后续更详细介绍。
有一个经典的 bug 如下:
for (var i = 0; i < 10; i ++) {
a[i] = function() {
console.log(i)
};
}
a[6]() // 10
console.log(i) // 10。var 声明的,在全局范围内都有效,所以全局只有 一个变量 i。 每一次循环,变量 i 的值都会发生改变,而循环内,被赋给数组 a 的函数内部的 console.log(i)中的 i 指向全局的 i。也就是说,所有数组 a 的成员中的 i 指向的都是同一个 i,导致运行时输出的是最后一轮的 i 值,也就是 10
// ------------------------------------
for (let i = 0; i < 10; i ++) {
a[i] = function() {
console.log(i)
};
}
a[6]() // 5
console.log(i) // i is not defind
变量提升
var
存在变量提升的现象,即变量可以在声明之前使用,值为 undefined
。
let
命令改变了语法行为,它所声明的变量一定要在声明后使用,否则便会报错。也就是说,let
不允许变量提升行为。
// var的情况
console.log (foo); // 输出 undefined
var foo = 2;
// let的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
暂时性死区
若在块级作用域中存在 let
命令,它声明的变量就 “绑定” 这个区域,不再受外部影响。
ES6 规定,若使用 let
和 const
声明变量,则在变量声明前都不可使用该变量,否则报错。在语法上被称为暂时性死区( temporal dead zone,简称 TDZ)。示例代码如下:
while (true) {
// TDZ 开始
tmp = 'abc'; // ReferenceError
console.log (tmp); // ReferenceError
let tmp; // TDZ结束
console.log (tmp) ; // undefined
tmp = 123;
console.log(tmp); // 123
}
有一些死区比较隐蔽,如:
function (x = y, y = 2) {
console.log(x); // ReferenceError。因为x用到了y变量赋值,而y变量在其后面声明,因此是死区
}
let x = x // ReferenceError
🧬 本质
只要进入当前作用域,所要使用的变量就己经存在,但是 不可获取,只有等到声明变量的那 一行代码出现 ,才可以获取和使用该变量。
重复声明
let
相比 var
,还有一个不同点是不允许重复声明。
var a = 1;
var a = 2;
console.log(a); // 2
let b = 1;
let b = 2; // 报错
作用域
作用域包含了 作用域作用、ES6 的块级作用域、块级作用域与函数声明、do
表达式 四个模块进行讲述。
作用域作用
阮一峰老师从 ES5 的作用域 入手,通过对比解释了 ES6 作用域的作用。在 ES5 中,只存在 全局作用域 和 函数作用域 两种,很多场景都显得很不合理。如:
场景一:内部变量覆盖外部变量
jsvar a = 1; if (true) { var a = 2; } console.log(a); // 2
场景二:循环变量全局泄漏
jsfor (var i = 0; i <= 3; i++) { // ... } console.log(i); // 4
ES6 的块级作用域
let
为 JavaScript 提供了块级作用域。外层作用域无法访问内层作用域的变量;内层作用域可以定义外层作用域同名变量且不会覆盖。
let a = 1;
if (true) {
let a = 2;
console.log(a); // 2
}
console.log(a); // 1
🖇 拓展
在 ES6 块级作用域出来前,想要实现上述方案,需要用到立即执行函数(IIFE),代码如下:
(function () {
var tmp = '...'
// ...
})
块级作用域的出现让立即执行函数的写法不再必要。
块级作用域与函数声明
在 ES5 中,函数只能在顶层作用域和函数作用域中声明,不能在块级作用域中声明。但是由于浏览器的兼容性考虑,在一些旧的浏览器中,函数声明在块级作用域中仍然是有效的。
ES6 引入了块级作用域,并明确允许在块级作用域中声明函数。在 ES6 规定中,块级作用域内声明的函数的行为类似于使用 let
声明的变量,只在块级作用域内有效,对作用域之外没有影响。
然而,在实际运行中,不同的浏览器对于块级作用域内函数声明的处理方式可能不同。ES6 规范允许浏览器的实现可以不遵守块级作用域内函数声明的规定,而有自己的行为方式。具体来说,这些浏览器会将块级作用域内的函数声明提升到全局作用域或函数作用域的头部。
💡 总结
- 在 ES5 中,函数不能在块级作用域中声明,但一些浏览器为了兼容性支持在块级作用域中声明函数。
- 在 ES6 中,函数可以在块级作用域中声明,但不同浏览器对此的处理方式可能不同。
为了避免代码的可读性和兼容性问题,应该避免在块级作用域内声明函数。如果确实需要,建议在块级作用域中使用函数表达式或箭头函数来代替函数声明。
{
let a = 1;
let f = function () {
// ...
}
}
在 ES6 中,块级作用域允许声明函数的规则只在使用大括号的情况下成立。如果没有使用大括号,就会导致语法错误。
具体来说:
- 在使用大括号包裹的块级作用域内声明函数是有效的。
- 在没有使用大括号包裹的情况下声明函数会导致语法错误。
if (true) {
function f() {} // 不报错
}
if (true) function f() {} // 报错
因此,需要注意在 ES6 中声明函数时,确保在块级作用域内使用大括号来包裹函数声明,以避免出现语法错误。
do表达式
在ES6中,块级作用域引入了 let
和 const
关键字,使得在代码块中声明的变量可以在该代码块外部使用。但是,由于块级作用域没有返回值,当我们需要在代码块外部获取代码块中计算结果时,就需要将变量定义在全局作用域中。
为了解决这个问题,提案(do expressions)提出了一种新的语法形式:do
表达式。使用 do
表达式可以将一段代码块封装成一个表达式,并且可以在该表达式后直接返回一个值。因此,我们可以使用do表达式来获得代码块中的计算结果,并将其赋值给变量。
例如,假设我们需要计算一个数的平方并加上另一个数,我们可以使用如下的代码:
let t = 2;
let l = 3;
let r = do {
let x = t * t;
x + l;
}
console.log(r); // 输出 7
以上代码中,我们使用do表达式将两个操作封装在一起,并将计算结果赋值给变量r。最终,我们可以在代码块外部获取变量r的值。
const 命令
书中从 基本用法、本质、声明变量的6种方法 三个方向做了详细介绍,其中:
基本用法
const
声明的是一个只读的常量,声明后该变量不可改变const
声明常量不可改变,因此在声明时就必须立即初始化,否则报错const
声明的变量不存在变量提升,同时也存在暂时性死区
const a = 1;
a = 2; // TypeError: Assignment to constant variable
const foo; // SyntaxError: Missing initializer in const declaration
console.log(tmp); // ReferenceError
const tmp = 123;
本质
const关键字实际上保证的是变量指向的内存地址不得改动,对于简单类型的数据(如数值、字符串、布尔值),其值保存在变量指向的内存地址中,因此等同于常量。
但是对于复合类型的数据(主要是对象和数组),变量指向的内存地址保存的只是一个指针,因此 const
只能保证这个指针是固定的,至于它指向的数据结构是否可变,这完全不能控制。
因此,当将一个对象声明为常量时必须非常小心。例如:
const foo = {};
// 为foo添加一个属性,可以成功
foo.prop = 123;
console.log(foo.prop); // 输出 123
// 将foo指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
上面的代码中,常量 foo
储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把 foo
指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
另外,如果真的想将对象冻结,应该使用 Object.freeze
方法。例如:
const foo = Object.freeze({});
// 在严格模式下,下面一行会报错
foo.prop = 123;
除了将对象本身冻结,对象的属性也应该冻结。以下是一个将对象彻底冻结的函数:
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'object') {
constantize(obj[key]);
}
});
};
这样,通过调用 constantize
函数,可以彻底冻结一个对象及其所有属性。
ES6 声明变量的6种方法
ES5中只有两种声明变量的方法:使用 var
命令和 function
命令。而 ES6 除了添加了 let
和 const
命令之外,还引入了 import
命令和 class
命令用于声明变量。
因此,ES6 一共有6种声明变量的方法:var
、function
、let
、const
、import
和 class
。
顶层对象属性
在 ES5 中,顶层对象的属性与全局变量是等价的,即顶层对象的属性赋值与全局变量的赋值是同一件事。这种设计被认为是 JavaScript 语言中最大的设计败笔之一,因为会导致无法在编译时提示未声明变量、容易不知不觉地创建全局变量以及不利于模块化编程。
为了改变这一点,ES6做出了规定:var
命令和 function
命令声明的全局变量依旧是顶层对象的属性,但 let
命令、const
命令、class
命令声明的全局变量不再是顶层对象的属性。从 ES6 开始,全局变量逐渐与顶层对象的属性隔离。
举例来说,使用 var
声明的全局变量会成为顶层对象的属性,而使用 let
声明的全局变量则不会成为顶层对象的属性,返回 undefined
。这样的设计改变了全局变量与顶层对象属性之间的关系,使得代码更加清晰和可靠。
var a = 1;
window.a // 1。如果是在 node 环境中,可写成 global.a 或 this.a
let b = 2;
window.b // undefined
global 对象
ES5 顶层对象也有不统一的问题,其中:
- 在浏览器中,顶层对象是
window
和self
- 在 Web Worker 中,顶层对象是
self
- 在 Node 中,顶层对象是
global
目前为了能在各种环境中获取顶层对象,通用方法是使用 this
变量,但是这也有缺点:
- 在全局环境中,
this
会返回顶层对象,但在 Node 模块和ES6模块中,this
会返回当前模块 - 对于函数中的
this
,如果函数不是作为对象的方法运行,则this
会指向顶层对象,但在严格模式下会返回undefined
。 new Function('return this')()
无论在何种模式下都会返回全局对象。
然而,在各种情况下找到一个通用的方法来获取顶层对象是很困难的。目前有一个提案在语言标准中引入 global
作为顶层对象,可以在所有环境下拿到顶层对象。同时,垫片库system.global
可以模拟这个提案,保证在各种环境下 global
对象都是存在的。
总结
第二章从 ES6 推出的 let
和 const
着手,讲解了他们和 var
之间的区别,如不存在变量提升、暂时性死区、不可重复声明、作用域等。
其中作用域阮一峰老师也展开讲解,ES6 与 ES5 相比多了一个块级作用域,其作用是为了解决 内部变量覆盖外部变量 和 循环变量全局泄漏 。
最后聊到了 顶层变量 ,在浏览器、Web Worker 和 Node 中,他们各自的顶层变量都不相同,为了解决这一现象,有人提出了引入 global
垫片方案。