在《ES6标准入门》第二章中讲解了let和const命令的基本用法,但是在我阅读时发现本章节上来先讲的let的用法,而把块级作用域放在了后面讲解,我个人感觉是不利于新手了解的。要想了解let,const,var三者的区别,首先得了解什么是块级作用域。示例代码我尽可能采用本书中原本的代码,这样可以对照读起来更方便。
那么什么是块级作用域呢?
在JavaScript语言中,用一对花括号{ }包裹起来的这一段代码块的空间称作块级作用域,块级作用域可以用来限定变量和函数的作用域范围。在块级作用域中声明的函数和变量只能在代码块内部访问,它就像一个箱子,把代码封装起来,外部没有办法去访问和使用。
在ES5中是没有块级作用域的,只有全局作用域和函数作用域这么一说。会造成两种问题:
- 通过变量提升(hoisting)特性,内层变量会覆盖外层变量。
下面是作者在书中举的例子
var tmp = new Date();
function f() {
console.log(tmp);
if (false) { var tmp = 'hello world'; }
}
f(); // undefined
在ES5中,if中的var tmp = 'hello world'中tmp变量的声明会被提升到f()的作用域的顶部,if条件为false,并不会执行赋值,所以最终会被浏览器认为是下面这样的:
var tmp = new Date();
function f() {
var tmp = undefined;
console.log(tmp);
//if并不会执行
}
f(); // undefined
-
用来计算的循环变量会变为全局变量
作者在书中举的例子
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
使用var声明的循环变量变为了全局变量,这也就是为什么在循环结束后,还能访问到i的值,值为5,多说一句,如果想要i的值在循环后不可见,可以使用let或const来声明循环变量,毕竟for循环的循环条件本就是一个块级作用域。
那么在ES6中,就新增了块级作用域。
- ES6允许块级作用域的嵌套,但我们要注意一点,外层作用域无法访问内部作用域的变量,但是内层作用域在没有找到相应变量时,会向外一层一层递归寻找,直到全局作用域那一层,这也叫域链。
{{{{
{let name = 'Hello World'}
}}}};
- 内层作用域可以与外层作用域变量名字相同,如果想详细了解这块知识,可以去了解一下词法作用域。
{{{{
let name = 'Hello World'
{let name = 'Hello World'}
}}}};
现在就可以讲一讲let和const命令了。
Let命令
let命令声明的变量只在他所在的作用域内生效。
{
let a = 10;
}
其实这里可以讲一讲一个for循环的一个坑,作者也在这一块有讲解。
这是作者在书中举得例子:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
console.log("i" in window); //true
其实这里不论调用a[i]任何一个,其输出结果都是10,这也就是为什么我在一开始要说作用域的问题了,在for循环中,通过console.log("i" in window)返回的true可以i是一个全局的变量,所以对i的判断以及后续的运算,都是对同一个i进行的操作。
于是在最后不论a数组的任意一个运算,最终的结果都是10。若说的详细一点,可以这么理解,for循环将console.log(i);存储在了a[i]中,当调用a[6]的,函数就会去寻找i,此时全局变量为10,所以最终打出来的就是10。
如果把循环内的var变为let,那么打印出的结果则更符合直觉,这是因为let具有块级作用域,
每次循环的i都不相同,我们可以理解为带着角标的 i1,i2...,结果如下:
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[0](); // 输出: 0
a[1](); // 输出: 1
// ...
a[9](); // 输出: 9
另外,书中还提到了一个小的tips,循环变量的循环条件与循环体是一个父子作用域的关系。
此外,在书中还提到了let不存在变量提升,我其实更愿意称其为先声明后使用更容易让人理解。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
文中还提到了暂时性死区(temporal dead zone,简称 TDZ),我的理解就是在块级作用域内,产生了先使用后声明的错误现象(let和const需要先声明后使用,需要区分清楚),从块级作用域第一个{开始到变量声明的这一段距离,就是暂时性死区。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
let命令不允许在同一个作用域内重复声明,当然const也是如此。
const命令
const命令是不允许重复赋值的,就像同一根棉签不能给两个人测核酸,而且const命令需要在声明时就对其赋值,否则就会报错。const命令和let命令一样,都具有暂时性死区,同时也需要先声明后使用。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
一般新手在const这里出现的疑问点就是,为什么我使用const声明的一个对象,对象内的值可以更改。类似于:
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
这就要讲到简单类型的数据,譬如说数字,字符串,布尔值,它们的值是保存在一个内存地址中的,然后const声明的变量存储这个这个内存地址,因此同等于常量,不可以重复赋值。对于对象或者数组,const声明的变量其实存储的也是对象或者数组的地址,但是对象或者数组内部的数据结构是可以改变的。就好比快递员运输快递,他们只会关注你的快递单号是否正确,对箱子内的东西是不关注的,这里的快递单号就好比对象的地址,快递箱子内的东西就好比对象内的数据结构。