ES6、ES7 ~ ES2021 都更新了啥?

3,166 阅读32分钟

从ES6开始

为什么从 ES6 开始呢?因为 ES6 从开始制定到最后发布,整整用了 15 年,其新增的特性,以及统一的标准,使得它成为 JavaScript 新语法、新纪元的代名词。因为浏览器的支持程度逐年提升,编译和打包工具也十分完善,很多从 ES6 直接起步的前端开发工程师,可以直接摒弃之前的历史包袱,拥抱新特性。

ECMAScript 1.0 是 1997 年发布的,接下来的两年,连续发布了 ECMAScript 2.0(1998 年 6 月)和 ECMAScript 3.0(1999 年 12 月)。3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习 JavaScript,其实就是在学 3.0 版的语法。

2000 年,ECMAScript 4.0 开始酝酿。这个版本最后没有通过,但是它的大部分内容被 ES6 继承了。因此,ES6 制定的起点其实是 2000 年。

为什么 ES4 没有通过呢?因为这个版本太激进了,对 ES3 做了彻底升级,导致标准委员会的一些成员不愿意接受。ECMA 的第 39 号技术专家委员会(Technical Committee 39,简称 TC39)负责制订 ECMAScript 标准,成员包括 Microsoft、Mozilla、Google 等大公司。

2007 年 10 月,ECMAScript 4.0 版草案发布,本来预计次年 8 月发布正式版本。但是,各方对于是否通过这个标准,发生了严重分歧。以 Yahoo、Microsoft、Google 为首的大公司,反对 JavaScript 的大幅升级,主张小幅改动;以 JavaScript 创造者 Brendan Eich 为首的 Mozilla 公司,则坚持当前的草案。

2008 年 7 月,由于对于下一个版本应该包括哪些功能,各方分歧太大,争论过于激烈,ECMA 开会决定,中止 ECMAScript 4.0 的开发,将其中涉及现有功能改善的一小部分,发布为 ECMAScript 3.1,而将其他激进的设想扩大范围,放入以后的版本,由于会议的气氛,该版本的项目代号起名为 Harmony(和谐)。会后不久,ECMAScript 3.1 就改名为 ECMAScript 5。

2009 年 12 月,ECMAScript 5.0 版正式发布。Harmony 项目则一分为二,一些较为可行的设想定名为 JavaScript.next 继续开发,后来演变成 ECMAScript 6;一些不是很成熟的设想,则被视为 JavaScript.next.next,在更远的将来再考虑推出。TC39 委员会的总体考虑是,ES5 与 ES3 基本保持兼容,较大的语法修正和新功能加入,将由 JavaScript.next 完成。当时,JavaScript.next 指的是 ES6,第六版发布以后,就指 ES7。TC39 的判断是,ES5 会在 2013 年的年中成为 JavaScript 开发的主流标准,并在此后五年中一直保持这个位置。

2011 年 6 月,ECMAScript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。

2013 年 3 月,ECMAScript 6 草案冻结,不再添加新功能。新的功能设想将被放到 ECMAScript 7。

2013 年 12 月,ECMAScript 6 草案发布。然后是 12 个月的讨论期,听取各方反馈。

2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。从 2000 年算起,这时已经过去了 15 年。

—— 摘自阮一峰《ES6入门教程》

正因为如此久的筹备和极高的完成度,ES6是现代浏览器中最重要,也是内容最多的提案变更。本文在ES6上仅做最常用的相关特性介绍,更多ES6特性,可以查看阮一峰《ES6入门教程》,或《你不知道的JavaScript · 第三卷》

ECMAScript 历史版本特性汇总

名称别名特性
ECMAScript2015ES6let & const结构赋值Array新特性Function新特性Object新特性String新特性Number新特性Math新特性ClassSymbolSetMapProxyReflect PromiseGeneratorIteratorModule
ECMAScript2016ES7Array.prototype.includes**幂运算符
ECMAScript2017ES8async/awaitString.prototype.padStartString.prototype.padEndObject.valuesObject.entriesObject.getOwnPropertyDescriptors尾后逗号
ECMAScript2018ES9for await...ofSymble.asyncIteratorPromise.prototype.finally正则新增:具名组匹配、后行断言、dotAll、unicode转义对象支持rest&spread操作符
ECMAScript2019ES10String.prototype.trimStartString.prototype.trimEndArray.prototype.flatArray.prototype.flatMapObject.fromEntries可选的Catch BindingSymble.prototype.descriptionJSON superset & stringify()增强
ECMAScript2020ES11String.prototype.matchAllDynamic importBigIntPromis.allSelltedglobalThis可选链(Optional chaining)空值合并(Nullish coalescing )
ECMAScript2021ES12String.prototype.replaceAll逻辑赋值运算符(Logical Assignment Operator)数字分隔符(Numeric Separators)Promise.anyWeakRef

ECMAScript2015(ES6)

名称别名特性
ECMAScript2015ES6let & const结构赋值Array新特性Function新特性Object新特性String新特性Number新特性Math新特性ClassSymbolSetMapProxyReflect PromiseGeneratorIteratorModule

块级作用域

// 块声明
{ StatementList }
// 标记块声明
// 用于视觉识别的可选 label 或 break 的目标
LabelIdentifier: { StatementList }

在非严格模式(non-strict mode)下的var 或者函数声明时, 通过var声明的变量或者非严格模式下(non-strict mode)创建的函数声明没有块级作用域。

使用 letconst 声明的变量是块级作用域的。且他们 不存在变量提升 ,所声明的变量一定要在声明后使用,否则报错。

let 语句声明一个块级作用域的本地变量(var为整个函数作用域或全局 window),并且可选的将其初始化为一个值。

const 是块级范围的,非常类似用 let 语句定义的变量,声明时,必须显示赋值。一旦赋值,无法(通过重新赋值)改变,也不能被重新声明。常量在声明的时候可以使用大小写,但通常情况下全部用大写字母。

var a = [];
for (var i = 0; i < 10; i++) {
	a[i] = function () {console.log(i);};
}
a[0]();  // 10
a[6]();  // 10

