自 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 将其称之为暂时性死区:
// 暂时性死区 开始
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 方法。
const array3 = Array.from(array, (num) => num * 2) // [2, 4, 6]
2. Array.of()
Array.of() 可以将一系列值转换成数组,引入这个是为了解决new Array()构造器使用单个参数或多个参数返回值混乱的问题。
new Array(2) // 表示创建一个长度为2的数组
new Array(1, 2) // 表示创建一个元素为1 2的数组
而 Array.of() 无论传入一个参数还是多个参数都会当作数组的元素处理。
3. 数组实例的 find() 和 findIndex()
Array.prototype.find() 找出第一个符合条件的数组成员,返回符合条件的值。
Array.prototype.findIndex() 找出第一个符合条件的数组成员的位置,返回符合条件的值的index,如果都不符合返回-1
[1, 4, -5, 10].find(n => n < 5) // -5
4. (ES7)数组实例的 includes()
为了解决indexOf()的两个缺点,一是不够语义化,二是它内部严格相等运算符进行判断会导致对NaN的误判。
[NaN].indexOf(NaN) // -1
[NaN].includes(NaN) // true
5. 数组实例的 entries(), keys() 和 values()
entries() 是对键值对的遍历,keys()是对键名的遍历,values() 是对键值的遍历。返回的都是一个迭代器对象,使用for…of循环进行处理。
6. reduce()
arr.reduce(callback,[initialValue])
(1)没有 initialValue 参数时
prev: 上一次调用回调返回的值,或者是提供的初始值(initialValue)
currentValue: 数组中当前被处理的元素
index: 当前元素在数组中的索引
array: 调用reduce的数组
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
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,返回一个数组。
let arr = [1, 2, 3, 4, 5]
arr.filter(item => item > 2)
// 结果:[3, 4, 5]
8. fill()
array.fill(value, start, end)
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. 嵌套对象解构
let node = {
loc: { start: {} }
};
let { loc: { start } } = node;
2. 数组解构
具有 Iterator 接口的数据结构,都可以采用数组形式的解构赋值。
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)字符串解构
const [a, b, c, d, e] = 'hello';
console.log(a, b, c, d, e) // 输出结果:h e l l o
let {length} = 'hello'; // 输出结果:5
(2)数值和布尔值解构赋值
let {toString: s} = 123;
s === Number.prototype.toString // 输出结果:true
let {toString: s} = true;
s === Boolean.prototype.toString // 输出结果:true
(3)函数参数或返回值解构赋值
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
4. 混合解构
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 中分别如何实现类:
// 传统构造函数
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 中分别如何实现继承:
// 传统构造函数继承
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 监听结果
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表示的就是第一个有默认值参数之前的普通参数个数。
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需要在调用时才会被确定的问题。
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. 将数组转化为用逗号分隔的参数序列
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. 拼接数组
var arr1 = [1, 2, 3,4];
var arr2 = [...arr1, 4, 5, 6];
console.log(arr2); // [1, 2, 3, 4, 4, 5, 6]
3. 将字符串转为逗号分隔的数组
var str='JavaScript';
var arr= [...str];
console.log(arr); // ["J", "a", "v", "a", "S", "c", "r", "i", "p", "t"]
十四、字符串方法
1. includes()
2. startsWith()
let str = 'Hello world!';
str.startsWith('Hello') // 输出结果:true
str.startsWith('Helle') // 输出结果:false
str.startsWith('wo', 6) // 输出结果:true,索引为6的位置以wo开头
3. endsWith()
let str = 'Hello world!';
str.endsWith('!') // 输出结果:true
str.endsWith('llo') // 输出结果:false
str.endsWith('llo', 5) // 输出结果:true, 前5个字符以llo结尾
4. repeat()
'x'.repeat(3) // 输出结果:"xxx"
'hello'.repeat(2) // 输出结果:"hellohello"
'na'.repeat(0) // 输出结果:""
如果参数是小数,会向下取整;
如果参数是负数或Infinity会报错;
如果参数是0到-1之间的小数等同于0;
如果参数是NaN等同于0;
如果参数是字符串,会先转换成数字;
参考: