- A+
ECMAScript edition 11
ECMAScript 2020
是 ECMAScript
第 11 版规范,英文比较好的可以看看 ECMA-262 阐述的标准。在这里记录一下学习笔记,并且尽量举一些例子,便于理解。
1. dynamic import
ES Module 是一套静态的模块系统,不支持按需加载/懒加载,不支持动态计算的模块名。比如我想在选择分支中引入模块:
if (condition) { const foo = import 'foo'; // 报错,SyntaxError }
不得不使用 require()
代替:
if (condition) { const foo = require('foo'); }
import
这种设计让基于源码的 静态分析 和 Tree Shaking 有了很大发挥空间,但是对于某些场景不够友好:
- 当静态导入的模块很明显的降低了代码的加载速度且被使用的可能性很低,或者并不需要马上使用它。
- 当静态导入的模块很明显的占用了大量系统内存且被使用的可能性很低
- 当被导入的模块,在加载时并不存在,需要异步获取
- 当导入模块的说明符,需要动态构建。(静态导入只能使用静态说明符)
- 当被导入的模块有副作用(这里说的副作用,可以理解为模块中会直接运行的代码),这些副作用只有在触发了某些条件才被需要时。(原则上来说,模块不能有副作用,但是很多时候,你无法控制你所依赖的模块的内容)
为了满足像上面列举的这些个场景,ES2020 推出了 dynamic import
特性(import()
):
import(specifier);
关键字 import
可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个 promise
对象。
loadMod = asnyc () => { const myMod = await import(moduleSpecifier); const res = myMod.foo(); return res; }
上面使用 asnyc&await 接收动态导入的异步结果。
? 注意,虽然这玩意儿长得像一个函数,但是实际上是一个操作符,因为操作符能够带上当前模块信息,而函数不能。
尽管动态引入提供了很大的方便,但是不能够在项目中进行滥用,如同开始所说的,静态框架能提供良好的 静态分析 和 Tree Shaking。
2. import.meta
顾名思义,这个特性是用来带出模块特定的元信息的,比如:
- 模块的 URL 或者文件名
- 所处的 script 标签
- 入口模块
但是在标准里被没有明确规定需要透露的属性和含义,全都有具体实现来定制,也就是说,将来这个属性里能获取到什么属性,都是由厂商说了算。
3. export * as ns from 'module'
// menu.js export * as ns from './info';
相当于以下代码:
import * as ns from './info'; export { ns };
? 不过在 menu.js 中是获取不到 ns 的,因为这个语法不会真的将模块导入。
4. 空值合并运算符
控制合并运算符看起来和 &&
以及 ||
这样的逻辑运算符很像,实际上是用来提供默认值的,也算是一种简略的逻辑判断:
actualVal ?? defaultVal; // 等价于 actualVal !== undefined && actualVal !== null ? actualVal : defaultVal;
也许之前你会用这样的判断:
const foo = actualVal || defaultVal;
但是这两者中有一个显著的不同,因为 JavaScript 是一门动态语言,当使用 ||
时会对前面的变量进行隐式转换:
let foo; const actualVal = 0; // fasly 值 foo = actualVal || 1; console.log(foo); // 1 foo = actualVal ?? 1; console.log(foo); // 0
前一种方法,当我们希望真实的值为 0(或者除 null
和 undefined
以外的 falsy 值) 的时候,却被错误地分配为 1。而 ??
操作符只有在 actualVal
为 null
或者 undefined
的时候才会返回右侧操作数。这更符合在实际业务中的逻辑处理,例如等级、金钱、数量等单位制属性。
5. 可选链操作符
可选链 ?.
是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误。
众所周知,在 JavaScript 里做链式取值操作很容易导致程序报错,例如:
// 获取用户的地址 const address = response.userInfo.address.street;
如果 response
中不存在 userInfo
这个属性,那么再去尝试取 address
的值就会报错,之后的 street
也需要检查前面的属性是否存在。ES2020 之前都是像这样避免这个错误:
// 用 && 操作符 const address = response.userInfo && response.userInfo.address && response.userInfo.address.street; // 三元表达式 const street = response.userInfo ? (response.userInfo.address ? response.userInfo.address.street : null) : null;
这样的缺点也非常明显,那就是重复编写前面的变量,当我们想要获取更深层的属性时,代码就会变得又臭又长,难以阅读。这就是为什么我们需要可选链 .?
来彻底地解决以上的问题。换用新特性赋值:
const address = response.userInfo?.address?.street;
如果可选链 ?.
前面的部分是 undefined
或者 null
,它会停止运算并返回该部分。(短路效应)
另外还有 ?.()
以及 ?.[]
这样的变体方法,原理是差不多的。
总结,可选链 ?. 语法有三种形式:
-
obj?.prop
—— 如果obj
存在则返回obj.prop
,否则返回undefined
-
obj?.[prop]
—— 如果obj
存在则返回obj[prop]
,否则返回undefined
-
obj.method?.()
—— 如果obj.method
存在则调用obj.method()
,否则返回undefined
? 应该慎重使用可选链,仅在当左边部分不存在也没问题的情况下使用为宜。以保证在代码中有编程上的错误出现时,也不会对我们隐藏。
6. BigInt
BigInt
是一种数字类型的数据,它可以表示任意精度格式的整数。在此之前,JS 中安全的最大数字是 9009199254740991,即 2^53-1,在控制台中输入 Number.MAX_SAFE_INTEGER
即可查看。超过这个值,JS 没有办法精确表示。另外,大于或等于 2 的 1024 次方的数值,JS 无法表示,会返回 Infinity
。
BigInt
即解决了这两个问题。BigInt
只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。为了和 Number
类型进行区分,BigInt
类型的数据必须添加后缀 n
。
//Number类型在超过9009199254740991后,计算结果即出现问题 const num1 = 90091992547409910; console.log(num1 + 1); //90091992547409900 //BigInt 计算结果正确 const num2 = 90091992547409910n; console.log(num2 + 1n); //90091992547409911n
还可以使用 BigInt
对象来初始化 BigInt
实例:
console.log(BigInt(999)); // 注意:没有 new 关键字
7. matchAll
字符串处理的一个常见场景是想要匹配字符串中所有的目标子串,例如:
const str = 'es2015/es6 es2016/es7 es2020/es11'; str.match(/(esd+)/es(d+)/g); // ["es2015/es6", "es2016/es7" "es2020/es11"]
但只能获取到字符串,并不能获取到额外信息例如捕获到的字串、索引值等,这时候只能使用 exec
方法。
而新增的 matchAll
方法就是用于处理这种场景的:
const reg = /[0-3]/g; const data = '2020'; console.log(data.matchAll(reg)); // data.matchAll 的返回值是一个迭代器 console.log([...data.matchAll(reg)]); /** * 0: ["2", index: 0, input: "2020", groups: undefined] * 1: ["0", index: 1, input: "2020", groups: undefined] * 2: ["2", index: 2, input: "2020", groups: undefined] * 3: ["0", index: 3, input: "2020", groups: undefined] */
返回迭代器的做法对数据量大的场景很友好。?
8. Promise.allSettled
promise.allSettled
和 promise.all
很像,区别在于它不会因为某个 rejected
状态进入失败状态,而是等所有任务都得到结果后才进入 Promise 链的下一环,也就是说它无论如何都会进入 fulfilled
状态。
const promise1 = Promise.resolve(100); const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'info')); const promise3 = new Promise((resolve, reject) => setTimeout(resolve, 200, 'name')); Promise.allSettled([promise1, promise2, promise3]).then((results) => console.log(result)); /* [ { status: 'fulfilled', value: 100 }, { status: 'rejected', reason: 'info' }, { status: 'fulfilled', value: 'name' } ] */
可以看到,它返回的结果是一个数组,里面包含了对应的 promise
的状态 status
属性,如果是 fulfilled
状态则带出 value
值,否则用 reason
属性带出失败原因。
9. globalThis
globalThis
是用来解决不同环境下的全局对象不统一,获取全局对象比较麻烦的问题。在 ES11 标准之前,可能需要用这样一个函数来获取全局对象:
const globalThis = (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'); })();
globalThis
作为统一的全局对象获取方式,总是指向全局作用域中的 this
值。
10. for-in 循环的规范
JavaScript 中通过 for-in
遍历对象时 key 的顺序是不确定的,因为没有明确定义,所以不同的引擎有各自不同的实现,很难统一。所以 ES2020 不要求统一遍历顺序,而是对遍历过程中一些特殊的案例明确定义了一些规则:
- 无法遍历到 Symbol 类型的属性
- 遍历过程中,目标对象的属性能够被删除,未遍历到却被删除了的属性会被忽略
- 遍历过程中,如果有新增属性,不保证新增的属性能在本次遍历中处理到
- 属性名不会重复出现
- 目标对象整条原型链上的属性都能遍历到
总结
相比于 ES2019,ES2020 的更新算是非常豪华了,既带来了日常工作中常用的 API,也有工程构建和基础运算的增强。