自 2015 年以来,TC39 团队成员每年都会一起讨论可用的提案,并发布已接受的提案。 对于一个提案,从提出到最后被纳入ES新特性,TC39的规范中分为五步:

  • stage0(strawman),任何TC39的成员都可以提交。
  • stage1(proposal),进入此阶段就意味着这一提案被认为是正式的了,需要对此提案的场景与API进行详尽的描述。
  • stage2(draft),演进到这一阶段的提案如果能最终进入到标准,那么在之后的阶段都不会有太大的变化,因为理论上只接受增量修改。
  • state3(candidate),这一阶段的提案只有在遇到了重大问题才会修改,规范文档需要被全面的完成。
  • state4(finished),这一阶段的提案将会被纳入到ES每年发布的规范之中

提案的功能将在达到第 4 阶段后被添加到新的ECMAScript标准中,这意味着它们已获得 TC-39 的批准,通过了测试,并且至少有两个实现。

ES6 虽提供了许多新特性,但我们实际工作中用到频率较高并不多,根据二八法则,我们应该用百分之八十的精力和时间,好好专研这百分之二十核心特性,将会收到事半功倍的奇效!

一、开发环境配置

使用babel编译ES6语法,使用webpack实现模块化。(具体配置可查看文章结尾的链接)

二、块级作用域

ES5只有全局作用域和函数作用域。ES6通过let和const实现了块级作用域。

1. const 关键字声明的变量是“不可修改”的。

其实,const 保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。但对于引用类型的数据(主要是对象和数组),变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是不变的,至于它指向的数据结构就不可控制了。

因此实际开发过程中,我们发现const 定义一个对象,后面可以正常修改对象的值就是因为这个原因。

2. var的弊端:

(1)内层变量覆盖外存变量、循环变量泄露为全局变量

(2)存在变量提升

变量提升的本质是JavaScript引擎在执行代码之前会对代码进行编译分析,这个阶段会将检测到的变量和函数声明添加到 JavaScript 引擎中名为 Lexical Environment 的内存中,并赋予一个初始化值 undefined。然后再进入代码执行阶段。所以在代码执行之前,JS 引擎就已经知道声明的变量和函数。

这种现象就不太符合我们的直觉,所以在ES6中,let和const关键字限制了变量提升,let 定义的变量添加到 Lexical Environment 后不再进行初始化为 undefined 操作,JS 引擎只会在执行到词法声明和赋值时才进行初始化。而在变量创建到真正初始化之间的时间跨度内,它们无法访问或使用,ES6 将其称之为暂时性死区:

1
2
3
4
5
// 暂时性死区 开始
a = "hello"; // Uncaught ReferenceError: Cannot access 'a' before initialization
let a;
// 暂时性死区 结束
console.log(a); // undefined

所以,let和const解决var变量提升的本质并不是说不会在编译阶段进行变量提升,而是提升之后不进行初始化操作。

ESlint开启规则:"no-var": 0; 来保证项目中没有var声明的变量。

三、数组的扩展

1. Array.from()

Array.from() 将类数组对象(arguments对象、DOM元素集)或迭代器对象转换为数组。

Array.from的第二个参数可以像[].map一样使用 Array.from 方法。

1
const array3 = Array.from(array, (num) => num * 2) // [2, 4, 6]

2. Array.of()

Array.of() 可以将一系列值转换成数组,引入这个是为了解决new Array()构造器使用单个参数或多个参数返回值混乱的问题。

1
2
3
new Array(2) // 表示创建一个长度为2的数组

new Array(1, 2) // 表示创建一个元素为1 2的数组

而 Array.of() 无论传入一个参数还是多个参数都会当作数组的元素处理。

3. 数组实例的 find() 和 findIndex()

Array.prototype.find() 找出第一个符合条件的数组成员,返回符合条件的值。

Array.prototype.findIndex() 找出第一个符合条件的数组成员的位置,返回符合条件的值的index,如果都不符合返回-1

1
[1, 4, -5, 10].find(n => n < 5) // -5

4. (ES7)数组实例的 includes()

为了解决indexOf()的两个缺点,一是不够语义化,二是它内部严格相等运算符进行判断会导致对NaN的误判。

1
2
[NaN].indexOf(NaN) // -1
[NaN].includes(NaN) // true

5. 数组实例的 entries(), keys() 和 values()

entries() 是对键值对的遍历,keys()是对键名的遍历,values() 是对键值的遍历。返回的都是一个迭代器对象,使用for…of循环进行处理。

6. reduce()

1
arr.reduce(callback,[initialValue])

(1)没有 initialValue 参数时

prev: 上一次调用回调返回的值,或者是提供的初始值(initialValue)

currentValue: 数组中当前被处理的元素

index: 当前元素在数组中的索引

array: 调用reduce的数组

1
2
3
4
5
6
7
let arr = [1, 2, 3, 4]
let sum = arr.reduce((prev, cur, index, arr) => {
console.log(prev, cur, index);
return prev + cur;
})

console.log(arr, sum); // [1,2,3,4] 10

(2)initialValue

1
2
3
4
5
6
7
let arr = [1, 2, 3, 4]
let sum = arr.reduce((prev, cur, index, arr) => {
console.log(prev, cur, index);
return prev + cur;
}, 5)

console.log(arr, sum); // [1,2,3,4] 15

得出结论: 如果没有提供 initialValue,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue,从索引0开始。

7. filter()

过滤数组,传入一个callback,返回一个数组。

1
2
3
let arr = [1, 2, 3, 4, 5]
arr.filter(item => item > 2)
// 结果:[3, 4, 5]

8. fill()

1
array.fill(value, start, end)
1
2
3
4
5
6
7
8
9
10
11
const arr = [0, 0, 0, 0, 0];

// 用5填充整个数组
arr.fill(5);
console.log(arr); // [5, 5, 5, 5, 5]
arr.fill(0); // 重置

// 用5填充索引大于等于3的元素
arr.fill(5, 3);
console.log(arr); // [0, 0, 0, 5, 5]
arr.fill(0); // 重置

四、扩展运算符

五、解构赋值

1. 嵌套对象解构

1
2
3
4
let node = {
loc: { start: {} }
};
let { loc: { start } } = node;

2. 数组解构

具有 Iterator 接口的数据结构,都可以采用数组形式的解构赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const names = ['Hnery', 'Allen'];
const [name1, name2] = names;

const [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo, bar, baz) // 输出结果:1 2 3

const [x, y] = [1, 2, 3]; // 提取前两个值
const [, y, z] = [1, 2, 3] // 提取后两个值
const [x, , z] = [1, 2, 3] // 提取第一三个值

// 对应的位置没有值就会将变量赋值为undefined
const [x, y, z] = [1, 2];
console.log(z) // 输出结果:undefined

// 使用rest操作符来捕获剩余项
const [x, ...y] = [1, 2, 3];
console.log(x); // 输出结果:1
console.log(y); // 输出结果:[2, 3]

// 支持默认值的解构
const [x, y, z = 3] = [1, 2];
console.log(z) // 输出结果:3

3. 其他解构赋值

(1)字符串解构

1
2
3
4
const [a, b, c, d, e] = 'hello';
console.log(a, b, c, d, e) // 输出结果:h e l l o

let {length} = 'hello'; // 输出结果:5

(2)数值和布尔值解构赋值

1
2
3
4
5
let {toString: s} = 123;
s === Number.prototype.toString // 输出结果:true

let {toString: s} = true;
s === Boolean.prototype.toString // 输出结果:true

(3)函数参数或返回值解构赋值

1
2
3
4
5
6
7
8
9
function add([x, y]){
return x + y;
}
add([1, 2]); // 3

function example() {
return [1, 2, 3];
}
let [a, b, c] = example();

4. 混合解构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const people = [
{name:"Henry",age:20},
{name:"Bucky",age:25},
{name:"Emily",age:30}
];

// es5 写法
var age = people[0].age;
console.log(age);

// es6 解构
const [age] = people;
console.log(age);// 第一次解构数组 {name:"Henry",age:20}
const [{age}] = people;// 再一次解构对象
console.log(age);//20

六、模板字符串(反引号标识)

如果在字符串中使用反引号,需要使用\来转义;

如果在多行字符串中有空格和缩进,那么它们都会被保留在输出中;

七、Class

从概念上讲,在 ES6 之前的 JS 中并没有和其他面向对象语言那样的“类”的概念。长时间里,人们把使用 new 关键字通过函数(也叫构造器)构造对象当做“类”来使用。由于 JS 不支持原生的类,而只是通过原型来模拟,各种模拟类的方式相对于传统的面向对象方式来说非常混乱,尤其是处理当子类继承父类、子类要调用父类的方法等等需求时。

ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。但是类只是基于原型的面向对象模式的语法糖。

1. 对比在传统构造函数和 ES6 中分别如何实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 传统构造函数
function MathHandle(x,y){
this.x=x;
this.y=y;
}
MathHandle.prototype.add =function(){
return this.x+this.y;
};
var m = new MathHandle(1,2);
console.log(m.add())


// class语法
class MathHandle {
constructor(x,y) {
this.x=x;
this.y=y;
}
add() {
return this.x+this.y;
}
}

const m=new MathHandle(1,2);
console.log(m.add())

这两者有什么联系?其实这两者本质是一样的,只不过是语法糖写法上有区别。所谓语法糖是指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。比如这里class语法糖让程序更加简洁,有更高的可读性。

2. 对比在传统构造函数和 ES6 中分别如何实现继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 传统构造函数继承
function Animal() {
this.eat = function () {
alert('Animal eat')
}
}

function Dog() {
this.bark = function () {
alert('Dog bark')
}
}

Dog.prototype = new Animal() // 绑定原型,实现继承
var hashiqi = new Dog()
hashiqi.bark() // Dog bark
hashiqi.eat() // Animal eat


// ES6继承
class Animal {
constructor(name) {
this.name = name
}
eat() {
alert(this.name + ' eat')
}
}

class Dog extends Animal {
constructor(name) {
super(name) // 有extend就必须要有super,它代表父类的构造函数,即Animal中的constructor
this.name = name
}
say() {
alert(this.name + ' say')
}
}
const dog = new Dog('哈士奇')
dog.say()//哈士奇 say
dog.eat()//哈士奇 eat

Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。

3. Class 和传统构造函数有何区别:

  • Class 在语法上更加贴合面向对象的写法
  • Class 实现继承更加易读、易理解,对初学者更加友好
  • 本质还是语法糖,使用prototype

八、Promise

1. Promise引入的原因

在JavaScript的世界中,所有代码都是单线程执行的。由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大。

ES6中的promise的出现给我们很好的解决了回调地狱的问题。

2. Promise原理

Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。promise 对象初始化状态为 pending ;当调用resolve(成功),会由pending => fulfilled ;当调用reject(失败),会由pending => rejected。

3. Promise的使用流程

(1)new Promise一个实例,而且要 return

(2)new Promise 时要传入函数,函数有resolve reject 两个参数

(3)成功时执行 resolve,失败时执行reject

(4)then 监听结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function loadImg(src){
const promise=new Promise(function(resolve,reject){
var img=document.createElement('img')
img.onload=function() {
resolve(img)
}
img.onerror=function(){
reject()
}
img.src=src
})
return promise//返回一个promise实例
}

var src="http://www.imooc.com/static/img/index/logo_new.png"
var result=loadImg(src)
result.then(function(img) {
console.log(img.width) //resolved(成功)时候的回调函数
},function() {
console.log("failed") //rejected(失败)时候的回调函数
})
result.then(function(img) {
console.log(img.height)
})

(详细讲解Promise见:深入理解JavaScript异步二、Promise)

九、Iterator 和 for…of 循环

JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就需要一种统一的接口机制,来处理所有不同的数据结构。遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

(1)Iterator的作用

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列
  • ES6创造了一种新的遍历命令for…of循环,Iterator接口主要供for…of消费。

(2)原生具备Iterator接口的数据(可用for of 遍历)

  • Array
  • set容器
  • map容器
  • String
  • 函数的 arguments 对象
  • NodeList 对象

