ECMAScript2020新特性总结及使用场景
1. String.prototype.matchAll
matchAll()
方法返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器。
const regexp = /t(e)(st(\d?))/g;
const str = "test1test2";
const array = [...str.matchAll(regexp)];
console.log(array[0]);
// expected output: Array ["test1", "e", "st1", "1"]
console.log(array[1]);
// expected output: Array ["test2", "e", "st2", "2"]
1.1 Regexp.exec() 和 matchAll()
在 matchAll
出现之前,通过在循环中调用 regexp.exec()
来获取所有匹配项信息(regexp 需使用 /g
标志):
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}.`
);
// "Found football start=6 end=14."
// "Found foosball start=16 end=24."
}
如果使用 matchAll
,就可以不必使用 while 循环加 exec 方式(且正则表达式需使用 /g
标志)。使用 matchAll
会得到一个迭代器的返回值,配合 [for...of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/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
}.`
);
}
// "Found football start=6 end=14."
// "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" ]
如果没有 /g
标志,matchAll
会抛出异常。
const regexp = RegExp("[a-c]", "");
const str = "abc";
Array.from(str.matchAll(regexp), (m) => m[0]);
// TypeError: String.prototype.matchAll called with a non-global RegExp argument
matchAll
内部做了一个 regexp 的复制,所以不像 regexp.exec, lastIndex
在字符串扫描时不会改变。
const regexp = RegExp("[a-c]", "g");
regexp.lastIndex = 1;
const str = "abc";
Array.from(str.matchAll(regexp), (m) => `${regexp.lastIndex} ${m[0]}`);
// Array [ "1 b", "1 c" ]
1.2 捕获组的更佳途径
matchAll
的另外一个亮点是更好地获取捕获组。因为当使用 match()
和 /g
标志方式获取匹配信息时,捕获组会被忽略:
var regexp = /t(e)(st(\d?))/g;
var str = "test1test2";
str.match(regexp);
// Array ['test1', 'test2']
使用 matchAll
可以通过如下方式获取分组捕获:
let array = [...str.matchAll(regexp)];
array[0];
// ['test1', 'e', 'st1', '1', index: 0, input: 'test1test2', length: 4]
array[1];
// ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2', length: 4]
2. 类的私有属性
类属性在默认情况下是公共的,可以被外部类检测或修改。在ES2020 实验草案 中,增加了定义私有类字段的能力,写法是使用一个#作为前缀。
class ClassWithPrivateField {
#privateField;
}
class ClassWithPrivateMethod {
#privateMethod() {
return "hello world";
}
}
class ClassWithPrivateStaticField {
static #PRIVATE_STATIC_FIELD;
}
2.1 私有静态字段
私有字段可以被类的构造方法(constructor)从内部声明。 静态变量只能被静态方法调用的限制仍然成立。
class ClassWithPrivateStaticField {
static #PRIVATE_STATIC_FIELD;
static publicStaticMethod() {
ClassWithPrivateStaticField.#PRIVATE_STATIC_FIELD = 42;
return ClassWithPrivateStaticField.#PRIVATE_STATIC_FIELD;
}
}
assert(ClassWithPrivateStaticField.publicStaticMethod() === 42);
在类评估时,私有静态字段被添加到类构造函数中。
私有静态字段有一个来源限制, 只有定义该私有静态字段的类能访问该字段。
这可能会导致:当使用this
时出现意想不到的行为。
class BaseClassWithPrivateStaticField {
static #PRIVATE_STATIC_FIELD;
static basePublicStaticMethod() {
this.#PRIVATE_STATIC_FIELD = 42;
return this.#PRIVATE_STATIC_FIELD;
}
}
class SubClass extends BaseClassWithPrivateStaticField {}
assertThrows(() => SubClass.basePublicStaticMethod(), TypeError);
2.2 私有实例字段
私有实例字段使用 #名称(发音为“哈希名称”)声明,这些名称以 #
开头。 #
是名称本身的一部分, 声明和访问时也需要加上。
封装由语言强制执行。 从作用域之外引用#名称是语法错误。
class ClassWithPrivateField {
#privateField;
constructor() {
this.#privateField = 42;
this.#randomField = 666; // Syntax error
}
}
const instance = new ClassWithPrivateField();
instance.#privateField === 42; // Syntax error
2.3 私有方法
2.3.1 私有静态方法
像它们的公有等价方法一样,私有静态方法是在类本身而非类的实例上调用的。 像私有静态字段一样,只能从类声明内部访问它们。 私有静态方法可能是生成器方法,异步方法和异步生成器方法。
class ClassWithPrivateStaticMethod {
static #privateStaticMethod() {
return 42;
}
static publicStaticMethod1() {
return ClassWithPrivateStaticMethod.#privateStaticMethod();
}
static publicStaticMethod2() {
return this.#privateStaticMethod();
}
}
assert(ClassWithPrivateStaticField.publicStaticMethod1() === 42);
assert(ClassWithPrivateStaticField.publicStaticMethod2() === 42);
使用this
可能会导致意想不到的行为(因为this
绑定规则适用)。
class Base {
static #privateStaticMethod() {
return 42;
}
static publicStaticMethod1() {
return Base.#privateStaticMethod();
}
static publicStaticMethod2() {
return this.#privateStaticMethod();
}
}
class Derived extends Base {}
console.log(Derived.publicStaticMethod1()); // 42
console.log(Derived.publicStaticMethod2()); // TypeError
2.3.2 私有实例方法
私有实例方法是类实例上可用的方法,它们的访问方式与私有实例字段相同。
class ClassWithPrivateMethod {
#privateMethod() {
return "hello world";
}
getPrivateMessage() {
return this.#privateMethod();
}
}
const instance = new ClassWithPrivateMethod();
console.log(instance.getPrivateMessage());
// expected output: "hello world"
私有实例方法可以是生成器方法,异步方法或异步生成器方法。 私有的 getter 和 setter 也是可能的:
class ClassWithPrivateAccessor {
#message;
get #decoratedMessage() {
return `✨${this.#message}✨`;
}
set #decoratedMessage(msg) {
this.#message = msg;
}
constructor() {
this.#decoratedMessage = "hello world";
console.log(this.#decoratedMessage);
}
}
new ClassWithPrivateAccessor();
// expected output: "✨hello world✨"
3. Promise.allSettled
该Promise.allSettled()
方法返回一个在所有给定的 promise 都已经fulfilled
或rejected
后的 promise,并带有一个对象数组,每个对象表示对应的 promise 结果。
当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise
的结果时,通常使用它。
相比之下,Promise.all()
更适合彼此相互依赖或者在其中任何一个reject
时立即结束。
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) =>
setTimeout(reject, 100, "foo")
);
const promises = [promise1, promise2];
Promise.allSettled(promises).then((results) =>
results.forEach((result) => console.log(result.status))
);
// expected output:
// "fulfilled"
// "rejected"
4. 可选链操作符
可选链操作符( ?.
)允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?.
操作符的功能类似于 .
链式操作符,不同之处在于,在引用为空(nullish ) (null
或者 undefined
) 的情况下不会引起错误,该表达式短路返回值是 undefined
。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined
。
当尝试访问可能不存在的对象属性时,可选链操作符将会使表达式更短、更简明。在探索一个对象的内容时,如果不能确定哪些属性必定存在,可选链操作符也是很有帮助的。
const adventurer = {
name: "Alice",
cat: {
name: "Dinah",
},
};
const dogName = adventurer.dog?.name;
console.log(dogName);
// undefined
console.log(adventurer.someNonExistentMethod?.());
// undefined
通过连接的对象的引用或函数可能是 undefined
或 null
时,可选链操作符提供了一种方法来简化被连接对象的值访问。
比如,思考一个存在嵌套结构的对象 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
。
这等价于以下表达式,但实际上没有创建临时变量:
let temp = obj.first;
let nestedProp = temp === null || temp === undefined ? undefined : temp.second;
4.1 可选链与函数调用
当尝试调用一个可能不存在的方法时也可以使用可选链。这将是很有帮助的,比如,当使用一个 API 的方法可能不可用时,要么因为实现的版本问题要么因为当前用户的设备不支持该功能。
函数调用时如果被调用的方法不存在,使用可选链可以使表达式自动返回undefined
而不是抛出一个异常。
let result = someInterface.customMethod?.();
注意:
- 如果存在一个属性名且不是函数, 使用 ?. 仍然会产生一个 TypeError 异常 (x.y is not a function).
- 如果 someInterface 自身是 null 或者 undefined ,异常 TypeError 仍会被抛出 someInterface is null 如果你希望允许 someInterface 也为 null 或者 undefined ,那么你需要像这样写 someInterface?.customMethod?.()
4.1.1 处理可选的回调函数或者事件处理器
如果使用解构赋值来解构的一个对象的回调函数或 fetch 方法,你可能得到不能当做函数直接调用的不存在的值,除非你已经校验了他们的存在性。使用?.
的你可以忽略这些额外的校验:
// ES2019的写法
function doSomething(onContent, onError) {
try {
// ... do something with the data
} catch (err) {
if (onError) {
// 校验onError是否真的存在
onError(err.message);
}
}
}
// 使用可选链进行函数调用
function doSomething(onContent, onError) {
try {
// ... do something with the data
} catch (err) {
onError?.(err.message); // 如果onError是undefined也不会有异常
}
}
4.2 可选链和表达式
当使用方括号与属性名的形式来访问属性时,你也可以使用可选链操作符:
let nestedProp = obj?.["prop" + "Name"];
4.3 可选链不能用于赋值
let object = {};
object?.property = 1; // Uncaught SyntaxError: Invalid left-hand side in assignment
4.4 可选链访问数组元素
let arrayItem = arr?.[42];
5. 动态 imoprt 导入
标准用法的 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");
6. globalThis 对象
全局属性 globalThis
包含全局的 this
值,类似于全局对象(global object)。
function canMakeHTTPRequest() {
return typeof globalThis.XMLHttpRequest === "function";
}
console.log(canMakeHTTPRequest());
// (in a browser): true
6.1 示例
在 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 方法!
}