/********************/

var a = [];
for (let i = 0; i < 10; i++) {
	a[i] = function () {console.log(i);};
}
a[0]();  // 0
a[1]();  // 1
a[6]();  // 6

块级作用域存在暂时性死区, 即只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响,即使外部已经有相同的变量,块内的变量在未声明前使用依旧会报错。

function go(n) {
  console.log(n); // Object {a: [1,2,3]}
  for (let n of n.a) { // ReferenceError
    console.log(n);
  }
}

go({a: [1, 2, 3]});

上方代码报错是因为 n.an 被理解为在 for 循环块级作用域内的变量,存在暂时性死区,let n of n.a 即在 n 还未声明时,想要读取 n.a,所以会报错。

箭头函数

  • 箭头函数没有自己的 this 对象,它的 this 始终指向定义时上层作用域中的this
  • 它不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数
  setInterval(() => this.s1++, 1000);
  // 普通函数
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0

由于箭头函数使得this从 “动态” 变成 “静态”,在对象上增加带有 this 的函数,及绑定事件等需要动态 this 指向的时候,不应该使用箭头函数。

var s = 21;

const obj = {
  s: 42,
  m: () => console.log(this.s)
};

obj.m() // 21

上面例子中,obj.m()使用箭头函数定义。JavaScript 引擎的处理方法是,先在全局空间生成这个箭头函数,然后赋值给obj.m,这导致箭头函数内部的this指向全局对象,所以obj.m()输出的是全局空间的21,而不是对象内部的42

var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});

上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

解构、展开和剩余

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为 解构(Destructuring)。如果解构不成功,变量的值就等于undefined

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
var {x: y = 3} = {x: 5};
y // 5

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

function myFunction(v, w, x, y, z) {}
var args = [0, 1];
myFunction(-1, ...args, 2, ...[3]);

剩余语法(Rest syntax) 看起来和展开语法完全相同,不同点在于, 剩余参数用于解构数组和对象。从某种意义上说,剩余语法与展开语法是相反的:展开语法将数组展开为其中的各个元素,而剩余语法则是将多个元素收集起来并“凝聚”为单个元素。

function multiply(multiplier, ...theArgs) {
  return theArgs.map(function (element) {
    return multiplier * element;
  });
}

var arr = multiply(2, 1, 2, 3);
console.log(arr);  // [2, 4, 6]

模板字符串

