ES11、ES12、ES13 新特性概览

传送门:ES6、 ES7、 ES8、 ES9、 ES10 新特性概览

1. ES11 新特性(2020)

1.1 动态 import ()

用了实现按需导入, import()是一个类似函数的语法关键字,类似super(),它接收一个字符串作为模块标识符,并返回一个 promise

在 ES 2015 定义的模块语法中,所有模块导入语法都是静态声明的:

import aExport from "./module"
import * as exportName from "./module"
import {
    
     export1, export2 as alias2 } from "./module"
import "./module"

虽然这套语法已经可以满足绝大多数的导入需求,而且还可以支持实现静态分析以及树抖动等一系列重要的功能。但却无法满足一些需要动态导入的需求。例如:

  • 需要根据浏览器兼容性有选择地加载一些支持库;
  • 在实际需要时才加载某个模块的代码;
  • 只是单纯地希望延迟加载某些模块来以渐进渲染的方式改进加载体验;

若没有动态导入,将难以实现这些需求。虽然可以通过创建 script 标签来动态地导入某些脚本,但这是特定于浏览器环境的实现方式,也无法直接和现有的模块语法结合在一起使用,所以只能作为内部实现机制,但不能直接暴露给模块的使用者。

但是动态 import () 解决了这个问题。它可在任何支持该语法的平台中使用,比如 webpack、node 或 浏览器环境。并且模块标识符的格式则是由各平台自行指定,如: webpack 及 node 支持使用模块名直接加载 node_modules 中的模块,而浏览器支持使用 url 加载远程模块。

import('lodash').then(_ => {
    
    
    // other
})

当模块及其所依赖的其它模块都被加载并执行完毕后,promise 将进入fulfilled 状态,结果值便是包含该模块所有导出内容的一个对象:具名导出项被放在该对象的同名属性中,而默认导出项则放在名为 default 的属性中。
如下模块 utils,导入方式如下:

// utils
export default 'hello lxm';
export const x = 11;
export const y = 22;

// 导入
import('a').then(module => {
    
    
    console.info(module)
})

// 结果:
{
    
    
   default: 'hello lxm'',
   x: 11,
   y: 22,
}

如果因为模块不存在或无法访问等问题导致模块加载或执行失败,promise 便会进入rejected 状态,可在其中执行一些回退处理。

1.2 空值合并运算符(?? )

大家可能遇到过,如果一个变量是空,需要给它赋值为一个默认值的情况。通常我们会这样写:

let num = number || 222

但是,以上的代码会有一个 bug。如果 number 的值是0,则会被当作取不到其值,会取到’无法获取’这个字符串。
如果想要做到这一点,在这之前就只能使用三元运算符来实现:

let num = (number !== undefined) ? number : 222

但现在可以使用了 ?? 运算符了,它只有当操作符左边的值是 null 或 undefined 的时候,才会取操作符右边的值:

let num = number ?? 222
const number = 0;
let num1 = number || 111; // 111
let num2 = number ?? 222; // 0

该运算符也支持短路特性:

const x = a ?? getDefaultValue()
// 当 `a` 不为 `undefined` 或 `null` 时,`getDefaultValue` 方法不会被执行

注意: ??运算符不能与 and 或 or 运算符共用,否则会抛出语法异常:

 a && b ?? "default"    // SyntaxError

这种代码的歧义比较严重,在不同人的理解中,有的人觉得按 (a && b) ?? “default” 运行是合理的,而另外一些人却觉得按 a && (b ?? “default”)运行才对,因此在设计该运算符时就干脆通过语法上的约束来避免了这种情况。
如果确实需要在同一个表达式中同时使用它们,那么使用括号加以区分即可:

(a && b) ?? "default"

1.3 可选链操作符(?. )

当需要尝试访问某个对象中的属性或方法而又不确定该对象是否存在时,该语法可以极大的简化代码。
比如下面这种情况:

const el = document.querySelector(".class-a")
const height = el.clientHeight

当并不知道页面中是否有一个类名为 class-a 的元素,因此在访问 clientHeight 之前为了防止 bug 产生需要先进行一些判断:

const height = el ? el.clientHeight : undefined

上面的写法虽然可以实现,但是的确有人会觉得麻烦,而使用可选链操作符 ,就可以将代码简化成如下形式:

const height = el?.clientHeight

下面介绍常用的使用场景:

属性访问
需要获取某个对象中的属性,就都可以使用该语法:

obj?.label
obj?.[value]

上面的代码中,如果 obj 为 undefined 或 null,则表达式会立即返回 undefined,否则返回所访问属性的值。
也就是说,它们与下面这段代码是等价的:

obj == null ? undefined : obj.label
obj == null ? undefined : obj.[value]

方法调用
在尝试调用某个方法时,也可以使用该语法:

a?.()

如果 a 为 undefined 或 null,则返回 undefined,否则将调用该方法。不过需要额外注意的是,该操作符并不会判断 a 是否是函数类型,因此如果 a 是一个其它类型的值,那么这段代码依然会在运行时抛出异常。

访问深层次属性
在访问某个对象较深层级的属性时,也可以串联使用该操作符:

a?.b?.[0]?.()?.d

可能有人会懒得先去判断是否真的有必要,就给访问链路中的每个属性都加上该操作符。但类似上面代码中所展示的那样,这种代码可读性比较差。而且若真的有一个应当存在的对象因为某些 bug 导致它没有存在,那么在访问它时就应当是抛出异常,这样可以及时发现问题,而不是使它被隐藏起来。建议只在必要的时候才使用可选链操作符。

1.4 BigInt

在 ES 中,所有 Number 类型的值都使用 64 位浮点数格式存储,因此 Number 类型可以有效表示的最大整数为 2^53。而使用新的 BigInt 类型,可以操作任意精度的整数。

有两种使用方式:1、在数字字面量的后面添加后缀 n;2、使用其构造函数 BigInt;

const bigInt = 9007199254740993n
const bigInt = BigInt(9007199254740992)

// 在超过 Number 最大整数限制时,我们也可以改为传入一个可能被正确解析的字符串
const bigInt = BigInt('9007199254740993')

和 Number 类似,BigInt 也支持 +、-、*、**、% 运算符:

3n + 2n    // => 5n
3n * 2n    // => 6n
3n ** 2n   // => 9n
3n % 2n    // => 1n

但因为 BigInt 是纯粹的整数类型,无法表示小数位,因此 BigInt 的除法运算(/)的结果值依然还是一个整数,即向下取整:

const bigInt = 3n;
bigInt / 2n; // => 1n,而不是 1.5n

同样也位支持位运算符,除了无符号右移运算符:

1n & 3n    // => 1n
1n | 3n    // => 3n
1n ^ 3n    // => 2n
~1n        // => -2n
1n << 3n   // => 8n
1n >> 3n   // => 0n

1n >>> 3n  // Uncaught TypeError: BigInts have no unsigned right shift, use >> instead

在这里插入图片描述
BigInt 可以和字符串之间使用+运算符连接

1n + ' Number'   // => 1 Number
'Number ' + 2n   // => Number 2

下面这些场景不支持使用BigInt:

1、BigInt 无法和 Number 一起运算,会抛出类型异常

1n + 1
// Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

2、一些内置模块如 Math 也不支持 BigInt,同样会抛出异常

Math.pow(2n, 64n)
// Uncaught TypeError: Cannot convert a BigInt value to a number

3、BigInt 和 Number 相等,但并不严格相等,但他们之间可以比较大小

1n == 1    // => true
1n === 1   // => false
1n < 2     // => true
1n < 1     // => false
2n > 1     // => true
2n > 2     // => false

而且在转换为 Boolean 值时,也和 Number 一样,0n 转为 false,其它值转为 true:

!!0n       // => false
!!1n       // => true

另外两者之间只能使用对方的构造函数进行转换:

Number(1n) // => 1
BigInt(1)  // => 1n

但两者之间的转换也都有一些边界问题:

// 当 BigInt 值的精度超出 Number 类型可表示的范围时,会出现精度丢失的问题
Number(9007199254740993n)
// => 9007199254740992

// 当 Number 值中有小数位时,BigInt 会抛出异常
BigInt(1.1)
// VM4854:1 Uncaught RangeError: The number 1.1 cannot be converted to a BigInt because it is not an integer

配套地,在类型化数组中也提供了与 BigInt 对应的两个数组类型:BigInt64Array 和 BigUint64Array:

const array = new BigInt64Array(4);

array[0]   // => 0n

array[0] = 2n
array[0]   // => 2n

但因为每个元素限定只有 64 位,因此即便使用无符号类型,最大也只能表示 2^64 - 1:

const array = new BigUint64Array(4);

array[0] = 2n ** 64n
array[0]   // => 0n

array[0] = 2n ** 64n - 1n
array[0]   // => 18446744073709551615n

1.5 globalThis

浏览器:window、worker:self、node:global

在浏览器环境中,我们可以有多种方式访问到全局对象,最常用到的肯定是 window,但除此之外还有 self,以及在特殊场景下使用的 frames、paraent 以及 top。
我们通常不怎么需要关心 window 和 self 之间的区别,但如果使用 Web Worker,那就应当了解 window 是只在主线程中才有的全局属性,在 Worker 线程中,我们需要改为使用 self。

而在 node.js 环境中,我们需要使用 global,至于像 JSC.js 这种更小众的环境中,则需要使用 this。
在一般的开发工作中,可能很少需要访问全局环境,而且大多时候也只需要基于一种环境进行开发,所以不太需要处理这种麻烦的问题。但是对于 es6-shim 这种需要支持多种环境的基础库来说,它们需要解决这个问题。

早先,我们可以通过下面这段代码较为方便地拿到全局对象:

const globals = (new Function('return this;'))()

但受到 Chrome APP 内容安全策略的影响(为缓解跨站脚本攻击的问题,该政策要求禁止使用 eval 及相关的功能),上面这段代码将无法在 Chrome APP 的运行环境中正常执行。

无奈之下,像 es6-shim 这种库就只能穷举所有可能的全局属性:

var getGlobal = function () {
    
    
    // the only reliable means to get the global object is
    // `Function('return this')()`
    // However, this causes CSP violations in Chrome apps.
    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 (!globals.Reflect) {
    
    
    defineProperty(globals, 'Reflect', {
    
    }, true);
}

这种问题等真的遇到了,每次处理起来也是很麻烦的。所以才有了这次提案中的 globalThis。
通过 globalThis,终于可以使用一种标准的方法拿到全局对象,而不用关心代码的运行环境。
对于 es6-shim 这种库来说,这是一个极大的便利特性:

if (!globalThis.Reflect) {
    
    
    defineProperty(globalThis, 'Reflect', {
    
    }, true);
}

另外,关于 globalThis 还有一些细节的问题,比如为满足 Secure ECMAScript 的要求,globalThis 是可写的。而在浏览器页面中,受到 outer window 特性的影响,globalThis 实际指向的是 WindowProxy,而不是当前页面内真实的全局对象(该对象不能被直接访问)。

1.6 Promise.allSettled

在 Promise 上有提供一组组合方法(如最常用到的 Promise.all),它们都是接收多个 promise 对象,并返回一个表示组合结果的新的 promise,依据所传入 promise 的结果状态,组合后的 promise 将切换为不同的状态。

目前为止这类方法一共有如下四个,这四个方法之间仅有判断逻辑上的区别,也都有各自所适用的场景:

  • Promise.all 返回一个组合后的 promise,当所有 promise 全部切换为 fulfilled 状态后,该 promise 切换为 fulfilled 状态;但若有任意一个 promise 切换为 rejected 状态,该 promise 将立即切换为 rejected 状态;
  • Promise.race 返回一个组合后的 promise,当 promise 中有任意一个切换为 fulfilled 或 rejected 状态时,该 promise 将立即切换为相同状态;
  • Promise.allSettled 返回一个组合后的 promise,当所有 promise 全部切换为 fulfilled 或 rejected 状态时,该 promise 将切换为 fulfilled 状态;
  • Promise.any 返回一个组合后的 promise,当 promise 中有任意一个切换为 fulfilled 状态时,该 promise 将立即切换为 fulfilled 状态,但只有所有 promise 全部切换为 rejected 状态时,该 promise 才切换为 rejected 状态。

Promise.allSettled用法:

传入一个数组,里面放任意多个 promise 对象,并接受一个表示组合结果的新的 promise。
注意:组合后的 promise 会等待所有所传入的 promise,当它们全部切换状态后(无论是 fulfilled 状态 还是 rejected 状态),这个组合后的 promise 会切换到 fulfilled 状态并给出所有 promise 的结果信息:

async function a() {
    
    
    const promiseA = fetch('/api/a')    // => rejected,  <Error: a>
    const promiseB = fetch('/api/B')    // => fulfilled, "b"
    const results = await Promise.allSettled([ promiseA, promiseB])

    results.length   // => 3
    results[0]       // => { status: "rejected", reason: <Error: a> }
    results[1]       // => { status: "fulfilled", value: "b" }
}

因为结果值是一个数组,所以可容易地过滤出任何你感兴趣的结果信息:

// 获取所有 fulfilled 状态的结果信息
results.filter( result => result.status === "fulfilled" )
// 获取所有 rejected 状态的结果信息
results.filter( result => result.status === "rejected" )
// 获取第一个 rejected 状态的结果信息
results.find( result => result.status === "rejected" )

使用场景如下:

  • 在进行页面的初始化流程时,需加载多份初始化数据,或执行一些其它初始化操作,而且通常会希望等待这些初始化操作全部完成之后再执行后续流程:
async function init() {
    
    
    setInited(false)
    setInitError(undefined)

    const results = await Promise.allSettled([
        loadDetail(),
        loadRecommentListFirstPage(),
        initSDK(),
    ])

    const errors = results
        .filter( result => result.status === "rejected" )
        .map( rejectedResult => rejectedResult.reason )

    if (errors.length) {
    
    
        setInitError(errors[0])
        $logs.error(errors)
    }

    setInited(true)
}
  • 如有自定义的全局消息中心,那么还可以基于 allSettled 作一些异步支持的事情。如:在打开登录弹出层并在用户成功登录后,向页面中广播一个 login 事件,通常页面中其它地方监听到该事件后需要向服务端请求新的数据,此时可能需要等待数据全部更新完毕之后再关闭登录弹出层:
async function login() {
    
    
    // goto login ...

    const results = messageCenter.login.emit()
    const promiseResults = results.filter(isPromise)

	if (promiseResults.length) {
    
    
        await Promise.allSettled(promiseResults)
	}

    closeLoginModal()
    closeLoading()
}

1.7 for-in 结构

用于规范 for-in 语句的遍历顺序

在之前的 ES 规范中几乎没有指定 for-in 语句在遍历时的顺序,但各 ES 引擎的实现在大多数情况下都是趋于一致的,只有在一些边界情况时才会有所差别。我们很难能够要求各引擎做到完全一致,主要原因在于 for-in 是 ES 中所有遍历 API 中最复杂的一个,再加上规范的疏漏,导致各大浏览器在实现该 API 时都有很多自己特有的实现逻辑,各引擎的维护人员很难有意愿去重新审查这部分的代码。

因此规范的作者作了大量的工作,去测试了很多现有的 ES 引擎中 for-in 的遍历逻辑。并梳理出了它们之间一致的部分,然后将这部分补充到了 ES 规范 当中。

另外,规范中还提供了一份示例代码,以供各引擎在实现 for-in 逻辑时参考使用,大家可以看一下:

function* EnumerateObjectProperties(obj) {
    
    
    const visited = new Set();

    for (const key of Reflect.ownKeys(obj)) {
    
    
        if (typeof key === "symbol") continue;
        const desc = Reflect.getOwnPropertyDescriptor(obj, key);
        if (desc) {
    
    
            visited.add(key);
            if (desc.enumerable) yield key;
        }
    }

    const proto = Reflect.getPrototypeOf(obj);
    if (proto === null) return;

    for (const protoKey of EnumerateObjectProperties(proto)) {
    
    
        if (!visited.has(protoKey)) yield protoKey;
    }
}

2. ES12 新特性 (2021)

2.1 replaceAll

模式的所有匹配都会被替代项替换。模式可以是字符串或正则表达式,而替换项可以是字符串或针对每次匹配执行的函数。并返回一个全新的字符串

在没有这个特性之前,我们会这样写

const str = "2022-07-12";
const newStr = str.replace(/-/g, "/");
console.log(newStr); // 2022/07/12

有了 replaceAll 之后,我们这么写

const str = "2022-07-12";
const newStr = str.replaceAll('-', '/');
console.log(newStr); // 2022/07/12

2.2 Promise.any

可以把 Promise.any 理解成 Promise.all 的相反操作。Promise.any 也可以接受一个 Promise 数组,当其中任何一个 Promise 完成(fullfill)时,就返回那个已经有完成值的 Promise。如果所有的 Promise 都拒绝(reject),则返回一个拒绝的 Promise,该 Promise 的返回值是一个 AggregateError 对象。

如下面有三个 Promise 请求正常情况下:

const p1 = new Promise((resolve, reject) => {
    
    
  setTimeout(() => resolve("A"), Math.floor(Math.random() * 1000));
});
const p2 = new Promise((resolve, reject) => {
    
    
  setTimeout(() => resolve("B"), Math.floor(Math.random() * 1000));
});
const p3 = new Promise((resolve, reject) => {
    
    
  setTimeout(() => resolve("C"), Math.floor(Math.random() * 1000));
});

(async function () {
    
    
  const result = await Promise.any([p1, p2, p3]);
  console.log(result); // 输出结果可能是 "A", "B" 或者 "C"
})();

如果存在某个 Promise 发生错误的情况:

const p = new Promise((resolve, reject) => reject());

try {
    
    
  (async function () {
    
    
    const result = await Promise.any([p]);
    console.log(result);
  })();
} catch (error) {
    
    
  console.log(error.errors);
}

在这里插入图片描述

2.3 逻辑赋值操作符 ??=、&&=、 ||=

逻辑空赋值(??=),逻辑空赋值运算符 (x ??= y) 仅在 x 是 (null 或 undefined) 时对其赋值

let a = {
    
     duration: 50, title: null };
a.duration ??= 10;
a.title ??= 'title is empty';
console.log(a.duration,a.title ); // 50 'title is empty'

逻辑与赋值(&&=),( x &&= y ) 仅仅当 x 为 true 时起作用,对其赋值

let a = {
    
     duration: 50, title: '' };
a.duration &&= 10;
a.title &&= 'title is empty';
console.log(a.duration,a.title ); // 10 ''

逻辑或赋值(||= ),逻辑或赋值运算符 (x ||= y) 是在 x 是 false 时对其赋值

let a = {
    
     duration: 50, title: '' };
a.duration ||= 10;
a.title ||= 'title is empty';
console.log(a.duration,a.title ); // 50 'title is empty'

2.4 WeakRef

WeakRef 是一个 Class,一个 WeakRef 对象可以让你拿到一个对象的弱引用。这样,就可以不用阻止垃圾回收这个对象了。
可以使用其构造函数来创建一个 WeakRef 对象。

// anObject 不会因为 ref 引用了这个对象,而不会被垃圾回收
let ref = new WeakRef(anObject);

我们可以用 WeakRef.prototype.deref() 来取到 anObject 的值。但是,在被引用对象被垃圾回收之后,这个函数就会返回undefined。

// 如果 someObj 被垃圾回收了,则 obj 就会是 undefined
let obj = ref.deref();

2.5 下划线 (_) 分隔符

当你要写一个很长的数字的时候,数字太长会导致可读性很差。
使用了数字分隔符 _ (下划线),就可以让数字读的更清晰:

const num = 1_100_0000
console.log(num) // 11000000

2.6 Intl.ListFormat

Intl.ListFormat 是一个构造函数,用来处理和多语言相关的对象格式化操作。

const list = ['Apple', 'Orange', 'Banana']

const enList = new Intl.ListFormat('en-GB', {
    
     style: 'long', type: 'conjunction' }).format(list);
console.log(enList) // "Apple, Orange and Banana"

const cnList =  new Intl.ListFormat('zh-cn', {
    
     style: 'short', type: 'conjunction' }).format(list);
console.log(cnList)// "Apple、Orange和Banana"

2.7 Intl.DateTimeFormat API 中的 dateStyle 和 timeStyle 的配置项

Intl.ListFormat 是一个用来处理多语言下的时间日期格式化的函数。
ES2021 中给这个函数添加了两个新的参数:dateStyle 和 timeStyle,可用于请求给定长度的,特定于语言环境的日期和时间。

timeStyle 和 dateStyle 配置项有三个(以 timeStyle 为例):

short:11:27 PM
medium:11:27:57 PM
long:11:27:57 PM GMT+11
const a = new Intl.DateTimeFormat("en" , {
    
     timeStyle: "short"});
console.log(a.format(Date.now())); // "13:31"
const b = new Intl.DateTimeFormat("en" , {
    
     dateStyle: "short"});
console.log(b.format(Date.now())); // "21.03.2012"

// 可以通过同时传入 timeStyle 和 dateStyle 这两个参数来获取更完整的格式化时间的字符串
const c = new Intl.DateTimeFormat("en" , {
    
    
  timeStyle: "medium",
  dateStyle: "short"
});
console.log(c.format(Date.now())); // 7/13/22, 12:14:50 AM

3. ES13 新特性 (2022)

3.1 Object.hasOwn()

在此之前,我们可以使用 Object.prototype.hasOwnProperty() 来检查一个属性是否属于该对象。
Object.hasOwn() 特性是一种更简洁、更可靠的检查属性是否直接设置在该对象上的方法:

const example = {
    
    
  name: 'leo'
};
 
console.log(Object.prototype.hasOwnProperty.call(example, 'name')); // true
// 更简介的方式
console.log(Object.hasOwn(example, 'name')); // true

**3.2 at() **

at() 是一个数组方法,用于通过给定索引来获取数组元素。
当给定索引为正数时,这种新方法与使用中括号表示法访问具有相同的行为;
当给出负整数索引时,就会从数组的最后一项开始检索;

const arr = [0,1,2,3,4,5];
 
console.log(arr[1]);    // 1
console.log(arr.at(1)); // 1
 
console.log(arr[arr.length - 1]);   // 5
console.log(arr.at(-1));   // 5
console.log(arr[arr.length - 2]);   // 4
console.log(arr.at(-2));   // 4

除了数组,字符串也同样适用:

const str = "hello world";
 
console.log(str[str.length - 3]);   // r
console.log(str.at(-3));   // r

3.3 正则表达式匹配索引

该特性允许利用 d 字符来表示想要匹配字符串的开始和结束索引。
以前,只能在字符串匹配操作期间获得一个包含提取的字符串和索引信息的数组。在某些情况下,这是不够的。因此,在这个规范中,如果设置标志 /d,将额外获得一个带有开始和结束索引的数组。

const matchObj = /(a+)(b+)/d.exec('aaaabb');
 
console.log(matchObj[1]);   // 'aaaa'
console.log(matchObj[2]);   // 'bb'

由于 /d 标识的存在,matchObj 还有一个属性 .indices,它用来记录捕获的每个编号组:

console.log(matchObj.indices[1]);   // [0, 4]
console.log(matchObj.indices[2]);   // [4, 6]

3.4 Error Cause

proposal-error-cause 这一提案,目的主要是为了便捷的传递导致错误的原因,如果不使用这个模块,想要清晰的跨越多个调用栈传递错误上下文信息,通常要这么做:

async function doJob() {
    
    
  const rawResource = await fetch('//domain/resource-a')
    .catch(err => {
    
    
      // How to wrap the error properly?
      // 1. throw new Error('Download raw resource failed: ' + err.message);
      // 2. const wrapErr = new Error('Download raw resource failed');
      //    wrapErr.cause = err;
      //    throw wrapErr;
      // 3. class CustomError extends Error {
    
    
      //      constructor(msg, cause) {
    
    
      //        super(msg);
      //        this.cause = cause;
      //      }
      //    }
      //    throw new CustomError('Download raw resource failed', err);
    })
  const jobResult = doComputationalHeavyJob(rawResource);
  await fetch('//domain/upload', {
    
     method: 'POST', body: jobResult });
}

await doJob(); // => TypeError: Failed to fetch

而按照这一提案的语法:

sync function doJob() {
    
    
  const rawResource = await fetch('//domain/resource-a')
    .catch(err => {
    
    
      throw new Error('Download raw resource failed', {
    
     cause: err });
    });
  const jobResult = doComputationalHeavyJob(rawResource);
  await fetch('//domain/upload', {
    
     method: 'POST', body: jobResult })
    .catch(err => {
    
    
      throw new Error('Upload job result failed', {
    
     cause: err });
    });
}

try {
    
    
  await doJob();
} catch (e) {
    
    
  console.log(e);
  console.log('Caused by', e.cause);
}
// Error: Upload job result failed
// Caused by TypeError: Failed to fetch

3.5 类

公共实例字段
公共类字段允许我们使用赋值运算符(=)将实例属性添加到类定义中。

import React, {
    
     Component } from "react";
 
export class Incrementor extends Component {
    
    
  constructor() {
    
    
    super();
    this.state = {
    
    
      count: 0,
    };
    this.increment = this.increment.bind(this);
  }
 
  increment() {
    
    
    this.setState({
    
     count: this.state.count + 1 });
  }
 
  render() {
    
    
    return (
      <button onClick={
    
    this.increment}>Increment: {
    
    this.state.count}</button>
    );
  }
}

在这个例子中,在构造函数中定义了实例字段和绑定方法,通过新的类语法,可以使代码更加直观。新的公共类字段语法允许我们直接将实例属性作为属性添加到类上,而无需使用构造函数方法。

import React from "react";
 
export class Incrementor extends React.Component {
    
    
  state = {
    
     count: 0 };
 
  increment = () => this.setState({
    
     count: this.state.count + 1 });
 
  render = () => (
    <button onClick={
    
    this.increment}>Increment: {
    
    this.state.count}</button>
  );
}

以下是一些公共实例字段的注意事项:

  • 公共实例字段存在于每个创建的类实例上。它们要么是在 Object.defineProperty() 中添加,要么是在基类中的构造时添加,要么在子类的 super() 返回之后添加
class Incrementor {
    
    
  count = 0
}
 
const instance = new Incrementor();
console.log(instance.count);   // 0
  • 未初始化的字段会自动设置为 undefined
class Incrementor {
    
    
  count
}
 
const instance = new Incrementor();
console.log(instance.count);   // undefined
  • 可以进行字段的计算
const PREFIX = 'main';
 
class Incrementor {
    
    
  [`${
      
      PREFIX}Count`] = 0
}
 
const instance = new Incrementor();
console.log(instance.mainCount);   // 0

私有实例字段、方法和访问器

默认情况下,ES6 中所有属性都是公共的,可以在类外检查或修改:

class TimeTracker {
    
    
  name = 'leo';
  project = 'blog';
  hours = 0;
 
  set addHours(hour) {
    
    
    this.hours += hour;
  }
 
  get timeSheet() {
    
    
    return `${
      
      this.name} works ${
      
      this.hours || 'nothing'} hours on ${
      
      this.project}`;
  }
}
 
let person = new TimeTracker();
person.addHours = 2;   // 标准 setter
person.hours = 4;   // 绕过 setter 进行设置
person.timeSheet;   // 'leo works 4 hours on blog'

可以看到,在类中没有任何措施可以防止在不调用 setter 的情况下更改属性。
而私有类字段将使用 # 前缀定义,在上面的示例中,可以修改它以包含私有类字段,以防止在类方法之外更改属性:

class TimeTracker {
    
    
  name = 'leo';
  project = 'blog';
  #hours = 0;   // 私有类字段
 
  set addHours(hour) {
    
    
    this.#hours += hour;
  }
 
  get timeSheet() {
    
    
    return `${
      
      this.name} works ${
      
      this.#hours || 'nothing'} hours on ${
      
      this.project}`;
  }
}
 
let person = new TimeTracker();
person.addHours = 4; // 标准 setter
person.timeSheet     // 'leo works 4 hours on blog'

当尝试在 setter 方法之外修改私有类字段时,就会报错:

person.hours = 4;   // Error Private field '#hours' must be declared in an enclosing class

还可以将方法或 getter/setter 设为私有,只需要给这些方法名称前面加 # 即可:

class TimeTracker {
    
    
  name = 'leo';
  project = 'blog';
  #hours = 0;   // 私有类字段
 
  set #addHours(hour) {
    
    
    this.#hours += hour;
  }
 
  get #timeSheet() {
    
    
    return `${
      
      this.name} works ${
      
      this.#hours || 'nothing'} hours on ${
      
      this.project}`;
  }
 
  constructor(hours) {
    
    
    this.#addHours = hours;
    console.log(this.#timeSheet);
  }
}
 
let person = new TimeTracker(4);   // 'leo works 4 hours on blog'

静态公共字段

在 ES6 中,不能在类的每个实例中访问静态字段或方法,只能在原型中访问。ES2022 提供了一种在 JavaScript 中使用 static 关键字声明静态类字段的方法:

class Shape {
    
    
  static color = 'blue';
 
  static getColor() {
    
    
    return this.color;
  }
 
  getMessage() {
    
    
    return `color:${
      
      this.color}` ;
  }
}

可以从类本身访问静态字段和方法:

 console.log(Shape.color);   // blue
 console.log(Shape.getColor());   // blue
 console.log('color' in Shape);   // true
 console.log('getColor' in Shape);   // true
 console.log('getMessage' in Shape);   // false

实例不能访问静态字段和方法:

const shapeInstance = new Shape();
console.log(shapeInstance.color);   // undefined
console.log(shapeInstance.getColor);   // undefined
console.log(shapeInstance.getMessage());   // color:undefined

静态字段只能通过静态方法访问:

console.log(Shape.getColor());   // blue
console.log(Shape.getMessage());   //TypeError: Shape.getMessage is not a function

这里的 Shape.getMessage() 就报错了,因为 getMessage 不是一个静态函数,所以它不能通过类名 Shape 访问。可以通过以下方式来解决这个问题:

getMessage() {
    
    
  return `color:${
      
      Shape.color}` ;
}

静态字段和方法是从父类继承的:

class Rectangle extends Shape {
    
     }   // 继承
 
console.log(Rectangle.color);   // blue
console.log(Rectangle.getColor());   // blue
console.log('color' in Rectangle);   // true
console.log('getColor' in Rectangle);   // true
console.log('getMessage' in Rectangle);   // false

** 静态私有字段和方法**

与私有实例字段和方法一样,静态私有字段和方法也使用哈希 # 前缀来定义:

class Shape {
    
    
  static #color = 'blue';
 
  static #getColor() {
    
    
    return this.#color;
  }
 
  getMessage() {
    
    
    return `color:${
      
      Shape.#getColor()}` ;
  }
}
const shapeInstance = new Shape();
shapeInstance.getMessage();   // color:blue

私有静态字段有一个限制,即只有定义私有静态字段的类才能访问该字段。这可能在使用 this 时导致出乎意料的情况:

class Shape {
    
    
  static #color = 'blue';
  static #getColor() {
    
    
    return this.#color;
  }
  static getMessage() {
    
    
    return `color:${
      
      this.#color}` ;
  }
  getMessageNonStatic() {
    
    
    return `color:${
      
      this.#getColor()}` ;
  }
}
 
