您所在的位置:首页 / 知识分享

从ES6到ES10的新特性万字大总结(不得不收藏)

2019.12.19

1392

fish

ECMAScript是一种由Ecma国际(前身为欧洲计算机制造商协会)在标准ECMA-262中定义的脚本语言规范。这种语言在万维网上应用广泛,它往往被称为JavaScript或JScript,但实际上后两者是ECMA-262标准的实现和扩展。

至发稿日为止有九个ECMA-262版本发表。其历史版本如下:

  1. 1997年6月:第一版
  2. 1998年6月:修改格式,使其与ISO/IEC16262国际标准一样
  3. 1999年12月:强大的正则表达式,更好的词法作用域链处理,新的控制指令,异常处理,错误定义更加明确,数据输出的格式化及其它改变
  4. 2009年12月:添加严格模式("use strict")。修改了前面版本模糊不清的概念。增加了getters,setters,JSON以及在对象属性上更完整的反射。
  5. 2011年6月:ECMAScript标5.1版形式上完全一致于国际标准ISO/IEC 16262:2011。
  6. 2015年6月:ECMAScript 2015(ES2015),第 6 版,最早被称作是 ECMAScript 6(ES6),添加了类和模块的语法,其他特性包括迭代器,Python风格的生成器和生成器表达式,箭头函数,二进制数据,静态类型数组,集合(maps,sets 和 weak maps),promise,reflection 和 proxies。作为最早的 ECMAScript Harmony 版本,也被叫做ES6 Harmony。
  7. 2016年6月:ECMAScript 2016(ES2016),第 7 版,多个新的概念和语言特性。
  8. 2017年6月:ECMAScript 2017(ES2017),第 8 版,多个新的概念和语言特性。
  9. 2018年6月:ECMAScript 2018 (ES2018),第 9 版,包含了异步循环,生成器,新的正则表达式特性和 rest/spread 语法。
  10. 2019年6月:ECMAScript 2019 (ES2019),第 10 版。

发展标准

TC39(Technical Committee 39)是一个推动JavaScript发展的委员会,它的成语来自各个主流浏览器的代表成语。会议实行多数决,每一项决策只有大部分人同意且没有强烈反对才能去实现。

TC39成员制定着ECMAScript的未来。

每一项新特性最终要进入到ECMAScript规范里,需要经历5个阶段,这5个阶段如下:

  • Stage 0: Strawperson

    只要是TC39成员或者贡献者,都可以提交想法

  • Stage 1: Proposal

    这个阶段确定一个正式的提案

  • Stage 2: draft

    规范的第一个版本,进入此阶段的提案大概率会成为标准

  • Stage 3: Candidate

    进一步完善提案细则

  • Stage 4: Finished

    表示已准备好将其添加到正式的ECMAScript标准中

由于ES6以前的属性诞生年底久远,我们使用也比较普遍,遂不进行说明,ES6之后的语言风格跟ES5以前的差异比较大,所以单独拎出来做个记录。

ES6(ES2015)

ES6是一次重大的革新,比起过去的版本,改动比较大,本文仅对常用的API以及语法糖进行讲解。

Let 和 Const

在ES6以前,JS只有var一种声明方式,但是在ES6之后,就多了let跟const这两种方式。用var定义的变量没有块级作用域的概念,而let跟const则会有,因为这三个关键字创建是不一样的。

区别如下:

{ var a = 10 let b = 20 const c = 30 }
a // 10 b // Uncaught ReferenceError: b is not defined c // c is not defined let d = 40 const e = 50 d = 60 d // 60 e = 70 // VM231:1 Uncaught TypeError: Assignment to constant variable. 复制代码
var let const
变量提升 × ×
全局变量 × ×
重复声明 × ×
重新赋值 ×
暂时死区 ×
块作用域 ×
只声明不初始化 ×

类(Class)

在ES6之前,如果我们要生成一个实例对象,传统的方法就是写一个构造函数,例子如下:

function Person(name, age) { this.name = name this.age = age
}
Person.prototype.information = function () { return 'My name is ' + this.name + ', I am ' + this.age
} 复制代码

但是在ES6之后,我们只需要写成以下形式:

class Person { constructor(name, age) { this.name = name this.age = age
    }
    information() { return 'My name is ' + this.name + ', I am ' + this.age
    }
} 复制代码

箭头函数(Arrow function)

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。这些函数表达式更适用于那些本来需要匿名函数的地方,并且它们不能用作构造函数。

在ES6以前,我们写函数一般是:

var list = [1, 2, 3, 4, 5, 6, 7] var newList = list.map(function (item) { return item * item
}) 复制代码

但是在ES6里,我们可以:

const list = [1, 2, 3, 4, 5, 6, 7] const newList = list.map(item => item * item) 复制代码

看,是不是简洁了不少

函数参数默认值(Function parameter defaults)

在ES6之前,如果我们写函数需要定义初始值的时候,需要这么写:

function config (data) { var data = data || 'data is empty' } 复制代码

这样看起来也没有问题,但是如果参数的布尔值为falsy时就会出问题,例如我们这样调用config:

config(0)
config('') 复制代码

那么结果就永远是后面的值

如果我们用函数参数默认值就没有这个问题,写法如下:

const config = (data = 'data is empty') => {} 复制代码

模板字符串(Template string)

在ES6之前,如果我们要拼接字符串,则需要像这样:

var name = 'kris' var age = 24 var info = 'My name is ' + this.name + ', I am ' + this.age 复制代码

但是在ES6之后,我们只需要写成以下形式:

const name = 'kris' const age = 24 const info = `My name is ${name}, I am ${age}` 复制代码

解构赋值(Destructuring assignment)

我们通过解构赋值, 可以将属性/值从对象/数组中取出,赋值给其他变量。

比如我们需要交换两个变量的值,在ES6之前我们可能需要:

var a = 10 var b = 20 var temp = a
a = b
b = temp 复制代码

但是在ES6里,我们有:

let a = 10 let b = 20 [a, b] = [b, a] 复制代码

是不是方便很多

模块化(Module)

在ES6之前,JS并没有模块化的概念,有的也只是社区定制的类似CommonJS和AMD之类的规则。例如基于CommonJS的NodeJS:

// circle.js // 输出 const { PI } = Math exports.area = (r) => PI * r ** 2 exports.circumference = (r) => 2 * PI * r // index.js // 输入 const circle = require('./circle.js') console.log(`半径为 4 的圆的面积是 ${circle.area(4)}`) 复制代码

在ES6之后我们则可以写成以下形式:

// circle.js // 输出 const { PI } = Math export const area = (r) => PI * r ** 2 export const circumference = (r) => 2 * PI * r // index.js // 输入 import {
    area
} = './circle.js' console.log(`半径为 4 的圆的面积是: ${area(4)}`) 复制代码

扩展操作符(Spread operator)

扩展操作符可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造字面量对象时, 将对象表达式按key-value的方式展开。

比如在ES5的时候,我们要对一个数组的元素进行相加,在不使用reduce或者reduceRight的场合,我们需要:

function sum(x, y, z) { return x + y + z;
} var list = [5, 6, 7] var total = sum.apply(null, list) 复制代码

但是如果我们使用扩展操作符,只需要如下:

const sum = (x, y, z) => x + y + z const list = [5, 6, 7] const total = sum(...list) 复制代码

非常的简单,但是要注意的是扩展操作符只能用于可迭代对象

如果是下面的情况,是会报错的:

var obj = {'key1': 'value1'} var array = [...obj] // TypeError: obj is not iterable 复制代码

对象属性简写(Object attribute shorthand)

在ES6之前,如果我们要将某个变量赋值为同样名称的对象元素,则需要:

var cat = 'Miaow' var dog = 'Woof' var bird = 'Peet peet' var someObject = { cat: cat, dog: dog, bird: bird
} 复制代码

但是在ES6里我们就方便很多:

let cat = 'Miaow' let dog = 'Woof' let bird = 'Peet peet' let someObject = {
  cat,
  dog,
  bird
} console.log(someObject) //{ //  cat: "Miaow", //  dog: "Woof", //  bird: "Peet peet" //} 复制代码

非常方便

Promise

Promise 是ES6提供的一种异步解决方案,比回调函数更加清晰明了。

Promise翻译过来就是承诺的意思,这个承诺会在未来有一个确切的答复,并且该承诺有三种状态,分别是:

  1. 等待中(pending)
  2. 完成了 (resolved)
  3. 拒绝了(rejected)

这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再次改变

new Promise((resolve, reject) => {
  resolve('success') // 无效 reject('reject')
}) 复制代码

当我们在构造Promise的时候,构造函数内部的代码是立即执行的

new Promise((resolve, reject) => { console.log('new Promise')
  resolve('success')
}) console.log('finifsh') // new Promise -> finifsh 复制代码

Promise实现了链式调用,也就是说每次调用then之后返回的都是一个Promise,并且是一个全新的Promise,原因也是因为状态不可变。如果你在then中 使用了return,那么return的值会被Promise.resolve()包装

Promise.resolve(1)
  .then(res => { console.log(res) // => 1 return 2 // 包装成 Promise.resolve(2) })
  .then(res => { console.log(res) // => 2 }) 复制代码

当然了,Promise也很好地解决了回调地狱的问题,例如:

ajax(url, () => { // 处理逻辑 ajax(url1, () => { // 处理逻辑 ajax(url2, () => { // 处理逻辑 })
    })
}) 复制代码

可以改写成:

ajax(url)
  .then(res => { console.log(res) return ajax(url1)
  }).then(res => { console.log(res) return ajax(url2)
  }).then(res => console.log(res)) 复制代码

for...of

for...of语句在可迭代对象(包括Array,Map,Set,String,TypedArray,arguments对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

例子如下:

const array1 = ['a', 'b', 'c']; for (const element of array1) { console.log(element)
} // "a" // "b" // "c" 复制代码

Symbol

symbol 是一种基本数据类型,Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。

每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。

例子如下:

const symbol1 = Symbol(); const symbol2 = Symbol(42); const symbol3 = Symbol('foo'); console.log(typeof symbol1); // "symbol" console.log(symbol3.toString()); // "Symbol(foo)" console.log(Symbol('foo') === Symbol('foo')); // false 复制代码

迭代器(Iterator)/ 生成器(Generator)

迭代器(Iterator)是一种迭代的机制,为各种不同的数据结构提供统一的访问机制。任何数据结构只要内部有 Iterator 接口,就可以完成依次迭代操作。

一旦创建,迭代器对象可以通过重复调用next()显式地迭代,从而获取该对象每一级的值,直到迭代完,返回{ value: undefined, done: true }

虽然自定义的迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此需要谨慎地创建。生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。 生成器函数使用 function*语法编写。 最初调用时,生成器函数不执行任何代码,而是返回一种称为Generator的迭代器。 通过调用生成器的下一个方法消耗值时,Generator函数将执行,直到遇到yield关键字。

可以根据需要多次调用该函数,并且每次都返回一个新的Generator,但每个Generator只能迭代一次。

所以我们可以有以下例子:

function* makeRangeIterator(start = 0, end = Infinity, step = 1) { for (let i = start; i < end; i += step) { yield i;
    }
} var a = makeRangeIterator(1,10,2)
a.next() // {value: 1, done: false} a.next() // {value: 3, done: false} a.next() // {value: 5, done: false} a.next() // {value: 7, done: false} a.next() // {value: 9, done: false} a.next() // {value: undefined, done: true} 复制代码

Set/WeakSet

Set对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。

所以我们可以通过Set实现数组去重

const numbers = [2,3,4,4,2,3,3,4,4,5,5,6,6,7,5,32,3,4,5] console.log([...new Set(numbers)]) // [2, 3, 4, 5, 6, 7, 32] 复制代码

WeakSet结构与Set类似,但区别有以下两点:

  • WeakSet对象中只能存放对象引用, 不能存放值, 而Set对象都可以。
  • WeakSet对象中存储的对象值都是被弱引用的, 如果没有其他的变量或属性引用这个对象值, 则这个对象值会被当成垃圾回收掉. 正因为这样,WeakSet对象是无法被枚举的, 没有办法拿到它包含的所有元素。

所以代码如下:

var ws = new WeakSet() var obj = {} var foo = {}

ws.add(window)
ws.add(obj)

ws.has(window) // true ws.has(foo) // false, 对象 foo 并没有被添加进 ws 中  ws.delete(window) // 从集合中删除 window 对象 ws.has(window) // false, window 对象已经被删除了 ws.clear() // 清空整个 WeakSet 对象 复制代码

Map/WeakMap

Map对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。

例子如下,我们甚至可以使用NaN来作为键值:

var myMap = new Map();
myMap.set(NaN, "not a number");

myMap.get(NaN); // "not a number" var otherNaN = Number("foo");
myMap.get(otherNaN); // "not a number" 复制代码

WeakMap对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

跟Map的区别与Set跟WeakSet的区别相似,具体代码如下:

var wm1 = new WeakMap(),
    wm2 = new WeakMap(),
    wm3 = new WeakMap(); var o1 = {},
    o2 = function(){},
    o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // value可以是任意值,包括一个对象 wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 键和值可以是任意对象,甚至另外一个WeakMap对象 wm1.get(o2); // "azerty" wm2.get(o2); // undefined,wm2中没有o2这个键 wm2.get(o3); // undefined,值就是undefined wm1.has(o2); // true wm2.has(o2); // false wm2.has(o3); // true (即使值是undefined) wm3.set(o1, 37);
wm3.get(o1); // 37 wm3.clear();
wm3.get(o1); // undefined,wm3已被清空 wm1.has(o1); // true wm1.delete(o1);
wm1.has(o1); // false 复制代码

Proxy/Reflect

Proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

Reflect是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与Proxy的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

Proxy跟Reflect是非常完美的配合,例子如下:

const observe = (data, callback) => { return new Proxy(data, {
            get(target, key) { return Reflect.get(target, key)
            },
            set(target, key, value, proxy) {
                  callback(key, value);
                  target[key] = value; return Reflect.set(target, key, value, proxy)
            }
      })
} const FooBar = { open: false }; const FooBarObserver = observe(FooBar, (property, value) => {
  property === 'open' && value 
          ? console.log('FooBar is open!!!') 
          : console.log('keep waiting');
}); console.log(FooBarObserver.open) // false FooBarObserver.open = true // FooBar is open!!! 复制代码

当然也不是什么都可以被代理的,如果对象带有configurable: false跟writable: false属性,则代理失效。

Regex对象的扩展

正则新增符号

  • i修饰符

    // i 修饰符 /[a-z]/i.test('\u212A') // false /[a-z]/iu.test('\u212A') // true 复制代码
  • y修饰符

    // y修饰符 var s = 'aaa_aa_a'; var r1 = /a+/g; var r2 = /a+/y;
    
    r1.exec(s) // ["aaa"] r2.exec(s) // ["aaa"] r1.exec(s) // ["aa"] r2.exec(s) // null 复制代码
  • String.prototype.flags

    // 查看RegExp构造函数的修饰符 var regex = new RegExp('xyz', 'i')
    regex.flags // 'i' 复制代码
  • unicode模式

    var s = '