1、动态 import ()

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

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

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

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

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

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

但是动态import()解决了这个问题。他可以在任何支持该语法的平台中使用,比如webpack、node或浏览器环境。并且模块标识符的格式则是由各平台自行指定,比如webpacknode支持使用模块名直接加载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状态,你可以在其中执行一些回退处理。


2、空值合并运算符(?? )

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

let num = number || 222

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

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

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

let num = number ?? 222

而且该运算符也支持短路特性

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

但需要注意一点,该运算符不能与ANDOR运算符共用,否则会抛出语法异常:

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

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

(a && b) ?? "default"

这个操作符的主要设计目的是为了给可选链操作符提供一个补充运算符,因此通常是和可选链操作符一起使用的:

const x = a?.b ?? 0;

下面介绍下ES11新增的可选链操作符(?.


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

属性访问

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

a?.b
a?.[x]

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

a == null ? undefined : a.b
a == null ? undefined : a[x]

方法调用

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

a?.()

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


访问深层次属性

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

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

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


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对应的两个数组类型:BigInt64ArrayBigUint64Array

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


5、globalThis

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

在浏览器环境中,我们可以有多种方式访问到全局对象,最常用到的肯定是window,但除此之外还有self,以及在特殊场景下使用的framesparaent以及top

我们通常不怎么需要关心windowself之间的区别,但如果使用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,而不是当前页面内真实的全局对象(该对象不能被直接访问)。


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中有任意一个切换为fulfilledrejected状态时,该promise将立即切换为相同状态;

  • Promise.allSettled返回一个组合后的promise,当所有promise全部切换为fulfilledrejected状态时,该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” )

使用场景如下:

1、有时候在进行一个页面的初始化流程时,需要加载多份初始化数据,或执行一些其它初始化操作,而且通常会希望等待这些初始化操作全部完成之后再执行后续流程:

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)
}

2、又例如我们有自定义的全局消息中心,那么还可以基于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()
}

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;
    }
}

results matching ""

    No results matching ""