模板字符串使用反引号 (` ) 来代替普通字符串中的用双引号和单引号。模板字符串可以包含特定语法(${expression})的占位符,大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性,还可以调用函数,也可以嵌套${expression}。在模版字符串内使用反引号时,需要在它前面加转义符(\)。

let x = 1;
let y = 2;

`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"

`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"

let obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// "3"

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

定义类的方法是使用 类声明类表达式。要声明一个类,你可以使用带有class关键字的类名。类声明不会做变量提升。你首先需要声明你的类,然后再访问它。

constructor方法是一个特殊的方法,即构造函数,这种方法用于创建和初始化一个由class创建的对象。一个类只能拥有一个名为 “constructor”的特殊方法。

class Rectangle {
    // constructor
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
    // Getter
    get area() {
        return this.calcArea()
    }
}
const square = new Rectangle(10, 10);

console.log(square.area);

extends 关键字在 类声明类表达式 中用于创建一个类作为另一个类的一个子类。在子类的构造函数内可以使用 super 关键字来调用一个父类的构造函数,如果子类中定义了构造函数,那么它必须先调用 super() 才能使用 this 。另外,super 关键字还可以用于调用对象的父对象上的函数。

function Animal (name) {
  this.name = name;
}
Animal.prototype.speak = function () {
  console.log(this.name + ' makes a noise.');
}

class Dog extends Animal {
  speak() {
    super.speak();
    console.log(this.name + ' barks.');
  }
}

var d = new Dog('Mitzie');
d.speak();//Mitzie makes a noise.  Mitzie barks.

模块化

在 ES6 之前,社区制定了一些模块加载方案,如不同的规范 CommonJS( Modules/*** )、AMD、CMD、UMD 等,还有不同的模块加载库如 RequireJS、Sea.js、Browserify 等,前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代之前的规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。这使得你在使用它之后,更容易从代码静态分析工具和 tree shaking 中受益。

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

模块功能主要由两个命令构成:exportimportimport命令用于输入其他模块提供的功能。

import defaultExport from "module-name";
import * as name from "module-name";
import { export } from "module-name";
import { export as alias } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
var promise = import("module-name");//这是一个处于第三阶段的提案。

export命令用于规定模块的对外接口。

// 导出单个特性
export let name1, name2, …, nameN; // also var, const
export let name1 = …, name2 = …, …, nameN; // also var, const
export function FunctionName(){...}
export class ClassName {...}

// 导出列表
export { name1, name2, …, nameN };

// 重命名导出
export { variable1 as name1, variable2 as name2, …, nameN };

// 解构导出并重命名
export const { name1, name2: bar } = o;

// 默认导出
export default expression;
export default function () { … } // also class, function*
export default function name1() { … } // also class, function*
export { name1 as default, … };

// 导出模块合集
export * from …; // does not set the default export
export * as name1 from …; // Draft ECMAScript® 2O21
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;

迭代器和生成器

处理集合中的每个项是很常见的操作。JavaScript 提供了许多迭代集合的方法,从简单的 for 循环到 map()filter()。迭代器和生成器将迭代的概念直接带入核心语言,并提供了一种机制来自定义 for...of 循环的行为。

这个方法因为太过于不常用,不再介绍。更多参见MDN迭代器和生成器

Promise

Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。

这个方法因为太过于常用,不再介绍。

Proxy和Reflect

Proxy 是 Vue 3.0 建立响应式的核心特性。Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : 37;
    }
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b);      // 1, undefined
console.log('c' in p, p.c); // false, 37

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

const duck = {
  name: 'Maurice',
  color: 'white',
  greeting: function() {
    console.log(`Quaaaack! My name is ${this.name}`);
  }
}

Reflect.has(duck, 'color');
// true
Reflect.has(duck, 'haircut');
// false

新增数据类型

Set 对象是的集合,你可以按照插入的顺序迭代它的元素。 Set中的元素只会出现一次,即 Set 中的元素是唯一的。它采用 === 操作符来判定是否相等。另外,NaNundefined都可以被存储在Set 中, NaN之间被视为相同的值(NaN被认为是相同的,尽管 NaN !== NaN)。Set可迭代、可展开、可以被解构赋值。

Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值都可以作为其一个键或一个值。它的结构是一个[key,value] 的数组。键的比较是基于 sameValueZero 算法,NaN是与 NaN 相等的(虽然 NaN !== NaN),剩下所有其它的值是根据 === 运算符的结果判断是否相等。

**WeakSet**和 Set 对象的区别有两点:

  • Set相比,WeakSet 只能是对象的集合,而不能是任何类型的任意值。
  • WeakSet是弱引用:集合中对象的引用为弱引用。 WeakSet中的对象如果没有其他地方对其引用,那么这些对象会被当成垃圾回收掉。 这也意味着WeakSet中没有存储当前对象的列表。 正因为这样,WeakSet 是不可枚举的。

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

一个类型化数组(**TypedArray)**对象描述了一个底层的二进制数据缓冲区(binary data buffer)的一个类数组视图(view)。它在 webcrypto 的应用中有非常重要的作用。

特性加强

字面量加强

对象属性加强

  • 属性定义支持短语法 obj = { x, y }
  • 属性名支持表达式 obj = {["baz" + quux() ]: 42}
  • 添加 __proto__ 属性(原来为chrome独有),但不建议使用

基础数据类型加强

对象新增了Object.assign方法,你也可以使用展开操作符来实现相关能力,他们都是只有一层的浅拷贝。

数组新增:Array.fromArray.ofArray.prototype.fillArray.prototype.findArray.prototype.findIndexArray.prototype.copyWithinArray.prototype.entriesArray.prototype.keysArray.prototype.values方法

const map = new Map([[1, 2], [2, 4], [4, 8]]);
Array.from(map);
// [[1, 2], [2, 4], [4, 8]]
const mapper = new Map([['1', 'a'], ['2', 'b']]);
Array.from(mapper.values());
// ['a', 'b'];

Array.of(7);       // [7]
Array.of(1, 2, 3); // [1, 2, 3]

Array(7);          // [ , , , , , , ]
Array(1, 2, 3);    // [1, 2, 3]

const array1 = [1, 2, 3, 4];
// fill with 0 from position 2 until position 4
console.log(array1.fill(0, 2, 4));
// expected output: [1, 2, 0, 0]
console.log(array1.fill(6));
// expected output: [6, 6, 6, 6]

String新增了String.prototype.includesString.prototype.repeatString.prototype.startsWithString.prototype.endsWith方法

var str = 'To be, or not to be, that is the question.';
console.log(str.includes('To be'));       // true
console.log(str.includes('nonexistent')); // false
console.log(str.includes('To be', 1));    // false

"abc".repeat(-1)     // RangeError: repeat count must be positive and less than inifinity
"abc".repeat(0)      // ""
"abc".repeat(2)      // "abcabc"
"abc".repeat(3.5)    // "abcabcabc" 参数count将会被自动转换成整数.
"abc".repeat(1/0)    // RangeError: repeat count must be positive and less than inifinity
({toString : () => "abc", repeat : String.prototype.repeat}).repeat(2)
//"abcabc",repeat是一个通用方法,也就是它的调用者可以不是一个字符串对象.

Number新增了Number.EPSILONNumber.isIntegerNumber.isSafeIntegerNumber.isFiniteNumber.isNaN(‘NaN’) 方法

Math新增了Math.acoshMath.hypotMath.imulMath.signMath.trunc 方法

ECMAScript2016(ES7)

名称别名特性
ECMAScript2016ES7Array.prototype.includes**幂运算符

Array.prototype.includes()

在 ES6 中我们有 String.prototype.includes() 可以查询给定字符串是否包含一个字符,而在 ES7 中,我们在数组中也可以用 Array.prototype.includes 方法来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false。

技术上来讲,includes() 使用 零值相等 算法来确定是否找到给定的元素。

const arr = [1, 3, 5, 2, '8', NaN, -0]
arr.includes(1) // true
arr.includes(1, 2) // false 该方法的第二个参数表示搜索的起始位置,默认为 0
arr.includes('1') // false
arr.includes(NaN) // true
arr.includes(+0) // true

幂运算符 **

求幂运算符(**)返回将第一个操作数加到第二个操作数的幂的结果。它等效于Math.pow,不同之处在于它也接受BigInts作为操作数。

console.log(3 ** 4);
// expected output: 81

console.log(10 ** -2);
// expected output: 0.01

console.log(2 ** 3 ** 2);
// expected output: 512

console.log((2 ** 3) ** 2);
// expected output: 64

ECMAScript2017(ES8)

名称别名特性
ECMAScript2017ES8async/awaitString.prototype.padStartString.prototype.padEndObject.valuesObject.entriesObject.getOwnPropertyDescriptors尾后逗号

Async 和 Await

async函数是使用async关键字声明的函数。 async函数是AsyncFunction构造函数的实例, 并且其中允许使用await关键字。asyncawait关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用promise

因为这个特性使用太过于普遍,此处不做过多介绍。

String padding

在 ES8 中 String 新增了两个实例函数 String.prototype.padStartString.prototype.padEnd,允许将 空字符串其他字符串 填充到原始字符串的开头或结尾。

padStart() 方法用另一个字符串填充当前字符串(如果需要的话,会重复多次),以便产生的字符串达到给定的长度。该方法从当前字符串的左侧开始填充。

const str1 = '5';

// 从左侧开始填充 '0',直到字符串长度达到 2
console.log(str1.padStart(2, '0'));
// expected output: "05"

const fullNumber = '2034399002125581';
const last4Digits = fullNumber.slice(-4);
// 从左侧开始填充 '*',直到字符串长度达到 fullNumber(原始字符串)的长度
const maskedNumber = last4Digits.padStart(fullNumber.length, '*');
console.log(maskedNumber);
// expected output: "************5581"

padEnd() 方法会用一个字符串填充当前字符串(如果需要的话则重复填充),返回填充后达到指定长度的字符串。从当前字符串的末尾(右侧)开始填充。

const str1 = 'Breaded Mushrooms';
console.log(str1.padEnd(25, '.'));
// expected output: "Breaded Mushrooms........"

const str2 = '200';
console.log(str2.padEnd(5));
// expected output: "200  "

Object扩展

Object.values()

Object.values() 方法返回一个给定对象自身的所有可枚举属性值的数组,值的顺序与使用for...in循环的顺序相同 ( 区别在于 for-in 循环枚举原型链中的属性 )。

var obj = { foo: 'bar', baz: 42 };
console.log(Object.values(obj)); // ['bar', 42]

// array like object with random key ordering
// when we use numeric keys, the value returned in a numerical order according to the keys
var an_obj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.values(an_obj)); // ['b', 'c', 'a']

// getFoo is property which isn't enumerable
var my_obj = Object.create({}, { getFoo: { value: function() { return this.foo; } } });
my_obj.foo = 'bar';
console.log(Object.values(my_obj)); // ['bar']

// non-object argument will be coerced to an object
console.log(Object.values('foo')); // ['f', 'o', 'o']

Object.entries()

Object.entries() 方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还会枚举原型链中的属性)。

这个方法常用于将一个 Object 转换为 Map

const object1 = {
  a: 'somestring',
  b: 42
};
for (const [key, value] of Object.entries(object1)) {
  console.log(`${key}: ${value}`);
}
// expected output:
// "a: somestring"
// "b: 42"

var obj = { foo: "bar", baz: 42 };
var map = new Map(Object.entries(obj));
console.log(map); // Map { foo: "bar", baz: 42 }

Object.getOwnPropertyDescriptors

**Object.getOwnPropertyDescriptors() ** 方法用来获取一个对象的所有自身属性的描述符。该方法的引入目的,主要是为了解决 Object.assign() 无法正确拷贝 get 属性和 set 属性的问题。

const source = {
  set foo (value) {
    console.log(value)
  },
  get bar () {
    return 'bar'
  }
}
const target1 = {}
Object.assign(target1, source)
console.log(Object.getOwnPropertyDescriptor(target1, 'foo'))

结果为:

image.png

上面代码中,source 对象的 foo 属性的值是一个赋值函数,Object.assign 方法将这个属性拷贝给 target1 对象,结果该属性的值变成了 undefined。这是因为 Object.assign 方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法

这时 Object.getOwnPropertyDescriptors() 方法配合 Object.defineProperties() 方法,就可以实现正确拷贝。

const source = {
  set foo (value) {
    console.log(value)
  },
  get bar () {
    return 'bar'
  }
}
const target2 = {}
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source))
console.log(Object.getOwnPropertyDescriptor(target2, 'foo'))

结果为:

image.png

尾后逗号

ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。

此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。

function clownsEverywhere(
  param1,
  param2
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar'
);

上面代码中,如果在param2bar后面加一个逗号,就会报错。

如果像上面这样,将参数写成多行(即每个参数占据一行),以后修改代码的时候,想为函数clownsEverywhere添加第三个参数,或者调整参数的次序,就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar',
);

这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。

ECMAScript2018(ES9)

名称别名特性
ECMAScript2018ES9for await...ofSymble.asyncIteratorPromise.prototype.finally正则新增:具名组匹配、后行断言、dotAll、unicode转义对象支持rest&spread操作符

for await...of 及 Symble.asyncIterator

for...of 方法能够遍历具有 Symbol.iterator 接口的同步迭代器数据,但是不能遍历异步迭代器。ES9 新增的 for await...of 可以用来遍历具有 Symbol.asyncIterator 方法的数据结构,也就是异步迭代器。

for await...of 语句创建一个循环,该循环遍历异步可迭代对象以及同步可迭代对象,包括: 内置的 String, Array,类似数组对象 (例如 argumentsNodeListTypedArray, Map, Set 和用户定义的异步/同步迭代器。

for await...of 不适用于不是异步可迭代的异步迭代器。

async function* asyncGenerator() {
  var i = 0;
  while (i < 3) {
    yield i++;
  }
}

(async function() {
  for await (num of asyncGenerator()) {
    console.log(num);
  }
})();
// 0
// 1
// 2

Symbol.asyncIterator 符号指定了一个对象的默认异步迭代器。如果一个对象设置了这个属性,它就是异步可迭代对象,可用于for await...of循环。

const myAsyncIterable = new Object();
myAsyncIterable[Symbol.asyncIterator] = async function*() {
    yield "hello";
    yield "async";
    yield "iteration!";
};

(async () => {
    for await (const x of myAsyncIterable) {
        console.log(x);
        // expected output:
        // "hello"
        // "async"
        // "iteration!"
    }
})();

Promise.prototype.finally

**finally() ** 方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都需要执行的代码提供了一种方式。

这避免了同样的语句需要在then()catch()中各写一次的情况。

因为太过于常用,不做过多介绍。

正则表达式扩展

具名组匹配

在一些正则表达式模式中,使用数字进行匹配可能会令人混淆。例如,使用正则表达式 /(\d{4})-(\d{2})-(\d{2})/ 来匹配日期。因为美式英语中的日期表示法和英式英语中的日期表示法不同,所以很难区分哪一组表示日期,哪一组表示月份:

const re = /(\d{4})-(\d{2})-(\d{2})/;
const match= re.exec('2019-01-01');
console.log(match[0]); // → 2019-01-01
console.log(match[1]); // → 2019
console.log(match[2]); // → 01
console.log(match[3]); // → 01

ES9 引入了命名捕获组,允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。具名匹配在圆括号内部,模式的头部添加 “问号 + 尖括号 + 组名”,然后就可以在 exec 方法返回结果的 groups 属性上引用该组名。

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = re.exec('2019-01-01');
console.log(match.groups); // → {year: "2019", month: "01", day: "01"}
console.log(match.groups.year); // → 2019
console.log(match.groups.month); // → 01
console.log(match.groups.day); // → 01

后行断言

JavaScript 语言的正则表达式,只支持先行断言,不支持后行断言,先行断言我们可以简单理解为“先遇到一个条件,再判断后面是否满足”,如下面例子:

let test = 'hello world'
console.log(test.match(/hello(?=\sworld)/))
// ["hello", index: 0, input: "hello world", groups: undefined]

但有时我们想判断前面是 world 的 hello,这个代码是实现不了的。在 ES9 就支持这个后行断言了:

let test = 'world hello'
console.log(test.match(/(?<=world\s)hello/))
// ["hello", index: 6, input: "world hello", groups: undefined]

(?<…) 是后行断言的符号,(?..) 是先行断言的符号,然后结合 =(等于)、!(不等)、\1(捕获匹配)。

dotAll

s(dotAll)flag 正则表达式中,点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外:一个是四个字节的 UTF-16 字符,这个可以用 u 修饰符解决;另一个是行终止符, 如换行符 (\n) 或回车符 (\r), 这个可以通过 ES9 的 s(dotAll)flag,在原正则表达式基础上添加 s 表示:

console.log(/foo.bar/.test('foo\nbar')) // false
console.log(/foo.bar/s.test('foo\nbar')) // true

const re = /foo.bar/s // Or, `const re = new RegExp('foo.bar', 's');`.
console.log(re.test('foo\nbar')) // true
console.log(re.dotAll) // true
console.log(re.flags) // 's'

unicode转义

Unicode 属性转义 ES2018 引入了一种新的类的写法\p{...}\P{...},允许正则表达式匹配符合 Unicode 某种属性的所有字符。比如你可以使用\p{Number}来匹配所有的 Unicode 数字,例如,假设你想匹配的 Unicode 字符㉛字符串:

const str = '㉛';
console.log(/\d/u.test(str)); // → false
console.log(/\p{Number}/u.test(str)); // → true

同样的,你可以使用\p{Alphabetic}来匹配所有的 Unicode 单词字符:

const str = ' ';
console.log(/\p{Alphabetic}/u.test(str)); // → true
// the \w shorthand cannot match  
console.log(/\w/u.test(str)); // → false

同样有一个负向的 Unicode 属性转义模板 \P{...}

console.log(/\P{Number}/u.test('㉛')); // → false
console.log(/\P{Number}/u.test(' ')); // → true
console.log(/\P{Alphabetic}/u.test('㉛')); // → true
console.log(/\P{Alphabetic}/u.test(' ')); // → false

对象支持rest&spread操作符

即,对象支持了展开和剩余操作运算符。

因为太过常用,此处不做过多介绍。

ECMAScript2019(ES10)

名称别名特性
ECMAScript2019ES10String.prototype.trimStartString.prototype.trimEndArray.prototype.flatArray.prototype.flatMapObject.fromEntries可选的Catch BindingSymble.prototype.descriptionFunction.prototype.toString增强JSON superset & stringify()增强

String.prototype.trimStart 和 String.prototype.trimEnd

trim() 方法会从一个字符串的两端删除空白字符。在这个上下文中的空白字符是所有的空白字符 (space, tab, no-break space 等) 以及所有行终止符字符(如 LF,CR等)。

String.prototype.trimStartString.prototype.trimEnd 可以移除开头或结尾的空格,之前我们用正则表达式来实现,现在 ES2019 新增了两个新特性,让这变得更简单!

trimStart() 方法从字符串的开头删除空格,trimLeft() 是此方法的别名。

trimEnd() 方法从一个字符串的末端移除空白字符。trimRight() 是这个方法的别名。

const greeting = '   Hello world!   ';
console.log(greeting.trim());
// expected output: "Hello world!";
console.log(greeting.trimStart());
// expected output: "Hello world!   ";
console.log(greeting.trimEnd());
// expected output: "   Hello world!";

Array.prototype.flat

多维数组是一种常见的数据格式,特别是在进行数据检索的时候。将多维数组打平是个常见的需求。通常我们能够实现,但是不够优雅。

**flat() **方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。接受一个传参,指定要提取嵌套数组的结构深度,默认值为 1。

flat() 方法会移除数组中的空项

var arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]

var arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]

var arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]

//使用 Infinity,可展开任意深度的嵌套数组
var arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

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

Array.prototype.flatMap

flatMap() 即先调用 map 方法,然后调用 flat 展开上一步的数组。方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。它与 map 连着深度值为1的 flat 几乎相同,但 flatMap 通常在合并成一种方法的效率稍微高一些。

注意它没有深度传参,因此只能展开 1 层深度。

var arr1 = [1, 2, 3, 4];

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

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

// only one level is flattened
arr1.flatMap(x => [[x * 2]]);
// [[2], [4], [6], [8]]

let arr1 = ["it's Sunny in", "", "California"];

arr1.map(x => x.split(" "));
// [["it's","Sunny","in"],[""],["California"]]

arr1.flatMap(x => x.split(" "));
// ["it's","Sunny","in", "", "California"]

Object.fromEntries

**Object.fromEntries() ** 与 Object.entries() 行为相反。该方法把键值对列表转换为一个对象。

const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
]);

const obj = Object.fromEntries(entries);

console.log(obj);
// expected output: Object { foo: "bar", baz: 42 }

示例:将对象中 value 值大于 21 的项删除。

const obj = {
  a: 12,
  b: 22,
  c: 56
};
let res = Object.fromEntries(Object.entries(obj).filter(([a, b]) => b > 21));
console.log(res); // {b: 22, c: 56}

可选的 Catch Binding

try catch 语句中的 catch 的 error 参数变为了一个可选项。以前我们写 catch 语句时,必须传递一个异常参数。这就意味着,即便我们在 catch 里面根本不需要用到这个异常参数也必须将其传递进去。现在可以不写。

try {
	// something
} catch {
  console.error('foo');
}

Symbol.prototype.description

**description **是一个只读属性,它会返回 Symbol 对象的可选描述的字符串。

Symbol 对象可以通过一个可选的描述创建,可用于调试,但不能用于访问 symbol 本身。Symbol.prototype.description 属性可以用于读取该描述。与 Symbol.prototype.toString() 不同的是它不会包含 "Symbol()" 的字符串。

Symbol('desc').toString();   // "Symbol(desc)"
Symbol('desc').description;  // "desc"
Symbol('').description;      // ""
Symbol().description;        // undefined

// well-known symbols
Symbol.iterator.toString();  // "Symbol(Symbol.iterator)"
Symbol.iterator.description; // "Symbol.iterator"

// global symbols
Symbol.for('foo').toString();  // "Symbol(foo)"
Symbol.for('foo').description; // "foo"

Function.prototype.toString增强

ES2019 对函数实例的toString()方法做出了修改。

toString()方法返回函数代码本身,以前会省略注释和空格。

function /* foo comment */ foo () {}
foo.toString()
// function foo() {}

上面代码中,函数foo的原始代码包含注释,函数名foo和圆括号之间有空格,但是toString()方法都把它们省略了。

修改后的toString()方法,明确要求返回一模一样的原始代码。

function /* foo comment */ foo () {}
foo.toString()
// "function /* foo comment */ foo () {}"

JSON superset

什么是 JSON 超集?简而言之就是让 ECMAScript 兼容所有JSON支持的文本。 ECMAScript 曾在标准 JSON.parse 部分阐明 JSON 确为其一个子集,但由于 JSON 内容可以正常包含 U+2028行分隔符 与 U+2029段分隔符,而ECMAScript 却不行。

JavaScript 字符串允许直接输入字符,以及输入字符的转义形式。举例来说,“中”的 Unicode 码点是 U+4e2d,你可以直接在字符串里面输入这个汉字,也可以输入它的转义形式\u4e2d,两者是等价的。

'中' === '\u4e2d' // true

但是,JavaScript 规定有5个字符,不能在字符串里面直接使用,只能使用转义形式。

  • U+005C:反斜杠(reverse solidus)
  • U+000D:回车(carriage return)
  • U+2028:行分隔符(line separator)
  • U+2029:段分隔符(paragraph separator)
  • U+000A:换行符(line feed)

举例来说,字符串里面不能直接包含反斜杠,一定要转义写成\\或者\u005c

这个规定本身没有问题,麻烦在于 JSON 格式允许字符串里面直接使用 U+2028(行分隔符)和 U+2029(段分隔符)。这样一来,服务器输出的 JSON 被JSON.parse解析,就有可能直接报错。

const json = '"\u2028"';
JSON.parse(json); // 可能报错

JSON 格式已经冻结(RFC 7159),没法修改了。为了消除这个报错,ES2019 允许 JavaScript 字符串直接输入 U+2028(行分隔符)和 U+2029(段分隔符)。

const PS = eval("'\u2029'");

根据这个提案,上面的代码不会报错。

注意,模板字符串现在就允许直接输入这两个字符。另外,正则表达式依然不允许直接输入这两个字符,这是没有问题的,因为 JSON 本来就不允许直接包含正则表达式。

JSON.stringify()增强

根据标准,JSON 数据必须是 UTF-8 编码。但是,现在的JSON.stringify()方法有可能返回不符合 UTF-8 标准的字符串。

具体来说,UTF-8 标准规定,0xD8000xDFFF之间的码点,不能单独使用,必须配对使用。比如,\uD834\uDF06是两个码点,但是必须放在一起配对使用,代表字符𝌆。这是为了表示码点大于0xFFFF的字符的一种变通方法。单独使用\uD834\uDF06这两个码点是不合法的,或者颠倒顺序也不行,因为\uDF06\uD834并没有对应的字符。

JSON.stringify()的问题在于,它可能返回0xD8000xDFFF之间的单个码点。

JSON.stringify('\u{D834}') // "\u{D834}"

为了确保返回的是合法的 UTF-8 字符,ES2019 改变了JSON.stringify()的行为。如果遇到0xD8000xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。

JSON.stringify('\u{D834}') // ""\\uD834""
JSON.stringify('\uDF06\uD834') // ""\\udf06\\ud834""

ECMAScript2020(ES11)

名称别名特性
ECMAScript2020ES11String.prototype.matchAllDynamic importBigIntPromis.allSelltedglobalThis可选链(Optional chaining)空值合并(Nullish coalescing )

String.prototype.matchAll

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

matchAll 出现之前,通过在循环中调用 regexp.exec() 来获取所有匹配项信息(regexp 需使用 /g 标志),因为每一次 exec 只返回一个匹配,所以需要不断循环,并判定没有更多匹配项才停止:

const regexp = RegExp('foo[a-z]*','g');
const str = 'table football, foosball';
let match;

while ((match = regexp.exec(str)) !== null) {
  console.log(`Found ${match[0]} start=${match.index} end=${regexp.lastIndex}.`);
  // expected output: "Found football start=6 end=14."
  // expected output: "Found foosball start=16 end=24."
}

如果使用 matchAll ,就可以不必使用 while 循环加 exec 方式(且正则表达式需使用 /g 标志)。使用 matchAll 会得到一个迭代器的返回值,配合 for...of, array spread, 或者 Array.from() 可以更方便实现功能:

const regexp = RegExp('foo[a-z]*','g');
const str = 'table football, foosball';
const matches = str.matchAll(regexp);

for (const match of matches) {
  console.log(`Found ${match[0]} start=${match.index} end=${match.index + match[0].length}.`);
}
// expected output: "Found football start=6 end=14."
// expected output: "Found foosball start=16 end=24."

// matches iterator is exhausted after the for..of iteration
// Call matchAll again to create a new iterator
Array.from(str.matchAll(regexp), m => m[0]);
// Array [ "football", "foosball" ]

动态导入 Dynamic import

标准用法的import导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。下面的是你可能会需要动态导入的场景:

  • 当静态导入的模块很明显的降低了代码的加载速度且被使用的可能性很低,或者并不需要马上使用它。
  • 当静态导入的模块很明显的占用了大量系统内存且被使用的可能性很低。
  • 当被导入的模块,在加载时并不存在,需要异步获取
  • 当导入模块的说明符,需要动态构建。(静态导入只能使用静态说明符)
  • 当被导入的模块有副作用(这里说的副作用,可以理解为模块中会直接运行的代码),这些副作用只有在触发了某些条件才被需要时。(原则上来说,模块不能有副作用,但是很多时候,你无法控制你所依赖的模块的内容)

请不要滥用动态导入(只有在必要情况下采用)。静态框架能更好的初始化依赖,而且更有利于静态分析工具和tree shaking 发挥作用

关键字import可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个 promise

import('/modules/my-module.js').then((module) => {
  // Do something with the module.
});

这种使用方式也支持 await 关键字。

let module = await import('/modules/my-module.js');

BigInt

BigInt 是一种内置对象,它提供了一种方法来表示大于 253 - 1 的整数。这原本是 Javascript中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。

可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数BigInt()

const theBiggestInt = 9007199254740991n;

const alsoHuge = BigInt(9007199254740991);
// ↪ 9007199254740991n

const hugeString = BigInt("9007199254740991");
// ↪ 9007199254740991n

const hugeHex = BigInt("0x1fffffffffffff");
// ↪ 9007199254740991n

const hugeBin = BigInt("0b11111111111111111111111111111111111111111111111111111");
// ↪ 9007199254740991n

它在某些方面类似于 Number ,但是也有几个关键的不同点:不能用于 Math 对象中的方法;不能和任何 Number 实例混合运算,两者必须转换成同一种类型。在两种类型来回转换时要小心,因为 BigInt 变量在转换成 Number 变量时可能会丢失精度。

以下操作符可以和 BigInt 一起使用: +*-**% 。除 >>> (无符号右移)之外的 位操作 也可以支持。因为 BigInt 都是有符号的, >>> (无符号右移)不能用于 BigInt为了兼容 asm.js BigInt 不支持单目 (+) 运算符。

const previousMaxSafe = BigInt(Number.MAX_SAFE_INTEGER);
// ↪ 9007199254740991n

const maxPlusOne = previousMaxSafe + 1n;
// ↪ 9007199254740992n

const theFuture = previousMaxSafe + 2n;
// ↪ 9007199254740993n, this works now!

const multi = previousMaxSafe * 2n;
// ↪ 18014398509481982n

const subtr = multi – 10n;
// ↪ 18014398509481972n

const mod = multi % 10n;
// ↪ 2n

const bigN = 2n ** 54n;
// ↪ 18014398509481984n

bigN * -1n
// ↪ –18014398509481984n

const expected = 4n / 2n;
// ↪ 2n

const rounded = 5n / 2n;
// ↪ 2n, not 2.5n

0n === 0
// ↪ false

0n == 0
// ↪ true

Promis.allSellted

顾名思义,返回 promise 数组所有的异步均为fulfilledrejected后的 promise,并带有一个对象数组,每个对象表示对应的 promise 结果。

当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise的结果时,通常使用它。

相比之下,Promise.all() 更适合彼此相互依赖或者在其中任何一个reject时立即结束。

不再展开介绍。

globalThis

在以前,从不同的 JavaScript 环境中获取全局对象需要不同的语句。在 Web 中,可以通过 windowself 或者 frames 取到全局对象,但是在 Web Workers 中,只有 self 可以。在 Node.js 中,它们都无法获取,必须使用 global

在松散模式下,可以在函数中返回 this 来获取全局对象,但是在严格模式和模块环境下,this 会返回 undefined。 You can also use Function('return this')(), but environments that disable eval(), like CSP in browsers, prevent use of Function in this way.

globalThis 提供了一个标准的方式来获取不同环境下的全局 this 对象(也就是全局对象自身)。不像 window 或者 self 这些属性,它确保可以在有无窗口的各种环境下正常工作。所以,你可以安心的使用 globalThis,不必担心它的运行环境。为便于记忆,你只需要记住,全局作用域中的 this 就是 globalThis

在很多引擎中, globalThis 被认为是真实的全局对象的引用,但是在浏览器中,由于 iframe 以及跨窗口安全性的考虑,它实际引用的是真实全局对象(不可以被直接访问)的 Proxy 代理。在通常的应用中,很少会涉及到代理与对象本身的区别,但是也需要加以注意。

globalThis 之前,获取某个全局对象的唯一方式就是 Function('return this')(),但是这在某些情况下会违反 CSP 规则,所以,es6-shim 使用了类似如下的方式:

var getGlobal = function () {
  if (typeof self !== 'undefined') { return self; }
  if (typeof window !== 'undefined') { return window; }
  if (typeof global !== 'undefined') { return global; }
  throw new Error('unable to locate global object');
};

var globals = getGlobal();

if (typeof globals.setTimeout !== 'function') {
  // 此环境中没有 setTimeout 方法!
}

但是有了 globalThis 之后,只需要:

if (typeof globalThis.setTimeout !== 'function') {
  //  此环境中没有 setTimeout 方法!
}

可选链 Optional chaining

可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空(nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined

通过连接的对象的引用或函数可能是 undefinednull 时,可选链操作符提供了一种方法来简化被连接对象的值访问。

比如,思考一个存在嵌套结构的对象 obj。不使用可选链的话,查找一个深度嵌套的子属性时,需要验证之间的引用,例如:

let nestedProp = obj.first && obj.first.second;

为了避免报错,在访问obj.first.second之前,要保证 obj.first 的值既不是 null,也不是 undefined。如果只是直接访问 obj.first.second,而不对 obj.first 进行校验,则有可能抛出错误。

有了可选链操作符(?.),在访问 obj.first.second 之前,不再需要明确地校验 obj.first 的状态,再并用短路计算获取最终结果:

let nestedProp = obj.first?.second;

通过使用 ?. 操作符取代 . 操作符,JavaScript 会在尝试访问 obj.first.second 之前,先隐式地检查并确定 obj.first 既不是 null 也不是 undefined。如果obj.first null 或者 undefined,表达式将会短路计算直接返回 undefined

函数调用时如果被调用的方法不存在,使用可选链可以使表达式自动返回undefined而不是抛出一个异常。

let result = someInterface?.customMethod?.();

当使用方括号与属性名的形式来访问属性时,你也可以使用可选链操作符:

let nestedProp = obj?.['prop' + 'Name'];

注意,可选链不能用于赋值

空值合并 Nullish coalescing

空值合并操作符??)是一个逻辑操作符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

逻辑或操作符(||不同,逻辑或操作符会在左侧操作数为假值时返回右侧操作数。也就是说,如果使用 || 来为某些变量设置默认值,可能会遇到意料之外的行为。比如为假值(例如,''0)时。见下面的例子。

const foo = null ?? 'default string';
console.log(foo);
// expected output: "default string"

const baz = 0 ?? 42;
console.log(baz);
// expected output: 0

与 OR 和 AND 逻辑操作符相似,当左表达式不为 nullundefined 时,不会对右表达式进行求值,即短路。

它不能与 AND 或 OR 操作符共用。将 ?? 直接与 AND(&&)和 OR(||)操作符组合使用是不可取的。(因为空值合并操作符和其他逻辑操作符之间的运算优先级/运算顺序是未定义的)这种情况下会抛出 SyntaxError

ECMAScript2021(ES12)

名称别名特性
ECMAScript2021ES12String.prototype.replaceAll逻辑赋值运算符(Logical Assignment Operator)数字分隔符(Numeric Separators)Promise.anyWeakRef

String.prototype.replaceAll

这可能是这个版本里最令我兴奋的功能。

如果使用之前的办法来替换字符串中所有出现的子字符串, replace()必须要结合全局正则表达式的方法来替换,这一方法已经成为业界主流的方式来使用。

现在,使用新方法replaceAll(),我们可以轻松返回一个新字符串,该字符串中的子字符串已经被全部替换,而无需使用复杂的正则表达式。

const myString =
  "I love Cats. Cats are supercute, especially when they are doing Catstuff";

let newString = myString.replaceAll("Cat", "Dog");

console.log(newString);

//I love Dogs. Dogs are supercute, especially when they are doing Dogstuff

此方法还带来了性能改进,因为该方法的内部实现是使用字符串比较,而不是正则表达式匹配。

逻辑赋值运算符 Logical Assignment Operator

逻辑赋值运算符结合了逻辑运算符 ( &&, ||, ??) 和赋值表达式 ( =)。

下面的代码示例显示了在运算符 AND ( &&)、OR ( ||) 和空合并运算符 ( ??)上使用的此功能。

//Only assigns if left-hand side is Truthy
//Old approach
a && (a = b)

//Logical assignment operator
a &&= b

//Only assigns if left hand-side is Falsy
//Old approach
a || (a = b)

//Logical assignment operator
a ||= b 

//Only assigns if left hand side is Nullish
 (null / undefined)
//Old approach
a ?? (a = b)
//Logical assignment operator
a ??= b

下面我们用空值合并逻辑赋值运算符来举例子。

假设,我们有变量money。使用空合并运算符,money变量值为undefinednull时,会被赋值为defaultValue,否则就会被赋值为 money 本身:

const defaultValue = 1;
let money = null;

money = money ?? defaultValue;

而使用逻辑赋值运算符,我们得到了一个稍微短一点的语法:

money ??= defaultValue;

数字分隔符 Numeric Separators

大数字乍一看可能难以阅读,尤其是在有重复数字时。数字分隔符是一个有用的工具,它在数字中用下划线 (_)分隔数字,从而使长数字文字更具可读性。

分隔符可以用在不同的位置,可以使用任意数量的分隔符,以任意大小分组。也就是说,这里可以每4位分割一次,以万、亿为分割,方便中国人阅读

const oneMillion = 1000000;
// 千分位
const oneMillionWithSeparators = 1_000_000;
// 万分位
const oneMillionWithSeparators = 100_0000;
const oneMillionAndALittleMore = 1_000_000.123_456;

正如我们所见,代码变得更具可读性了。

另外,数字分隔符也适用于八进制整数文字。

Promise.any 和 AggregateError

简而言之,这个方法和Promise.all()类似。

Promise.any()接受一个可迭代的 Promise 对象数组,在数组中任意一个Promise resolve 时,即resolve。

考虑下面的示例,我们创建了三个 Promise 并将它们输入到Promise.any().

const promise1 = new Promise((resolve) => setTimeout(resolve, 100, 'first'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 300, 'second'));
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, 'third'));

const promises = [promise1, promise2, promise3];

Promise.any(promises).then((value) => console.log(value));

// Expected output: "first"

如果所有 Promise 都没有resolve,则会抛出一种新类型的异常AggregateErrorAggregateError将错误以对象数组的形式组合为一个错误数组。漂亮整齐!

弱引用 WeakRef

一般来说,在JavaScript中,对象的引用是强引用的,这意味着只要持有对象的引用,它就不会被垃圾回收。只有当该对象没有任何的强引用时, js引擎垃圾回收器才会销毁该对象并且回收该对象所占的内存空间。

var a, b;
a = b = document.querySelector('.someClass')
a = undefined
// ... GarbageCollecting...
// b is still references to the DOM-object .someClass

如果我们不想无限期地将对象保留在内存中,可以用WeakRef来实现缓存到大对象的映射。当不使用时,内存可以被垃圾收集并在再次需要时生成一个新的缓存。

WeakRef 用new WeakRef 来创建,用 .deref() 来读取。

const x = new WeakRef(document.querySelector('.someClass'));
const element = x.deref();

JavaScript 不断集成新功能,今天我们研究了 JavaScript ES2021 中的一些功能。有关提案的更多信息以及接下来会发生什么,请仔细查看此处