自 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 | // 暂时性死区 开始 |
所以,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 | new Array(2) // 表示创建一个长度为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 | [NaN].indexOf(NaN) // -1 |
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 | let arr = [1, 2, 3, 4] |
(2)initialValue
1 | let arr = [1, 2, 3, 4] |
得出结论: 如果没有提供 initialValue,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue,从索引0开始。
7. filter()
过滤数组,传入一个callback,返回一个数组。
1 | let arr = [1, 2, 3, 4, 5] |
8. fill()
1 | array.fill(value, start, end) |
1 | const arr = [0, 0, 0, 0, 0]; |
四、扩展运算符…
五、解构赋值
1. 嵌套对象解构
1 | let node = { |
2. 数组解构
具有 Iterator 接口的数据结构,都可以采用数组形式的解构赋值。
1 | const names = ['Hnery', 'Allen']; |
3. 其他解构赋值
(1)字符串解构
1 | const [a, b, c, d, e] = 'hello'; |
(2)数值和布尔值解构赋值
1 | let {toString: s} = 123; |
(3)函数参数或返回值解构赋值
1 | function add([x, y]){ |
4. 混合解构
1 | const people = [ |
六、模板字符串(反引号标识)
如果在字符串中使用反引号,需要使用\来转义;
如果在多行字符串中有空格和缩进,那么它们都会被保留在输出中;
七、Class
从概念上讲,在 ES6 之前的 JS 中并没有和其他面向对象语言那样的“类”的概念。长时间里,人们把使用 new 关键字通过函数(也叫构造器)构造对象当做“类”来使用。由于 JS 不支持原生的类,而只是通过原型来模拟,各种模拟类的方式相对于传统的面向对象方式来说非常混乱,尤其是处理当子类继承父类、子类要调用父类的方法等等需求时。
ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。但是类只是基于原型的面向对象模式的语法糖。
1. 对比在传统构造函数和 ES6 中分别如何实现类:
1 | // 传统构造函数 |
这两者有什么联系?其实这两者本质是一样的,只不过是语法糖写法上有区别。所谓语法糖是指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。比如这里class语法糖让程序更加简洁,有更高的可读性。
2. 对比在传统构造函数和 ES6 中分别如何实现继承:
1 | // 传统构造函数继承 |
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 | function loadImg(src){ |
(详细讲解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 | const funcA = function(x, y) {}; |
十二、箭头函数
1. 箭头函数没有自己的this
箭头函数不会创建自己自己的this,所以它没有自己的this,它只会在自己作用域的上一次继承this。所以箭头函数中this的指向在它定义时已经确定了,之后不会改变。这就解决了function()中this需要在调用时才会被确定的问题。
1 | const funcA = function(x, y) {}; |
对象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 | function test(a,b,c){ |
2. 拼接数组
1 | var arr1 = [1, 2, 3,4]; |
3. 将字符串转为逗号分隔的数组
1 | var str='JavaScript'; |
十四、字符串方法
1. includes()
2. startsWith()
1 | let str = 'Hello world!'; |
3. endsWith()
1 | let str = 'Hello world!'; |
4. repeat()
1 | 'x'.repeat(3) // 输出结果:"xxx" |
如果参数是小数,会向下取整;
如果参数是负数或Infinity会报错;
如果参数是0到-1之间的小数等同于0;
如果参数是NaN等同于0;
如果参数是字符串,会先转换成数字;
参考: