[本文持续更新,收藏不亏,最近一次于 2021-10-01 更新]
在工作中,我们最常用的特性基本都包含在 ES 2015(ES 6)那个版本里面了,事实上,最近几年来,有很多实用的新语法加入到了 JavaScript 中来,今天我们从 ES 2019 开始,按照时间的顺序,一齐打包把他们都介绍了。
ps: ES 2022 要发布的特性还没有完全确定,故 ES 2022 介绍的内容是截止自今天(2021年7月18日)。
ES 2019
1. Optional catch binding
以前我们写 try...catch 需要这么写,不管我们需不需要变量 e,都得写上:
try {
...
} catch(e) {
console.log(e)
}
现在,catch 绑定变量是可选的了,可以简写成:
try {
...
} catch {
// catch 简写了
}
说到这里,吐槽一句支付宝小程序,这个语法都还不支持。
2. Symbol.prototype.description
const s = Symbol('foo');
console.log(s.description); // foo
const s1 = Symbol();
console.log(s1.description); // undefined
这个属性是只读的,不存在的话会返回 undefined。
3. Function.prototype.toString
以前此方法的返回值没有一个标准,完全按照浏览器厂商的喜好来,现在标准要求返回函数的源代码:
function foo() {
console.log('hi')
}
foo.toString() // "function foo() {console.log('hi')}"
4. Object.fromEntries
此方法接受一个实现了 iterable 接口的对象作为入参,比如 Map 、Array。您也可以让自己的自定义对象实现 iterable 接口,同样可以使用此方法。
Object.fromEntries 和 Object.entrie 作用正好是相反的,把它俩放在一块看一下:
const entries = new Map([
['key1', 'val1'],
['key2', 'val2']
])
const obj = Object.fromEntries(entries)
console.log(obj)// {key1: "val1", key2: "val2"}
const map = Object.entries(obj)
console.log(map) // 和 entries 的值一样
5. Array.prototype.flat
它在不影响原数组的基础上,返回一个「拍平」了的新数组:
[1, 2, [3]].flat() // [1, 2, 3]
它还可以接受一个入参,指定深度是多少,不传的话,深度默认是 1,于是乎,就会产生下面这样的结果:
[1, 2, [3, [4, 5]]].flat() // [1, 2, 3, [4, 5]]]
[1, 2, [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5]
如果你是个猛男,不管多少层,都拍成一维的,可以直接入参 Infinity:
[1, 2, [3, [4, 5, [6, 7]]]].flat(Infinity) // [1, 2, 3, 4, 5, 6, 7]
6. Array.prototype.flatMap
人如其名,它的效果就是 Array.flat + Array.map,理解它的最好方式就是举个例子:
let arr = [1, 2, 3, 4];
arr.flatMap(x => [x*2]) // [2, 4, 6, 8]
// 最后的结果就相当于
arr.map(x => [x*2]).flat() // [2, 4, 6, 8]
虽然得到的结果是一样的,但是这个函数实现方式要比 map + flat 的方式要高效,因为写成一个方法,我们可以只遍历一遍就做到这件事。
7. String.prototype.trimStart & String.prototype.trimEnd
这个是比较简单的,直接看示例就好:
const str = ' Hello World '
str.trimStart() // "Hello World "
str.trimEnd() // " Hello World"
ES 2020
8. String.prototype.matchAll
这个函数入参的类型是正则,返回的是一个实现 iterator 接口的对象,我们比较一下它和 String.prototype.match 的用法:
9. 动态 import 引入
以前限于 ES Module 的实现原理,是不支持动态引入的,我有一篇旧文,涉及到了它的一部分原理,但是现在可以了:
// filename: a.js
export function hello() {
console.log('Hello World!')
}
import('./a.js').then(module => {
console.log(module.hello()); // Hello World !
})
10. BigInt
以前在 JavaScript 中,最大的安全整数是 2^53 - 1,所以会有下面的现象:
如果我们现在使用 BigInt 去存储就不会有这样的问题了:
11. Promise.allSettled
它和 Promise.all 的功能比较相近,但是 Promise.all 有短路原则,会在一个 Promise 对象进入 rejected 态后就结束了。但是 Promise.allSettled 没有任何短路原则,都解决了才会结束。
var myPromiseArray = [Promise.resolve(), Promise.reject(new Error())]
Promise.all(myPromiseArray).then(
res => console.log(res),
err => console.error('出现了一些错误'),
);
但是,Promise.allSettled 的表现如下:
Promise.allSettled(myPromiseArray).then(
res => console.log(res),
err => console.error('出现了一些错误'),
);
12. globalThis
在浏览器中,window 代指全局对象,Node 环境中,global 代指全局对象,webWorker 中,self 代指全局对象......
如果我们的代码需要在不同的平台运行,为了获取全局变量,就得考虑各个平台下的兼容性,就得做一些比较麻烦的判断,就比如在 Underscore.js 的源码中,就有这么一段折磨人的代码:
var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global) ||
this;
这就是引入 globalThis 的目的,现在可以一个变量走天下了。
13. Optional Chaining(a?.value)
var fooValue = myForm.querySelector('input[name=foo]')?.value
就相当于原来的:
var fooInput = myForm.querySelector('input[name=foo]')
var fooValue = fooInput ? fooInput.value : undefined
这个特性真的好评。
同时,在函数中也可以用,比如我们想调用 foo 这个函数,但是 foo 可能是 null 或者 undefined,以前的话,我们可能这样子做:
if (foo) {
foo()
}
但是现在我们有了更方便的写法:
foo?.();
14. Nullish coalescing Operator(??)
只有在值为 null 或者 undefined 的时候才取后面的值。具体请看下面的示例:
15. import.meta
// filename: index.html
<script type="module" src="path/to/some.js"></script>
// filename: index.js
console.log(import.meta); // { url: "file://path/to/some.js" }
// filename: index.mjs
import './index2.mjs?someURLInfo=5';
// filename: index2.mjs
new URL(import.meta.url).searchParams.get('someURLInfo'); // 5
虽然我知道语法,但是对它的用途还不是特别理解。
ES 2021
16. String.prototype.replaceAll
之前,如果我们想取代一个字符串所有的字符,一般使用正则去做:
const queryString = 'q=query+string+parameters';
const withSpaces = queryString.replace(/+/g, ' ');
现在有了此方法,我们可以:
const queryString = 'q=query+string+parameters';
const withSpaces = queryString.replaceAll('+', ' ');
17. Promise.any
这个可以和之前的 Promise.race 进行比较,二者都有短路原则,Promise.race 会在某一个 Promise 被解决的时候完成,而 Promise.any 会在某一个 Promise 处于 fulfilled 态的时候完成。有点绕,还是举例子:
var promise1 = new Promise((resolve, reject)=> {
setTimeout(() => {
reject('rejected')
}, 100)
})
var promise2 = new Promise((resolve, reject)=> {
setTimeout(() => {
resolve('resolved')
}, 200)
})
首先,看 promise.race 的结果:
Promise.race([promise1, promise2]).then(res => {
console.log(res)
}, (error) => {
console.error(error)
})
接下来,看 Promise.any 的结果:
Promise.any([promise1, promise2]).then(res => {
console.log('我没有错误')
console.log(res)
}, (error) => {
console.error(error)
})
18. WeakRefs
最简单的使用就是像下面这样:
var foo = () => {console.log('hi')};
var weakFoo = new WeakRef(foo);
console.log(weakFoo.deref()) // () => {console.log('hi')}
WeakRef 的入参是一个对象,它会创建对这个对象的弱引用,并且不会阻止这个对象的垃圾回收。但是鉴于浏览器的垃圾回收机制并不统一,所以它的表现可能也会因为浏览器的不同而不同,建议是能别用就别用。
weakFoo.deref 用来取得指向的原对象,如果还没有被回收,就返回原对象,回收了就返回 undefined。
19. Logical Assignment Operators
x &&= y // 相当于 x && (x = y)
x ||= y // 相当于 x || (x = y)
x ??= y // 相当于 x ?? (x = y)
个人觉得第 1、3 个很实用。
20. Numeric separators
现在数字有了更方便阅读的书写方式:
const a = 1_000
console.log(a) // 1000
const b = 1_000_000
console.log(b) // 1000000
ES 2022
21. Class Fields
历尽艰辛,私有类的提案终于在这个版本被通过了,里面包括了私有变量、私有方法、静态私有变量、静态私有方法四个。
设置私有属性的方式是在变量前面加一个修饰符 #:
class ClassWithPrivateProperty {
#privateField;
static #PRIVATE_STATIC_FIELD;
constructor() {
this.#privateField = 42;
}
#privateMethod() {
return 'hello world';
}
static #privateStaticMethod() {
return 'hello world';
}
}
值得注意的是,在当前版本的 TypeScript 中,我们的私有属性使用 private 这一个修饰符,但是这个修饰符只会在我们写代码的时候限制我们,真正转译成 JavaScript 代码的时候,还是公共属性。
22. RegExp Match Indices
以前我们的正则表达式后面可以加 i 和 g 去修饰,i 代表忽略大小写,g 代表全局匹配,现在又加了一个 d:
const re1 = /a+(?<Z>z)?/d;
const s1 = "xaaaz";
const m1 = re1.exec(s1);
m1.indices[0][0] === 1;
m1.indices[0][1] === 5;
s1.slice(...m1.indices[0]) === "aaaz";
re1 这个正则表达式中,有个 (?<Z>z) 的写法,它是什么意思呢?它会匹配 z 这个字符,并把匹配到的结果放到 groups 这个对象的 Z 属性里。更多资料请参阅Groups_and_Ranges
23. Top-level await
以前我们使用 await 时,必须要放到 async 函数里,这就限制了一些场景,比如我们在全局作用域使用 import 的异步加载方式。而这个特性就是为这些场景提供了便利:
let jQuery;
try {
jQuery = await import('https://cdn-a.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.com/jQuery');
}
const strings = await import(`/i18n/${navigator.language}`);
24. Ergonomic brand checks for Private Fields
支持了使用 in 去判断私有属性在对象里面存不存在。拿例子说话比较好:
class C {
#brand;
#method() {}
get #getter() {}
static isC(obj) {
return #brand in obj && #method in obj && #getter in obj;
}
}
那它的应用场景是什么呢?
假设我们有一个创造类的工厂方法,并创造了两个类:
function createClass() {
return class {
#name;
constructor(name) {
this.#name = name;
}
static getName(inst) {
return inst.#name;
}
};
}
const Class1 = createClass()
const Class2 = createClass()
这两个类虽然函数的属性是一样的,但是每一个类的私有属性都是独立的,如果我们这样使用就会报错:
Class1.getName(new Class2('小明'))
为了兼容这种场景,就有了这个提案,我们可以像下面这解决:
function createClass() {
return class {
#name;
constructor(name) {
this.#name = name;
}
static getName(inst) {
if (#name in inst) {
return inst.#name;
}
return undefined;
}
};
}
25. .at()
这个就是取数组索引的:
var a = [1, 2, 3];
a.at(1) // 2
a.at(-1) // 3
因为 JavaScript 数组的特殊性(一个特殊的对象),我们无法通过 a[-1] 这种形式取到数组的倒数第一项,所以有了这个语法。
26. Accessible Object.prototype.hasOwnProperty`
因为 JS 并没有保护叫做 'hasOwnProperty' 的属性名,为了防止意外的产生,现在 Eslint 一般都会默认开启不允许在对象上判断 hasOwnProperty 这条校验。
除此之外,我们有一个对象没有原型,也就不能调用 hasOwnProperty 方法了:
Object.create(null).hasOwnProperty("foo")
// Uncaught TypeError: Object.create(...).hasOwnProperty is not a function
于是乎,我们一般都这样来判断:
let hasOwnProperty = Object.prototype.hasOwnProperty
if (hasOwnProperty.call(object, "foo")) {
console.log("has property foo")
}
我自己在项目中也是这么写的,但是大家都这么写,JS 就觉得可以考虑出一个公共方法了,就有了 hasOwn 方法,可以比较大的简化我们上面的写法:
if (Object.hasOwn(object, "foo")) {
console.log("has property foo")
}
27. Class Static Block
以前,我们初始化类的静态成员变量只能在定义的时候去做,不能放到构造函数里面(静态方法不用实例化,就不用调用构造函数了)。
现在,我们可以在类内部开辟一个专门为静态成员初始化的作用域,这对一些比较复杂的场景很适用:
在没有作用域之前,我们可能会使用一个工具函数去初始化:
class Translator {
static translations = {
yes: 'ja',
no: 'nein',
maybe: 'vielleicht',
};
static englishWords = [];
static germanWords = [];
static _ = initializeTranslator(); // (A)
}
function initializeTranslator() {
for (const [english, german] of Object.entries(Translator.translations)) {
Translator.englishWords.push(english);
Translator.germanWords.push(german);
}
}
有了类的静态作用域之后,我们就可以按如下的格式了:
class Translator {
static translations = {
yes: 'ja',
no: 'nein',
maybe: 'vielleicht',
};
static englishWords = [];
static germanWords = [];
static { // (A)
for (const [english, german] of Object.entries(this.translations)) {
this.englishWords.push(english);
this.germanWords.push(german);
}
}
}
到这里,除却一些优化 API 的提案(数组排序优化、for...in 顺序等),我已经把 ES2019 - 2022 通过的大部分提案都介绍了,后续有了新的,我也会补充到这个文章里来。
谢谢阅读 ~