class Rectangle extends Shape {
    
    }
 
console.log(Rectangle.getMessage());   // Uncaught TypeError: Cannot read private member #color from an object whose class did not declare it
 
const rectangle = new Rectangle();
console.log(rectangle.getMessageNonStatic());   // TypeError: Cannot read private member #getColor from an object whose class did not declare it

在这个例子中,this 指向的是 Rectangle 类,它无权访问私有字段 #color。当我们尝试调用 Rectangle.getMessage() 时,它无法读取 #color 并抛出了 TypeError。

class Shape {
    
    
  static #color = 'blue';
  static #getColor() {
    
    
    return this.#color;
  }
  static getMessage() {
    
    
    return `${
      
      Shape.#color}`;
  }
  getMessageNonStatic() {
    
    
    return `color:${
      
      Shape.#getColor()} color`;
  }
}
 
class Rectangle extends Shape {
    
    }
console.log(Rectangle.getMessage()); // color:blue
 
const rectangle = new Rectangle();
console.log(rectangle.getMessageNonStatic()); // color:blue

3.6 Top-level await(顶层 await)

在 ES2017 中,引入了 async await,以简化 Promise 的使用,但是 await 关键字只能在 async 函数内使用。如果在异步函数之外使用 await 就会报错。
顶层 await 允许我们在 async 函数外使用 await 关键字。它允许模块充当大型异步函数,通过顶层 await,这些模块可以等待资源加载,这样其他导入这些模块的模块在执行代码之前要等待资源加载完才会去执行。
之前由于 await 仅在 async 函数中使用,因此模块需通过将代码包装在 async 函数来在代码中包含await:

