回顾一下 ECMAScript 的发展历程:

  • 1997 年 6 月,ES1 发布
  • 1998 年 6 月,ES2 发布
  • 1999 年 12 月,ES3 发布
  • 然后,ES4 胎死腹中
  • 2009 年 12 月,ES5 发布
  • 2015 年 6 月,ES6(ES2015)发布
  • ES2016
  • ES2017
  • ES2018
  • ……

经过 ES3 到 ES5 10 年的停滞以后,从 ES6 (ES2015) 开始,又开始小跑向前,连版本命名都改为年份的方式。跑那么快,作为从业者,有点担心扯着蛋。

下面就来了解一下 ES6 之后,又新增那些东西(参见TC39 提案列表)。

ECMAScript 2016

Array.prototype.includes()

includes 用于判断某个元素是否存在于数组中(包括 NaN)。

const arr = [1, 2, 3, NaN];

arr.includes(3); // true
arr.includes(NaN); // true

arr.indexOf(NaN); // -1

注意indexOf() 查找 NaN 时始终返回 -1includes() 则不同。如果基于 NaN === NaNfalseindexOf() 的结果似乎更合理。不知 includes() 为何没有遵循这个逻辑。

番外:此 API 原本想命名为 contains 的,但 Mootools 已经在数组上扩展了 contains 方法,为了避免对历史代码产生影响,改用 includes

所以,不建议在 ES 内置对象(包括其原型)上定义属性、方法,除非做 polyfill,在老浏览器实现已经成为标准的功能。

乘方中缀操作符

ES 中已经有 ++-- 操作符,ES2016 引入了 ** 操作符进行乘方操作,作为 Math.pow 的一种替代。

Math.pow(7, 2); // 49

7 ** 2 // 49

ECMAScript 2017

Object.values()

Object.keys() 返回一个对象的 key 数组,Object.values() 则返回值的数组。

const cars = { BENZ: 3, Tesla:2, Honda: 1 };

// ES2015
const carVals = Object.keys(cars).map(key => cars[key]);
// [3, 2, 1]

// ES2017
const vals = Object.values(cars);

Object.entries()

以二维数组的形式返回对象的键值对。

const cars = { BENZ: 3, Tesla:2, Honda: 1 };

// ES 5.1
Object.keys(cars).forEach(key => {
  console.log(`key: ${key} value: ${cars[key]}`);
});

// ES2017
for (let [key, value] of Object.entries(cars)) {
  console.log(`key: ${key} value: ${value}`);
}

// ES2015
const map1 = new Map();
Object.keys(cars).forEach(key => {
  map1.set(key, cars[key]);
});

// ES2017
const map = new Map(Object.entries(cars));

字符填充

— String.prototype.padStart(numberOfCharcters [,stringForPadding]);

  • String.prototype.padEnd(numberOfCharcters [,stringForPadding]);

前置/后置填充,第一个参数为字符串的目标长度,第二个参数为填充的字符,默认为空格。如果目标长度小于字符串的长度,则不做处理,直接返回原始字符串。

字符串填充一般用于界面中对齐文字,美化展示效果。

let uid = '12345';
const len = 8;

// 这个判断其实没有必要
if (uid.length < len) {
  uid = uid.padStart(len, '0');
}

'5'.padStart(10); // '          5'
'5'.padStart(10, '=*'); //'=*=*=*=*=5'

'5'.padEnd(10); // '5         '
'5'.padEnd(10, '=*'); // '5=*=*=*=*='
const cars = {
  '🚙BMW': '10',
  '🚘Tesla': '5',
  '🚖Lamborghini': '0'
}

Object.entries(cars).map(([name, count]) => {
  //padEnd appends ' -' until the name becomes 20 characters
  //padStart prepends '0' until the count becomes 3 characters.
  console.log(`${name.padEnd(20, ' -')} Count: ${count.padStart(3, '0')}`)
});

//Prints..
// 🚙BMW - - - - - - -  Count: 010
// 🚘Tesla - - - - - -  Count: 005
// 🚖Lamborghini - - -  Count: 000

Emoji 等双字节字符填充

Emoji 等使用多字节 unicode 呈现,padStartpadEnd 可能无法得到预期的结果。

看下面的例子:

'heart'.padStart(10, '❤️'); // prints.. '❤️❤️❤heart'

上面的代码执行结果是 2 个 ❤️ 和 1 个 ❤,而不是五个 ️❤️。这是因为 ❤️ 码点长度为 2 ('\u2764\uFE0F'),heart 的长度为 5,总共只能填充 5 个字符,也就是 '\u2764\uFE0F\u2764\uFE0F\u2764'

附:unicode 在线转换

Object.getOwnPropertyDescriptors

返回对象所有属性的详细信息(包括 gettersetter)。

Object.assign 执行浅拷贝时,不会包含 gettersetterObject.getOwnPropertyDescriptors 的主要目的就是解决这一问题。

var Car = {
  name: 'BMW',
  price: 1000000,
  set discount(x) {
    this.d = x;
  },
  get discount() {
    return this.d;
  },
};
//Print details of Car object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(Car, 'discount'));
//prints..
// { 
//   get: [Function: get],
//   set: [Function: set],
//   enumerable: true,
//   configurable: true
// }
//Copy Car's properties to ElectricCar using Object.assign
const ElectricCar = Object.assign({}, Car);
//Print details of ElectricCar object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(ElectricCar, 'discount'));
//prints..
// { 
//   value: undefined,
//   writable: true,
//   enumerable: true,
//   configurable: true 

// }
//⚠️Notice that getters and setters are missing in ElectricCar object for 'discount' property !👎👎
//Copy Car's properties to ElectricCar2 using Object.defineProperties 
//and extract Car's properties using Object.getOwnPropertyDescriptors
const ElectricCar2 = Object.defineProperties({}, Object.getOwnPropertyDescriptors(Car));
//Print details of ElectricCar2 object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(ElectricCar2, 'discount'));
//prints..
// { get: [Function: get],  👈🏼👈🏼👈🏼
//   set: [Function: set],  👈🏼👈🏼👈🏼
//   enumerable: true,
//   configurable: true 
// }
// Notice that getters and setters are present in the ElectricCar2 object for 'discount' property!

函数参数尾部逗号

函数参数尾部逗号主要是解决 git blame 等工具使用问题。

// 问题

// 开发者 #1 创建了函数
function Person(
  name,
  age //<-- ES2017 以前的版本添加参数尾部逗号会抛出错误
) {
  this.name = name;
  this.age = age;
}

// 开发者 #2 填加了一个参数
function Person(
  name,
  age, // 添加了逗号 <--- 由于这个逗号,开发者 #1 会被 git blame 等工具标记为修改者
  address
) {
  this.name = name;
  this.age = age;
  this.address = address;
}

// ES2017 支持参数尾部逗号,解决这一问题
function Person(
  name,
  age, //<--- 不会报错,开发者 #2 不需要再修改此行
) {
  // ...
}

函数调用时添加尾部逗号也是允许的。

Math.max(10, 20,);

Async/Await

这应该是最重要、最有用的功能了。先是 callback 噩梦,然后有了 Promise 链式写法,一路跌跌撞撞,总算有了更清晰明了的 Async/Await。

async 关键字声明一个异步函数,JS 编译器每次执行到包含 await 的语句时都会暂定,等待 await 后面的语句返回的 Promise resolvereject 后才继续往下执行。

// ES2015 Promise
function getAmount(userId) {
  getUset(userId)
    .then(getBankBabance)
    .then(amount => {
      console.log(amount);
    });
}

// ES2017
async function getAmount7(userId) {
  const user = await getUser(userId);
  const amount = getBankBalance(user);
  
  console.log(amount);
}

getAmount('1'); // $1000
getAmount7('1'); // $1000