(3)几种遍历方式的比较

  • for of 循环不仅支持数组、大多数伪数组对象,也支持字符串遍历,此外还支持 Map 和 Set 对象遍历。(不能遍历对象)
  • for in 循环可以遍历字符串、对象、数组,不能遍历Set/Map。(主要用来遍历对象)
  • forEach 循环不能遍历字符串、对象, 可以遍历Set/Map

十、ES6模块化

import和export旨在成为浏览器和服务器通用的模块解决方案。

十一、函数默认参数

ES6之前,函数不支持默认参数。ES6实现了对此的支持,并且只有不传入参数时才会触发默认值。

函数length属性通常用来表示函数参数的个数。当引入函数默认值后,length表示的就是第一个有默认值参数之前的普通参数个数。

1
2
3
4
5
6
7
8
9
10
const funcA = function(x, y) {};
console.log(funcA.length); // 输出结果:2


const funcB = function(x, y = 1) {};
console.log(funcB.length); // 输出结果:1


const funcC = function(x = 1, y) {};
console.log(funcC.length); // 输出结果 0

十二、箭头函数

1. 箭头函数没有自己的this

箭头函数不会创建自己自己的this,所以它没有自己的this,它只会在自己作用域的上一次继承this。所以箭头函数中this的指向在它定义时已经确定了,之后不会改变。这就解决了function()中this需要在调用时才会被确定的问题。

1
2
3
4
5
6
7
8
const funcA = function(x, y) {};
console.log(funcA.length); // 输出结果:2

const funcB = function(x, y = 1) {};
console.log(funcB.length); // 输出结果:1

const funcC = function(x = 1, y) {};
console.log(funcC.length); // 输出结果 0

对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。

同样,使用call()、apply()、bind()等方法也不能改变箭头函数中this的指向。

2. 不可作为构造函数

构造函数 new 操作符的执行步骤如下:

  • 创建一个对象
  • 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
  • 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
  • 返回新的对象

实际上第二步就是将函数中的this指向该对象。但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。

3. 不绑定arguments

箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。

十三、扩展运算符

扩展运算符…就像是rest参数的逆运算,将一个数组转为用逗号分割的参数序列,对数组进行解包。(ES9给对象也引入了扩展运算符)

1. 将数组转化为用逗号分隔的参数序列

1
2
3
4
5
6
7
8
9
function  test(a,b,c){
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
}


var arr = [1, 2, 3];
test(...arr);

2. 拼接数组

1
2
3
var arr1 = [1, 2, 3,4];
var arr2 = [...arr1, 4, 5, 6];
console.log(arr2); // [1, 2, 3, 4, 4, 5, 6]

3. 将字符串转为逗号分隔的数组

1
2
3
var str='JavaScript';
var arr= [...str];
console.log(arr); // ["J", "a", "v", "a", "S", "c", "r", "i", "p", "t"]

十四、字符串方法

1. includes()

2. startsWith()

1
2
3
4
5
let str = 'Hello world!';

str.startsWith('Hello') // 输出结果:true
str.startsWith('Helle') // 输出结果:false
str.startsWith('wo', 6) // 输出结果:true,索引为6的位置以wo开头

3. endsWith()

1
2
3
4
5
let str = 'Hello world!';

str.endsWith('!') // 输出结果:true
str.endsWith('llo') // 输出结果:false
str.endsWith('llo', 5) // 输出结果:true, 前5个字符以llo结尾

4. repeat()

1
2
3
'x'.repeat(3)     // 输出结果:"xxx"
'hello'.repeat(2) // 输出结果:"hellohello"
'na'.repeat(0) // 输出结果:""

如果参数是小数,会向下取整;

如果参数是负数或Infinity会报错;

如果参数是0到-1之间的小数等同于0;

如果参数是NaN等同于0;

如果参数是字符串,会先转换成数字;

参考:

https://github.com/ljianshu/Blog/issues/10

https://mp.weixin.qq.com/s/KsoSwA73PzGwYMqZOwUvNQ