// test.js
let users;
 
export const fetchUsers = async () => {
    
    
  const res = await fetch('https://www.leoTest.com/users');
  users = res.json();
}
fetchUsers();
 
export {
    
     users };
 
 
// usingAwait.js
import {
    
     users } from './test.js';
console.log('users: ', users);
console.log('usingAwait module');

这样会有一个缺点,直接导入的 users 会是 undefined。如果我们需要访问得到它,需要在异步执行完成之后:

// usingAwait.js
import {
    
     users } from './test.js';
 
console.log('users:', users);   // 直接访问,undefined
 
setTimeout(() => {
    
    
  console.log('users:', users);   // 等待异步执行完成,再进行访问
}, 100);
 
console.log('usingAwait module');

当然,这种方法并不完全实现,因为如果异步函数执行花费的时间超过100毫秒,它就不会起作用了,users 仍然是 undefined。
还有另一个方法是导出一个 Promise,让导入模块知道数据已经准备好了:

// test.js
export default (async () => {
    
                       // 导出一个 Promise
  const res = await fetch('https://www.leoTest.com/users');
  users = res.json();
})();
export {
    
     users };
 
 
// usingAwait.js
import Promise, {
    
     users } from './test.js';
Promise.then(() => {
    
     
  console.log('usingAwait module');
  setTimeout(() => console.log('users:', users), 100); 
});

虽然这种方法似乎是会得出预期的结果,但也有一定的局限性:导入模块必须了解这种模式才能正确使用它。
而 top-level await(顶层 await)可以解决这些问题:

// test.js
const res = await fetch('https://www.leoTest.com/users');   // 使用顶层 await
const users = res.json();
export {
    
     users };
 
 
// usingAwait.js
import {
    
     users } from './test.js';
 
console.log(users);
console.log('usingAwait module');

此时的 users 必须等到异步执行完毕才会访问。

顶层 await 在以下几种场景中将非常有用:

  • 动态加载模块
const str = await import(`/i18n/${
      
      navigator.language}`);
  • 资源初始化
const con = await dbConnector();
  • 依赖回退
let translations;
try {
    
    
  translations = await import('https://app.fr.json');
} catch {
    
    
  translations = await import('https://fallback.en.json');
}

猜你喜欢

转载自blog.csdn.net/ZYS10000/article/details/125733925
今日推荐