function getUser(userId) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('john');
    }, 1000);
  });
}

function getBankBalance(user) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (user === 'john') {
        resolve('$1000');
      } else {
        reject('unknown user');
      }
    }, 1000);
  });
}

Async 函数本身返回 Promise

Async 函数的返回结果需要使用 Promise 的 .then 方法处理。

function getUser(userId) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('john');
    }, 1000);
  });
}

async function getUserName(userId) {
  const name = await getUser(userId);
  // 其他操作
  // ...

  return name;
}

getUserName(userId)
  .then(console.log);

并行调用

可以使用 Promise.all 并行调用 await 后面的语句。

function doubleAfter1Sec(param) {
  return new Promise(resolve => {
    setTimeout(resolve(param * 2), 1000);
  });
}

async function doubleAndAdd(a, b) {
  [a, b] = await Promise.all([doubleAfter1Sec(a), doubleAfter1Sec(b)]);
  
  return a + b;
}

doubleAndAdd(1, 2)
  .then(console.log); // 6

错误处理

方式 1:在函数内部使用 try catch
// Option 1 - Use try catch within the function
async function doubleAndAdd(a, b) {
  try {
    a = await doubleAfter1Sec(a);
    b = await doubleAfter1Sec(b);
  } catch (e) {
    return NaN; // return something
  }
  return a + b;
}

// 🚀Usage:
doubleAndAdd('one', 2).then(console.log); // NaN
doubleAndAdd(1, 2).then(console.log); // 6

function doubleAfter1Sec(param) {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      let val = param * 2;
      isNaN(val) ? reject(NaN) : resolve(val);
    }, 1000);
  });
}
方式 2:捕获每个 await 表达式
//Option 2 - *Catch* errors on  every await line
//as each await expression is a Promise in itself
async function doubleAndAdd(a, b) {
  a = await doubleAfter1Sec(a).catch(e => console.log('"a" is NaN')); // 👈
  b = await doubleAfter1Sec(b).catch(e => console.log('"b" is NaN')); // 👈

  if (!a || !b) {
    return NaN;
  }

  return a + b;
}

// Usage:
doubleAndAdd('one', 2).then(console.log); // NaN  and logs:  "a" is NaN
doubleAndAdd(1, 2).then(console.log); // 6

function doubleAfter1Sec(param) {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      let val = param * 2;
      isNaN(val) ? reject(NaN) : resolve(val);
    }, 1000);
  });
}
方式 3:捕获整个 async-await 函数
// Option 3 - Dont do anything but handle outside the function
// since async / await returns a promise, we can catch the whole function's error
async function doubleAndAdd(a, b) {
  a = await doubleAfter1Sec(a);
  b = await doubleAfter1Sec(b);
  return a + b;
}

// Usage:
doubleAndAdd('one', 2)
  .then(console.log)
  .catch(console.log); // 👈👈 <------- use "catch"

function doubleAfter1Sec(param) {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      let val = param * 2;
      isNaN(val) ? reject(NaN) : resolve(val);
    }, 1000);
  });
}

ESMAScript 2018

ES2018 目前出于最终草案,发布时间为 2018 年 6 月或 7 月。

Shared memory and atomics

这是一个庞大、非常高级的功能,对于 JS 引擎而言是核心增强。

主要思想是 JavaScript 引入某些多线程功能,让开发者可以自行管理内存,取代 JS 引擎内部的内存管理,以编写高性能、并发的程序。

这个功能通过一个新的全局对象 SharedArrayBuffer 实现,其本质上是将数据存储在共享内存空间。所以这些数据可以在 JS 主线程和 web-worker 线程之间共享。

迄今为止,如果想要在 JS 主线程和 web-worker 之间共享数据,必须拷贝数据然后使用 postMessage 发送。有了 SharedArrayBuffer,这样历史就一去不复还了。

