序
为什么选择ES6的Promise?——Promise能比较好地解决异步嵌套问题,ES6代表前端的未来,涉及很多现代编程语言概念中很流行的部分,让前端程序员的代码更加优美并且能够向前兼容。
ES6和JS的关系
ECMAScript和JavaScript的关系是,前者是后者的规格,后者是前者的 一 种实现。
let和const命令
let命令
基本用法
ES6中的let命令用法类似于var,但是所声明的变量只在let命令所在的代码块内生效:
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
for循环的计数器就很适合使用let命令:
var a = [];
for(let i = 0; i < 10; i++) // 如果此处使用var变量,则a[6]为10
{
a[i] = function(){
console.log(i)
}
}
a[6]() // 6
原因如下:
-
如果变量 i 是用var声明的,则该变量在全局范围内都有效,全局只有一个变量 i,每一次循环i的值都会发生变化。也就是说,所有数组 a 的成员中的 i 指向的都是同 一个 i ,导致运行时输出的是最后一轮的 i 值,也就是 10 。
-
如果变量 i 是用let声明的,则当前i只在本轮循环中有效,所以每一次循环 i 都是一个新的变量,所以最终输出6
如果每一轮循环的变量 i 都是重新声明的,那么它怎么知道上一轮的循环的值从而计算出本轮循环的值呢?
JavaScript引擎内部会记住上一次循环的值,初始化本轮的 i 值时,就在上一轮的循环中计算。
此外,for循环还有一个特别之处:设置循环变量的那一部分是一个父作用域,循环内部是一个单独的子作用域:
for(let i = 0; i < 3; i++)
{
let i = 'abc'
console.log(i)
}
// abc
// abc
// abc
不存在变量提升
- var命令会出现变量提升现象,即变量可以在声明前使用,值为undefined。
- let所声明的变量一定要在声明后使用
暂时性死区
只要块级作用域存在let命令,它所声明的变量就“绑定”这个区域,不受外部影响:
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp; // let的声明导致tmp变量绑定这个作用域
}
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上称为“暂时性死区”(temporal deadzone,简称TDZ)。
“暂时性死区”也意味着 typeof 不再是一个百分之百的安全操作。
typeof x; // ReferenceError
let x;
上面的代码中,变量x使用let命令声明,所以在声明之前都属于x的“死区”,只要用到该变量就会报错。
几种较为隐蔽的“死区”:
//1.
function bar(x = y, y = 2){
return [x, y];
// 之所以调用bar函数报错,是因为参数x的默认值等于另一个参数y,而此时y还没有声明。如果y的默认值是x,就不会报错。
}
//2.
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError:x is not defined
不允许重复声明
let不允许在相同作用域内重复声明同一个变量。
// 报错
function () {
let a = 10;
var a = 1;
}
// 报错
function func(arg){
let arg = 10;
}
块级作用域
ES6的块级作用域
- 外层作用域无法读取内层作用域的变量
- 内层作用域可以定义外层作用域的同名变量
块级作用域的出现,实际上使得获得广泛应用的立即执行函数匿名函数(IIFE)不再必要了。
// IIFE写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
块级作用域与函数声明
ES6引入了块级作用域,明确允许在块级作用域之中声明函数。
ES6规定,在块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
注意:在ES6浏览器中,块级作用域内声明函数的行为类似于var声明变量!
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式的形式,而不是函数声明语句:
{
let a = 'abc';
let f = function(){
return a;
}
}
注意:ES6的块级作用域允许声明函数的规则只在使用大括号的情况下成立,如果没用使用大括号则会报错!
do表达式
本质上,块级作用域是一个语句,将多个操作封装在一起,没用返回值。即在外部无法获取块级作用域内部变量的值。
现在有一个提案,使得块级作用域可以变为表达式,即可以返回值,方法就是在块级作用域之前加上do语句,使它变为do表达式:
let x = do{
let t = f();
t * t + 1;
}
// 变量x会得到整个块级作用域的返回值
const命令
基本用法
const声明一个只读的常量,一旦声明,常量的值就不能改变。
————这意味着,const一旦声明常量,就必须立即进行初始化,不能留到以后赋值。
- const作用域与let命令相同,只在声明所在的块级作用域内有效。
- const命令声明的常量也不会提升,同样存在暂存性死区,只能在声明后使用。
- const声明常量和let一样,不可重复声明
本质
const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。
对于简单类型的数据(数值、字符串、布尔值)而言,值就保存在变量指向的内存地址中,因此等同于常量。
但对于复合类型的数据(主要是对象和数组)而言,变量指向的内存地址保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,这完全不能控制。——因此声明一个对象为常量时必须非常小心。
const foo = {};
// 为foo添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将foo指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
注意:不可变的只是这个对象的地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
如果真的想将对象冻结,应该使用Object.freeze
方法。
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用,因为该对象被冻结
// 严格模式时,改行会报错
foo.prop = 123;
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach((key, i) => {
if(typeof obj[key] === 'object'){
constantize(obj[key])
}
})
}
ES6声明变量的六种方法
- var命令
- function命令
- let命令
- const命令
- import命令
- class命令
顶层对象的属性
顶层对象在浏览器环境中指的是window对象,在Node环境中指的是global对象。在ES5中,顶层对象的属性与全局变量是等价的。
顶层对象的属性与全局变量相关,被认为是JavaScript语言中最大的设计败笔之一 :
- 无法在编译时就提示变量未声明的错误,只有在运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的)。
- 程序员很容易不知不觉地创建全局变量。
- 顶层对象的属性是到处都可以读写的,这非常不利于模块化编程。
- window对象有实体含义,指的是浏览器的窗口对象,这样也是不合适的。
ES6为了改变这一点,规定:
- 为了保持兼容性,var命令和function命令声明的全局变量依旧是顶层对象的属性
- 另一方面规定,let命令、const命令、class命令声明的全局变量不属于顶层对象的属性
var a = 1;
// 如果在Node的REPL环境,可以写成gloabal.a
// 或者采用通用方法,写成this.a
window.a // 1
let b = 1;
window.b // undefined
global对象
ES5的顶层对象本身也是一个问题,因为它在各种实现中是不统一的。
- 在浏览器中,顶层对象是window,但 Node 和 Web Worker 没有 window。
- 在浏览器和Web Worker中,self也指向顶层对象,但是Node没有self。
- 在Node中,顶层对象是global,但其他环境都不支持。
同一段代码为了能够在各种环境中取到顶层对象,目前一般是使用this
变量,但是也有局限性。
- 在全局环境中,this 会返回顶层对象。但是,在 Node 模块和 ES6模块中,this返回的是当前模块。
- 对于函数中的 this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,this会返回 undefined。
- 不管是严格模式,还是普通模式,
new Function ('return this') ()
总会返回全局对象。但是,如果浏览器用了 CSP(内容安全政策),那么eval、new Function 这些方法都可能无法使用。
现有一个提案,在语言标准的局面引入 global
作为顶层对象。也就是说,global
都是存在的,都可以拿到顶层对象。
// CommonJS的写法
require('system.global/shim')();
// ES6模块的写法
import shim from 'system.global/shim'; shim();
上面的代码可以保证,在各种环境中global对象都是存在的。
// CommonJS的写法
var global = require('system.global')();
// ES6模块的写法
import getGlobal from 'system.global';
const global = getGlobal()
上面的代码将顶层对象放入变量global中。
变量的解构赋值
数组的结构赋值
基本用法
- ES6语法可以从数组中提取值,按照对应位置对变量赋值:
let [foo, [[bar], baz]] = [1, [[2], 3]];
let [, , third] = ['foo', 'bar', 'baz'];
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
本质上这种写法属于模式匹配,只要等号两边的模式相同,左边的变量就会赋予对应的值。
- 如果解构不成功,变量的值就等于undefined。
let [foo] = [];
let [bar, foo] = [1];
- 另一种情况是不完全解构,即等号左边的模式只匹配一部分的等号右边的数组,这种情况下,解构依然可以成功。
let [x, y] = [1, 2, 3];
x // 1
y // 2
- 如果等号的右边不是数组(或者严格说不是可遍历的结构)那么将会报错:
// 报错
let [foo] = 1;
let [foo] = NAN;
let [foo] = {}
上面的语句都会报错,因为等号右边的值或是转为对象以后不具备Iterator接口,或是本身就不具备Iterator接口。
对于Set结构,也可以使用数组的解构赋值——事实上,只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。
默认值
- 解构赋值允许指定默认值:
let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x = 'a', y = 'b'
let [x, y = 'b'] = ['a', undefined]; // x = 'a', y = 'b'
注意:ES6内部使用严格相等运算符(===)判断一个位置是否有值。所以,如果一个数组成员不严格等于
undefined
,默认值是不会生效的。
例如:
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null
上面的代码中,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined
- 如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到时才会求值。
function f() {
console.log('aaa');
}
let [x = f()] = [1];
- 上面的代码中,因为x能取到值,所以函数f根本不会执行。上面的代码等价如下:
let x;
if ([1][0] === undefined) {
x = f();
} else {
x = [1][0];
}
- 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [] // ReferenceError
对象的解构赋值
解构不仅可以用于数组,还可以用于对象。
对象的解构和数组有一个重要的不同——
数组的元素是按次序排列的,变量的取值是由它的位置决定的;
而对象的属性没有次序,变量必须与属性同名才能取到正确的值。
如果变量名和属性名不一致,必须写成下面这样:
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // 'aaa'
实际上,对象的赋值解解构是下面形式的简写:
var { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
也就是说,对象的解构赋值的内部机制是先找到同名属性,然后再赋值给对应的变量。真正被赋值的是后者,而不是前者。
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
上面的代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。
- 与数组一样,解构也可以用于嵌套结构的对象。
- 对象的解构也可以指定默认值——默认值生效的条件是,对象的属性值严格等于undefined。
- 如果解构失败,变量的值等于
undefined
。 - 如果解构模式是嵌套的对象,且子对象所在的父属性不存在,那么将会报错。
如果要将一个已经声明的变量用于解构赋值,必须非常小心:
let x;
{x} = {x: 1};
// SyntaxError: syntax error
这是因为JavaScript引擎会将 {x} 理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免JavaScript将其解释为代码块,才能解决这个问题:
let x;
({x} = {x: 1});
关于圆括号与赋值结构的关系,参见下文:
解构赋值允许等号左边的模式之中不放置任何变量名:
({} = [true, false]);
({} = 'abc')
上面的表达式虽然毫无意义,但可以执行。
对象的解构赋值可以很方便地将现有对象的方法赋值到某个变量。
let { log, sin, cos } = Math;
上面的代码将Math对象的对数、正弦、余弦三个方法赋值到对应的变量上,使用起来就方便很多。
由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
字符串的解构赋值
- 字符串可以解构赋值,因为此时字符串被转换成了一个类似数组的对象。
- 类似数组的对象都有一个length对象,因此还可以对这个属性进行解构赋值。
数值和布尔值的解构赋值
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
赋值解构的规则是:只要等号右边的值不是对象或数组,就先将其转为对象。
由于undefined和null无法转换为对象,所以对它们进行解构赋值时会报错。
函数参数的解构赋值
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
上面的代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y,对于函数内部的代码来说,它们能感受到的参数就是x和y。
另一个例子:
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [3, 7]
圆括号问题
解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。
由此带来的问题是,如果模式中出现圆括号该怎么处理?
ES6的规则是,只要有可能导致解构的歧义,就不得使用圆括号。
不能使用圆括号的情况
- 变量声明语句
- 函数参数
- 赋值语句的模式,即将整个模式放在圆括号中(而非将整个赋值语句放在圆括号中)
可以使用圆括号的情况
可以使用圆括号的情况只有 一 种 : 赋值语句的非模式部分可以使用圆括号。
[(b)] = [3];
({ p: (d)} = {});
[(parseInt.prop)] = [3];