ES7-ES12新特性及其兼容性一览(持更)

2,590 阅读11分钟

ES全称ECMAScript,ECMAScript是ECMA制定的标准化脚本语言。ECMA规范最终由TC39敲定。TC39由包括浏览器厂商在内的各方组成,他们开会推动JavaScript提案沿着一条严格的发展道路前进。 从提案到入选ECMA规范主要有以下几个阶段:

  • Stage 0: strawman——最初想法的提交
  • Stage 1: proposal(提案)——由TC39至少一名成员倡导的正式提案文件
  • Stage 2: draft(草案)——功能规范的初始版本,该版本包含功能规范的两个实验实现
  • Stage 3: candidate(候选)——提案规范通过审查并从厂商那里收集反馈
  • Stage 4: finished(完成)——提案准备加入ECMAScript,但是到浏览器或者Nodejs中可能需要更长的时间

对于ES6的使用我们已经相对熟悉,自从ES6发布之后TC39每年都会发布新特性,市面上的浏览器也在不断支持ECMAScript的新特性,所以了解这些新特性很有必要。

该文档说明了ES的各版本(es7-es12)规范包含的所有特性,但仅拣选部分小程序环境内(开启增强编译)已经过验证支持并且个人认为相对实用、常用的特性进行详细说明,这些其实已经足够我们日常使用,对于比较冷门的特性大家如果感兴趣的话本文也有推荐的文档可自行查阅。

关于兼容性,这里说明一下为什么只拎出来小程序的兼容性说明?

小程序的 JS 执行环境 在不同平台上的执行环境存在差异,因此导致不同平台对 ECMAScript 标准的支持存在差异。 小程序基础库为了尽量抹平这些差异,内置了一份 core-js Polyfillcore-js 可以将平台环境缺失的标准 API 补齐。

现在前端开发基本都是工程化(关于前端工程化),依靠代码转换工具以及Polyfill后几乎可以在你的工程化项目中无忧使用新特性,因为你可以自己任意配置你要转换的特性,但是小程序中的代码转换是由官方定义的,每个基础库的版本对应加入的polyfill不同,这就我们往往要兼容稍低版本的基础库,以覆盖一些老机型或使用老版本微信客户端的用户,所以,开发小程序时还是有很必要关注一下新特性的兼容性问题。

小程序的运行环境

微信小程序运行在多种平台上:iOS/iPadOS 微信客户端、Android 微信客户端、Windows PC 微信客户端、Mac 微信客户端、小程序硬件框架和用于调试的微信开发者工具等。

不同运行环境下,脚本执行环境以及用于组件渲染的环境是不同的,性能表现也存在差异:

在苹果生态上,小程序逻辑层的JS代码运行在JavaScriptCore中,而在Android上,JS代码运行在V8引擎中。

尽管各运行环境是十分相似的,但是还是有些许区别:

JavaScript 语法和 API 支持不一致:语法上开发者可以通过开启 ES6 转 ES5 的功能(默认自动选中增强编译)来规避(详情);此外,小程序基础库内置了必要的Polyfill,来弥补 API 的差异

增强编译是基于 babel7 实现的,使用preset-env,支持最新的ECMAScript语法,编译目标为 {chrome:53, ios:8},增强了ES6转ES5的能力,启用后会使用新的编译逻辑。

这块我决定另开一篇文章详细写一下babel转码的过程,我们可以更深刻地理解小程序是如何支持这些新特性的。

所以,开启增强编译后意味着绝大部分新特性都可以在小程序中使用了,开发起来更爽了!

ES7新特性(2016)

  • Array.prototype.includes
  • 指数运算符 **

Array.prototype.includes

此功能引入了更易读的语法,用于检查数组是否包含元素,某些情况下可替换indexOf

indexOf 比 includes 多一个判断

const arry = [1];

if (arry.indexOf(1) !== -1)  console.log("数组存在1")

if (arry.includes(1)) console.log("数组存在1")

替代写多个逻辑判断,includes也是避免代码复杂度过高的有效方法之一

if(from === 'a' || from === 'b' || from === 'c'){}

if(['a', 'b', 'c'].includes(from)) {}

指数运算符 **

指数运算符 ** 等价于 Math.pow()