但是线程间共享内存会导致竞态条件,为避免这个问题,引入了 Atomics 全局对象。Atomics 提供各种方法锁定线程正在使用数据的共享内存,同时提供安全更新共享内存里的数据的方法。

建议通过某些库使用这个功能,但目前还没有基于此功能封装的库。

更多参考链接:

移除标签式模板字面量(Tagged Template literal)限制

标签式模板字面量示例如下(更多细节参见 MDN 文档)。

// A "Tag" function returns a custom string literal.
// In this example, greet calls timeGreet() to append Good //Morning/Afternoon/Evening depending on the time of the day.
function greet(hardCodedPartsArray, ...replacementPartsArray) {
  console.log(hardCodedPartsArray); // [ 'Hello ', '!' ]
  console.log(replacementPartsArray); // [ 'Raja' ]
  let str = '';

  hardCodedPartsArray.forEach((string, i) => {
    if (i < replacementPartsArray.length) {
      str += `${string} ${replacementPartsArray[i] || ''}`;
    } else {
      str += `${string} ${timeGreet()}`; // <-- append Good morning/afternoon/evening here
    }
  });
  return str;
}

// 🚀Usage:
const firstName = 'Raja';
const greetings = greet`Hello ${firstName}!`; // 👈 <-- Tagged literal

console.log(greetings); //'Hello  Raja! Good Morning!' 🔥

function timeGreet() {
  const hr = new Date().getHours();
  return hr < 12
    ? 'Good Morning!'
    : hr < 18 ? 'Good Afternoon!' : 'Good Evening!';
}

但是,ES2015、ES2016 规范不允许使用转义字符,如 "\u" (unicode), "\x" (十六进制),除非完全像 \u00A9\u{2F804}\xA9 这几种形式。

如果在某些领域(如终端)使用形如 \ubla123abla 的标签式模板字符串,会抛出语法错误。

ES2018 解除了这一限制。

function myTagFunc(str) {
  return { "cooked": "undefined", "raw": str.raw[0] }
} 

const str = myTagFunc `hi \ubla123abla`; // call myTagFunc

// { cooked: "undefined", raw: "hi \\unicode" }

正则表达式 dotAll 标记(s

当前 JS 的正则中,. 匹配任意字符,但不包含换行符 \n\r\u2028\u2029

ES2018 添加了 s 标记,让 . 可以匹配包括换行符在内的所有字符(提案详情)。

// Before
/first.second/.test('first\nsecond'); // false

// ECMAScript 2018,注意 s 标记
/first.second/s.test('first\nsecond'); // true

正则表达式命名捕获组

正则表达式命名捕获组是从 Python、Java 等语言中借鉴来的功能,以 (?<name>...) 的形式标识正则中的分组,然后通过组名获取该组的匹配的值。

基本示例

下面的例子中使用命名捕获组获取年、月、日。

// 未使用命名捕获组
const reg1 = /(\d{4})-(\d{2})-(\d{2})/;
const result1 = reg1.exec('2015-01-02');
console.log(result1);
//  ["2015-01-02", "2015", "01", "02", index: 0, input: "2015-01-02", groups: undefined]

// 使用 ES2018 命名捕获组
const reg2 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result2 = reg2.exec('2018-05-07');
// ["2018-05-07", "2018", "05", "07", index: 0, input: "2018-05-07", groups: {year: "2018", month: "05", day: "07"}]

// 剩下的你应该知道怎么用了吧

在正则内部使用命名捕获组

在正则内容可以通过 \k<group name> 的形式反向引用捕获组。

// 命名捕获组 `fruit` 可以捕获 `apple` 或者 `orange`
// 通过 `\k<fruit>` 发现引用 `fruit` 捕获组,以判断左右是否一致
const sameFruit = /(?<fruit>apple|orange)==\k<fruit>/u;

sameFruit.test('apple==apple'); // true
sameFruit.test('orange==orange'); // true
sameFruit.test('apple==orange'); // false

String.prototype.replace() 中使用命名捕获组

const reg = /(?<firtName>[A-Za-z]+) (?<lastName>[A-Za-z]+$)/u;

'Minwe Luo'.replace(reg, '$<lastName>, $<firstName>'); // 'Luo Minwe'

参考链接:

展开/剩余操作符 ...

展开/剩余操作符 ... 用得比较普通,应该都很熟悉了。

Tip:赋值等号右边的为展开操作,左边为剩余操作

Object Rest/Spread Properties for ECMAScript

正则表达式向后断言(Lookbehind Assertions)

向后断言可用于判断特定字符是否出现在某些字符之前

肯定断言(?<=...)

/(?<=#).*/.test('hello'); // false
/(?<=#).*/.test('#hello'); // true

'#hello'.match(/#.*/)[0]; // '#hello' 匹配的字符串包含 `#`
'#hello'.match(/(?<=#).*/)[0]; // 匹配 'hello',不包含 `#`

否定断言 (?<!...):下面的正则提取前面不包含 $ 的数字。

// 不匹配,数字前面是 `$`
'A gallon of milk is $3.00'.match(/(?<!\$)\d+.?\d+/); // null

// 匹配,数字前面非 `$`
'A gallon of milk is ¥3.43'.match(/(?<!\$)\d+.?\d+/)[0]; // 3.43

RegExp Unicode Property Escapes

参考链接:

Promise.prototype.finally()

finally() 方法用于 Promise 无论是 resove 还是 reject 都执行一个回调,一般用于清理性操作。

resove 的情形:

let started = true;

let myPromise = new Promise(resolve => resolve('all good'))
    .then(value => console.log(value)) // 输出 'all good'
    .catch(e => console.log(e)) // 跳过
    .finally(() => {
      console.log('done');
      
      started = false; // 清理工作
    });

reject 的情形:

let started = true;

let myPromise = new Promise(resolve => resolve('reject'))
    .then(value => console.log(value)) // 输出 'reject'
    .catch(e => console.log(e)) // 跳过
    .finally(() => {
      console.log('done');
      
      started = false; // 清理工作
    });

Promise 中抛出异常的情形:

let started = true;

let myPromise = new Promise(() => {
  throw new Error('error')
})
  .then(value => console.log(value)) // 跳过
  .catch(e => console.log(e)) // 捕获错误
  .finally(() => {
    console.log('done'); // 执行,但是没有值传下来
    
    started = false; // 清理工作
  });

catch 抛出异常的情形:

let started = true;

let myPromise = new Promise(() => {
  throw new Error('error')
})
  .then(value => console.log(value)) // 跳过
  .catch(e => {
    throw new Error('error in catch');
  }) // 捕获错误
  .finally(() => {
    console.log('done'); // 仍然执行
    
    started = false; // 清理工作
    // catch 中抛出的异常需要在其他地方处理
  });

异步迭代

新增的 for-await-of 循环执行异步迭代。

const promises = [
  new Promise((resolve => resolve(1))),
  new Promise((resolve => resolve(2))),
  new Promise((resolve => resolve(3))),
];

// for-of 同步迭代:输出三个 promise 对象
async function test1() {
  for (const obj of promises) {
    console.log(obj);
  }
}

// for-await-of 异步迭代,等待 Promise resove 并返回结果
async  function test2() {
  for await (const obj of promises) {
    console.log(obj); 
  }
}

test1(); // promise, promise, promise
test2(); // 1, 2, 3

参考链接:

呃,列举下来还真不少,是不是看得有点头大?

使用这些功能时,某些可以通过 Babel 转换,某些则需要注意增加相应的 polyfill,不然代码到老一点的浏览器就挂了。换个角度看,如果需要考虑兼容性,Array.prototype.includes() 之类的 API 并不值得使用,毕竟和 indexOf() 差别不大,引入 polyfill 的成本远大于其带来的便利。

VIA: Here are examples of everything new in ECMAScript 2016, 2017, and 2018