Math.pow2,10) === 2 ** 10
Math.pow4,2)=== 4 ** 2

ES8新特性(2017)

  • 字符串填充(padStart 和 padEnd)
  • Object.values
  • Object.entries
  • Object.getOwnPropertyDescriptors()
  • 函数参数列表结尾允许逗号
  • async/await
  • 共享内存 和 Atomics

字符串填充(padStart 和 padEnd)

字符串填充的目的是 向字符串添加字符,使字符串达到指定的长度。

该特性基础库2.16.1起开始支持


String.padStart(targetLength,[padString])
console.log('0.0'.padStart(4,'10')) //10.0
console.log('0.0'.padStart(20))// 0.00
String.padEnd(targetLength,padString])
console.log('0.0'.padEnd(4,'0')) //0.00
console.log('0.0'.padEnd(10,'0'))//0.00000000

Object.values()

返回一个包含所有对象自身属性值的数组,不包含原型链中的属性值。

该特性基础库2.16.1起开始支持


const person = { name: 'Fred', age: 87 }
Object.values(person) // ['Fred', 87]
const people = ['Fred', 'Tony']
Object.values(people) // ['Fred', 'Tony']

Object.entries()

返回一个包含所有对象自身属性的数组,作为 [key,value] 对的数组。

该特性基础库2.16.1起开始支持

const person = { name: 'Fred', age: 87 }
Object.entries(person) // [['name', 'Fred'], ['age', 87]]
const people = ['Fred', 'Tony']
Object.entries(people) // [['0', 'Fred'], ['1', 'Tony']]

与for...of循环一起使用,可以很方便地对键值进行操作。

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

Async/Await

到目前为止,这个特性应该是最重要和最有用的功能。阮一峰老师的文章里进行了很细致的说明,本文就不过多赘述啦。 大体功能就是使用async 函数将异步操作包裹起来,然后在函数内部使用await命令等待异步操作的完成,把异步写成同步代码的形式,看起来非常简洁易读,强烈推荐使用。

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

ES9新特性(2018)

  • 对象的Rest(剩余)/Spread(展开) 属性
  • Asynchronous iteration (异步迭代)- for-await-of
  • Promise.prototype.finally()
  • 正则表达式命名捕获组
  • 正则表达式dotAll模式 - ‘s’标志
  • 正则表达式反向断言
  • Unicode 属性转义 \p{…} 和 \P{…}
  • 非转义序列的模板字符串
  • Lifting template literal restriction - 去除模板文字限制
  • 共享内存和 atomics

Promise.finally()

在某些情况下,你想要在无论Promise运行成功还是失败,运行相同的代码;

小程序基础库2.16.1开始支持,但是在此版本之前你如果想用的话需要手动补充Promise补充原型方法


// 兼容ios真机环境下Promise对象不存在finally方法
if (!Promise.prototype.finally) {
  Promise.prototype.finally = function (cb) {
    return this.then((res) => {
      cb && cb(res);
    }, (error) => {
      cb && cb(error);
    });
  };
}

对象的Rest/Spread

ES2015引入了Rest参数和扩展运算符。三个点(...)仅用于数组。 Rest参数语法允许我们将一个不定数量的参数提取为一个数组。

restParam(1, 2, 3, 4, 5);

function restParam(p1, p2, ...p3) {
  // p1 = 1
  // p2 = 2
  // p3 = [3, 4, 5]
}
// ...p3 即提取剩余参数为开一个数组赋值给 p3

展开操作符以相反的方式工作,将数组展开为单独参数的形式。例如Math.max()返回给定数字中的最大值:

const values = [99, 100, -1, 48, 16];
console.log( Math.max(...values) ); // 100

ES9为对象解构提供了和数组一样的Rest参数()和展开操作符,一个简单的例子:

const myObject = {
        a: 1,
        b: 2,
        c: 3
    };

const { a, ...x } = myObject;
// a = 1
// x = { b: 2, c: 3 } (提取出myObject的b和c属性赋值给x)

可以使用它给函数传递参数:

restParam({
    a: 1,
    b: 2,
    c: 3
  });

function restParam({ a, ...x }) {
// a = 1
// x = { b: 2, c: 3 }
}

跟数组一样,Rest参数只能在声明的结尾处使用。此外,它只适用于每个对象的顶层,如果对象中嵌套对象则无法适用。

可用作对象的浅拷贝:

const obj1 = { a: 1, b: 2, c: 3 };
const obj2 = { ...obj1, z: 26 };
// obj2 is { a: 1, b: 2, c: 3, z: 26 }

正则表达式命名捕获组

ES2018允许命名捕获组使用符号"?",在打开捕获括号(后立即命名,示例如下:

// 原正则: /[0-9]{4}-[0-9]{2}([0-9]{2}/
// 然后我们将每一项命名分组
const
  reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
  match = reDate.exec('2020-04-30'),
  year = match.groups.year, // 2020
  month = match.groups.month, // 04
  day = match.groups.day; // 30

命名捕获也可以使用在replace()方法中。例如将日期转换为 MM-DD-YYYY 格式:


const
reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
d = '2018-04-30',
usDate = d.replace(reDate, '$<month>-$<day>-$<year>');  // 04-30-2018

正则表达式dotAll模式 - ‘s’ 标志

正则表达式中点.匹配除回车外的任何单字符,标记s改变这种行为,允许行终止符的出现,例如:


/hello.world/.test('hello\nworld'); // false
/hello.world/s.test('hello\nworld'); // true 加入s之后允许匹配\n

异步迭代

这是一个非常有用的特性,基于该特性我们可以轻松创建异步代码循环,尤其是重复类型的异步操作过多时,更应该使用该写法。

  const fn = (time) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(`${time}s后我执行成功了`);
      }, time);
    });
  };
  let arr = [fn(3000),fn(2000),fn(1000)]
  async function fn2(){
      for await(e of arr){
          console.log(e);
      }
  }
  fn2();
  输出:
  3000s后我执行成功了
  2000s后我执行成功了
  1000s后我执行成功了

ES10新特性(2019)

  • String.prototype.trimStart() / String.prototype.trimEnd()
  • Object.fromEntries()
  • Array.prototype.flat() / Array.prototype.flatMap()
  • Optional Catch Binding - 可选的catch参数
  • Function.prototype.toString 重新修订
  • Symbol.prototype.description
  • JSON Superset超集
  • JSON.stringify() 加强格式转化
  • Array.prototype.sort() 更加稳定

String的trimStart()方法和trimEnd()

用于去除字符串首尾空白字符

该特性基础库2.16.1起开始支持

Object.fromEntries()

Object.entries()方法的作用是返回一个给定对象自身可枚举属性的键值对数组,而Object.fromEntries() 则是 Object.entries() 的反转。 Object.fromEntries()可以将map或数组转为Object

该特性基础库2.16.1起开始支持


const arr = [ ['0', 'a'], ['1', 'b'], ['2', 'c'] ];
const obj = Object.fromEntries(arr); // { 0: "a", 1: "b", 2: "c" }

Array的flat()方法和flatMap()

这两个也是很有用的数组方法,处理接口返回的复杂结构数据时会经常用到

该特性基础库2.16.1起开始支持

flat()用于数组降维


const arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2); // 压平2层
// [1, 2, 3, 4, 5, 6]

flat()还可用于去除数组的空项


const arr4 = [1, 2, , 4, 5];
arr4.flat();
// [1, 2, 4, 5]

flatMap()相当于在map()的基础上将结果压平一层


const arr1 = [1, 2, 3, 4];
arr1.map(x => [x * 2]); // [[2], [4], [6], [8]]
arr1.flatMap(x => [x * 2]); // [2, 4, 6, 8]

// 只会将 flatMap 中的函数返回的数组 “压平” 一层
arr1.flatMap(x => [[x * 2]]); // [[2], [4], [6], [8]]

修改 catch 绑定

ES10 提案使我们能够简单的把变量省略掉


try {} catch(e) {}
try {} catch {}

ES11新特性(2020)

  • Optional Chaining - 可选链运算符
  • Nullish coalescing Operator - 空值合并运算符
  • import.meta - 给模块内部提供一种获取上下文信息的途径
  • for-in mechanics - 规范for-in的迭代顺序,内核优化
  • globalThis - 全局对象
  • Promise.allSettled
  • BigInt
  • import() - 动态导入
  • String.prototype.matchAll
  • 类的私有方法和属性

可选链运算符 - Optional Chaining

用来解决判断属性是否存在时,代码容易写的很长


const street = user.info && user.info.address && user.info.address.street;
使用可选链运算符,可以简化代码
const street = user.info?.address?.street;

强烈推荐,别再用冗长的 && 运算符了。

空值合并运算符 Nullish coalescing Operator

左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数,与||的区别是,||左侧为false的情况也会命中这个逻辑

const undefinedValue = response.settings.undefinedValue ?? 'some other default';

String.prototype.matchAll

matchAll() 方法返回一个包含所有匹配正则表达式及分组捕获结果的迭代器。 获取全局所有匹配项,包括子项

该特性基础库2.16.1起开始支持


const regexp = /t(e)(st(\d?))/g;
const str = 'test1test2';
str.match(regexp); // match方法
// Array ['test1', 'test2']
let array = [...str.matchAll(regexp)];  // matchAll返回的是一个迭代器,不能直接转为数组
array[0]; // ['test1', 'e', 'st1', '1', index: 0, input: 'test1test2', length: 4]
array[1]; // ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2', length: 4]

Promise.allSettled

allSettled 统一处理 Promise 的状态,当所有 Promise 对象状态都变为 resolved 或 rejected 时,返回一个状态数组。

该特性基础库2.16.1起开始支持,旧版本如需使用可自写方法实现。


const promise1 = new Promise(function(resolve,reject){
  setTimeout(function(){
    reject('promise1')
  },2000)
})
const promise2 = new Promise(function(resolve,reject){
  setTimeout(function(){
    resolve('promise2')
  },3000)
})
const promise3 = Promise.resolve('promise3')
const promise4 = Promise.reject('promise4')

Promise.allSettled([promise1,promise2,promise3,promise4]).then((args)=> {
  console.log(args);
  /*
  result:
  [
    {"status":"rejected","reason":"promise1"},
    {"status":"fulfilled","value":"promise2"},
    {"status":"fulfilled","value":"promise3"},
    {"status":"rejected","reason":"promise4"}
  ]*/
})

类的私有方法和属性

使用 # 可以在类中定义私有方法和属性

参照

class Foo {
  #a;
  #b;
  constructor(a, b) {
    this.#a = a;
    this.#b = b;
  }
  #sum() {
    return this.#a + this.#b;
  }
  printSum() {
    console.log(this.#sum());
  }
}

ES12新特性(2021)

  • String.prototype.replaceAll
  • Logical Assignment Operators - 逻辑赋值操作符
  • Promise.any
  • WeakRefs - 弱引用
  • Numeric separators - 数字分隔符

String.prototype.replaceAll

替换所有匹配的字符串

const str = "Visit Microsoft! Visit Microsoft!";
const n = str.replaceAll("Microsoft","Runoob");
console.log(n); // Visit Runoob! Visit Runoob!

Logical Assignment Operators - 逻辑赋值操作符

新的 Logical Assignment Operators 提案来自于 Ruby 语言,旨在不影响可读性的情况下尽可能最小化代码量


a ||= b; // 等同于 a || (a = b);
a &&= b; // 等同于 a && (a = b);
a ??= b; // 等同于 a ?? (a = b);

可用于缓存变量,减少耗时操作,提高性能 例:

return this.appShowScene ??= wx.getStorageSync('appShowScene');

等同于

if (this.appShowScene) {
  return this.appShowScene;
}
this.appShowScene = wx.getStorageSync('appShowScene');
return this.appShowScene;

Promise.any

Promise.any()有一个子实例成功就算成功,全部子实例失败才算失败。

该特性基础库2.16.1起开始支持,旧版本如需使用可自写方法。

Promise.any([promise1, promise2]).then(()=>{})
// promise1`和`promise2`任意一个成功就会then

Numeric separators - 数字分隔符

数字分隔符不影响原本的数值,可以增加可读性。

console.log(1_000_000_000);
// 1000000000

console.log(10.000_001);
// 10.000001

参考文档

TC39流程
提案文档
已完成的提案
ECMAScript 2016,2017 和 2018 中所有新功能的示例
ES6、ES7、ES8、ES9、ES10新特性一览
特性在不同JS引擎下的支持情况