メインコンテンツへ移動

ES2020

無料2020-06-21#JS#es2020#es11#es2020总结#es2020指南#es2020教程

ES2019 と比較して、ES2020 は大きなアップデートと言えます。動的 import、安全なチェーン操作、大きな整数のサポート……すべてが豪華ランチに追加されました

はじめに

ES2020(つまり ES11) は先週(2020 年 6 月)に正式にリリースされ、それ以前に Stage 4 に入った 10 件の提案はすべて仕様に取り入れられ、JavaScript 言語の新機能となりました

一.特性一覧

ES Module はいくつかの強化を迎えました:

  • import():動的モジュール識別子でモジュールを非同期に導入できる構文

  • import.meta:モジュール関連のメタ情報を運ぶためのオブジェクト

  • export * as ns from "mod";:新しい集約輸出構文

安全なチェーン操作を正式にサポート:

大きな数値演算のネイティブサポートを提供:

いくつかの基礎 API も新しい変化がありました:

  • Promise.allSettled:新しい Promise コンバイナー。allrace のようなショートカット特性を持たない

  • String.prototype.matchAll:イテレータ形式でグローバルマッチモード下の正規表現マッチ結果(indexgroups など)をすべて返す

  • globalThis:グローバルスコープ this にアクセスする通用方法

  • for-in mechanicsfor-in ループの特定の動作を規範

二.ES Module 強化

動的 import

[ES Module](/articles/module-es6 ノート 13/) は静的なモジュールシステムであることは知っています:

The existing syntactic forms for importing modules are static declarations.

静的であることは:

They accept a string literal as the module specifier, and introduce bindings into the local scope via a pre-runtime "linking" process.

  • 静的ロード:import/export 宣言はトップレベルスコープにのみ出現可能。按需ロード、遅延ロードをサポートしない

  • 静的識別子:モジュール識別子は文字列リテラルのみ。ランタイムで動的に計算されたモジュール名をサポートしない

例えば:

if (Math.random()) {
    import 'foo'; // SyntaxError
}

// You can't even nest `import` and `export`
// inside a simple block:
{
    import 'foo'; // SyntaxError
}

このような厳格な静的モジュールメカニズムは、ソースコードベースの静的分析、コンパイル最適化により大きな発揮空間を与えます:

This is a great design for the 90% case, and supports important use cases such as static analysis, bundling tools, and tree shaking.

しかし他のいくつかのシーンにはあまり友好的ではありません。例えば:

  • 最初のパフォーマンスを苛求するシーン:import 宣言で引用されたすべてのモジュール(初期化時に一時的に使用しないモジュールを含む)はすべて初期化段階で事前ロードされ、最初のパフォーマンスに影響

  • 目標モジュール識別子を事前に確定するのが難しいシーン:例えばユーザーの言語オプションに応じて異なるモジュールを動的にロード(module-enmodule-zh など)

  • 特殊な状況でのみ特定のモジュールをロードする必要があるシーン:例えば異常状況でダウングレードモジュールをロード

これらの動的にモジュールをロードする必要があるシーンを満足させるために、ES2020 は動的 import 特性(import())を推出しました:

import(specifier)

import()「関数」はモジュール識別子 specifier(その解析ルールは import 宣言と同じ)を入力し、Promise を出力。例えば:

// 目標モジュール  ./lib/my-math.js
function times(a, b) {
  return a * b;
}
export function square(x) {
  return times(x, x);
}
export const LIGHTSPEED = 299792458;

// 現在のモジュール index.js
const dir = './lib/';
const moduleSpecifier = dir + 'my-math.mjs';

async function loadConstant() {
  const myMath = await import(moduleSpecifier);
  const result = myMath.LIGHTSPEED;
  assert.equal(result, 299792458);
  return result;
}
// または async & await を使用しない
function loadConstant() {
  return import(moduleSpecifier)
  .then(myMath => {
    const result = myMath.LIGHTSPEED;
    assert.equal(result, 299792458);
    return result;
  });
}

import 宣言と比較して、import() の特徴は以下の通り:

  • 関数、ブランチなどの非トップレベルスコープで使用可能。按需ロード、遅延ロードは問題ではない

  • モジュール識別子は変数传入をサポート。動的に計算してモジュール識別子を確定可能

  • module に限定されず、普通の script 中でも使用可能

注意、関数のように見えますが、import() は実際には演算子です。なぜなら演算子は現在のモジュール関連情報を運ぶことができる(モジュール表示を解析するため)が、関数はできないから:

Even though it works much like a function, import() is an operator: in order to resolve module specifiers relatively to the current module, it needs to know from which module it is invoked. A normal function cannot receive this information as implicitly as an operator can. It would need, for example, a parameter.

import.meta

もう 1 つの ES Module 新特性は import.meta で、モジュール特定のメタ情報を透出するために使用:

import.meta, a host-populated object available in Modules that may contain contextual information about the Module.

例えば:

  • モジュールの URL またはファイル名:例えば Node.js 中の __dirname__filename

  • 所在する script タグ:例えばブラウザがサポートする document.currentScript

  • エントリーモジュール:例えば Node.js 中の process.mainModule

このようなメタ情報はすべて import.meta 属性に挂载可能。例えば:

// モジュールの URL(ブラウザ環境)
import.meta.url
// 現在のモジュールが所在する script タグ
import.meta.scriptElement

しかし注意が必要なのは、仕様は具体的な属性名と意味を明確に定義していないことで、すべて具体的な実装に依存。そのため特性提案中のブラウザにサポートしてほしいこれら 2 つの属性は将来サポートされる也可能し、されない也可能

P.S. import.meta 自体はオブジェクトで、プロトタイプは null

export-ns-from

3 番目の ES Module 関連の新特性はもう一種のモジュール輸出構文:

export * as ns from "mod";

export ... from ... 形式の 集約輸出 に同属。作用上は以下に類似:

import * as ns from "mod";
export {ns};

しかし現在のモジュールスコープに目標モジュールの各 API 変数を導入しない

P.S. import * as ns from "mod"; 構文と対照すると、ES6 モジュール設計中の排列組合の 1 つの疏漏のように見えます;)

三.チェーン操作サポート

Optional Chaining

非常に実用的な特性で、このような冗長な安全チェーン操作を代替するために使用:

const street = user && user.address && user.address.street;

新特性(?.)に換用可能:

const street = user?.address?.street;

構文形式は以下の通り:

obj?.prop     // オプションの静的属性にアクセス
// 等价于
(obj !== undefined && obj !== null) ? obj.prop : undefined

obj?.[?expr?] // オプションの動的属性にアクセス
// 等价于
(obj !== undefined && obj !== null) ? obj[?expr?] : undefined

func?.(?arg0?, ?arg1?) // オプションの関数またはメソッドを呼び出し
// 等价于
(func !== undefined && func !== null) ? func(arg0, arg1) : undefined

P.S.注意演算子は ?. ではなく単 ?。関数呼び出し中で少し奇妙 alert?.()。これは三項演算子中の ? と区別するため

メカニズムは非常に簡単。疑問符前の値が undefined または null でない場合のみ、疑問符後の操作を実行。否则 undefined を返

同様にショートカット特性を持ちま��:

// .b?.m でショートカットして undefined を返し、alert 'here' しない
({a: 1})?.a?.b?.m?.(alert('here'))

&& と比較して、新しい ?. 演算子は安全にチェーン操作を行うシーンにより適合。なぜなら:

  • セマンティクスがより明確:?. は属性/メソッドが存在しなければ undefined を返。&& のように左側の値を返すわけではない(ほとんど役に立たない)

  • 存在性判断がより正確:?.nullundefined のみを対象。&&任意の偽値 に遭遇すると返。時に需要を満足できない

例えば常用的な正規表現で目標文字列を抽出。構文記述は非常に簡潔:

'string'.match(/(sing)/)?.[1] // undefined
// 以前はこのようにする必要
('string'.match(/(sing)/) || [])[1] // undefined

Nullish coalescing Operator 特性と配合してデフォルト値を充填可能:

'string'.match(/(sing)/)?.[1] ?? '' // ''
// 以前はこのようにする必要
('string'.match(/(sing)/) || [])[1] || '' // ''
// または
('string'.match(/(sing)/) || [, ''])[1] // ''

Nullish coalescing Operator

同様に新しい構文構造(??)を導入:

actualValue ?? defaultValue
// 等价于
actualValue !== undefined && actualValue !== null ? actualValue : defaultValue

デフォルト値を提供するために使用。左側の actualValueundefined または null の場合、右側の defaultValue を返。否则左側の actualValue を返

|| に類似。主な違いは ??nullundefined のみを対象。|| は任意の偽値に遭遇すると右側のデフォルト値を返

四.大きな数値演算

BigInt と呼ばれる新しい基礎タイプを追加。大きな整数演算サポートを提供:

BigInt is a new primitive that provides a way to represent whole numbers larger than 2^53, which is the largest number Javascript can reliably represent with the Number primitive.

BigInt

JavaScript 中 Number タイプが正確に表現できる最大の整数は 2^53。より大きな数に対する演算をサポートしない:

const x = Number.MAX_SAFE_INTEGER;
// 9007199254740991 つまり 2^53 - 1
const y = x + 1;
// 9007199254740992 正确
const z = x + 2
// 9007199254740992 错了、変わらない

P.S. なぜ 2 の 53 乗なのかというと、JS 中の数値はすべて 64 ビット浮動小数点形式で存放。1 つの符号ビット、11 個の指数ビット(科学計数法中の指数)を除き、残りの 52 ビットで数値を存放。2 の 53 乗に対応するこの 52 ビットはすべて 0。表現できる次の数は 2^53 + 2。中間の 2^53 + 1 は表現不可:

[caption id="attachment_2213" align="alignnone" width="625"]JavaScript Max Safe Integer JavaScript Max Safe Integer[/caption]

具体的な説明は BigInts in JavaScript: A case study in TC39 を参照

BigInt タイプの出現はまさに此类の問題を解決するため:

9007199254740991n + 2n
// 9007199254740993n 正确

導入された新しいものは以下を含む:

  • 大きな整数字面量:数字に後綴 n を付けて大きな整数を表。例えば 9007199254740993n0xFFn(二進法、八進法、十進法、十六進法字面量すべて後綴 n を付けて BigInt に変換可能)

  • bigint 基礎タイプ:typeof 1n === 'bigint'

  • タイプ構築関数:BigInt

  • 数学演算子(加減乗除など)をオーバーロード:大きな整数演算をサポート

例えば:

// BigInt を作成
9007199254740993n
// または
BigInt(9007199254740993)

// 乗算演算
9007199254740993n * 2n
// べき演算
9007199254740993n ** 2n
// 比較演算
0n === 0  // false
0n === 0n // true
// toString
123n.toString() === '123'

P.S. BigInt API 詳細に関するより多くの情報は、ECMAScript feature: BigInt – arbitrary precision integers を参照

注意が必要なのは BigIntNumber と混用して演算できない

9007199254740993n * 2
// エラー Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

また BigInt は整数のみを表せるため、除算は直接切り捨て(Math.trunc() に相当):

3n / 2n === 1n

五.基礎 API

基礎 API にもいくつかの新しい変化があり、Promise、文字列正規表現マッチ、for-in ループなどを含む

Promise.allSettled

[Promise.all](/articles/完全理解 promise/#articleHeader12)、[Promise.race](/articles/完全理解 promise/#articleHeader13) に続き、Promise は新しい静的メソッド allSettled を追加:

// 传入されたすべての promise が結果(pending 状態から fulfilled または rejected になる)を持った後、onFulfilled をトリガー
Promise.allSettled([promise1, promise2]).then(onFulfilled);

P.S. 另外、any も路上。現在(2020/6/21)は Stage 3

all に類似。しかし某些項 rejected でショートカットしない。つまり、allSettled はすべての項が結果(成功失敗問わず)を持った後で初めて Promise チェーンの次の環に入る(したがって必ず Fulfilled 状態になる):

A common use case for this combinator is wanting to take an action after multiple requests have completed, regardless of their success or failure.

例えば:

Promise.allSettled([Promise.reject('No way'), Promise.resolve('Here')])
  .then(results => {
    console.log(results);
    // [
    //   {status: "rejected", reason: "No way"},
    //   {status: "fulfilled", value: "Here"}
    // ]
  }, error => {
    // No error can get here!
  })

String.prototype.matchAll

文字列処理の一般的なシーンは文字列中のすべての目標部分文字列をマッチしたい。例えば:

const str = 'es2015/es6 es2016/es7 es2020/es11';
str.match(/(es\d+)\/es(\d+)/g)
// 順調に ["es2015/es6", "es2016/es7", "es2020/es11"] を得

match() メソッド中で、正規表現がマッチした複数の結果は配列にパックされて返。しかし各マッチの結果以外の関連情報を得知できない。例えばキャプチャされた部分文字列、マッチした index 位置など:

This is a bit of a messy way to obtain the desired information on all matches.

此时は最も強大な exec に求助するしかない:

const str = 'es2015/es6 es2016/es7 es2020/es11';
const reg = /(es\d+)\/es(\d+)/g;
let matched;
let formatted = [];
while (matched = reg.exec(str)) {
  formatted.push(`${matched[1]} alias v${matched[2]}`);
}
console.log(formatted);
// ["es2015 alias v6", "es2016 alias v7", "es2020 alias v11"] を得

ES2020 が新增した matchAll() メソッドはまさに此类のシーンに対する補充:

const results = 'es2015/es6 es2016/es7 es2020/es11'.matchAll(/(es\d+)\/es(\d+)/g);
// 配列に変換して処理
Array.from(results).map(r => `${r[1]} alias v${r[2]}`);
// またはイテレータから取り出して直接処理
// for (const matched of results) {}
// 結果は上記と同じ

注意、matchAll()match() のように配列を返すのではなく、[イテレータ](/articles/generator(生成器)-es6 ノート 2/#articleHeader2) を返。大数据量のシーンにより友好的

for-in 走査メカニズム

JavaScript 中で for-in でオブジェクトを走査する時 key の順序は不確定。なぜなら仕様は明確に定義しておらず、かつプロトタイプ属性を走査できることで for-in の実装メカニズムは非常に複雑になり、異なる JavaScript エンジンは各自根深い異なる実装を持ち、統一するのが非常に難しい

したがって ES2020 は属性走査順序を統一することを要求せず、走査プロセス中のいくつかの特殊な Case に対して明確にいくつかのルールを定義:

  • Symbol タイプの属性を走査できない

  • 走査プロセス中、目標オブジェクトの属性は削除可能。まだ走査されておらずすでに削除された属性を無視

  • 走査プロセス中、新增属性がある場合、新しい属性が当回の走査で処理されることを保証しない

  • 属性名は重複して出現しない(1 つの属性名は最多 1 回出現)

  • 目標オブジェクト整条プロトタイプチェーン上の属性はすべて走査可能

詳細は 13.7.5.15 EnumerateObjectProperties を参照

globalThis

最後の新特性は globalThis。ブラウザ、Node.js など異なる環境中で、グローバルオブジェクト名称が統一されておらず、グローバルオブジェクトを取得するのが麻烦な問題を解決するために使用:

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

globalThis は統一されたグローバルオブジェクトアクセス方式として、常にグローバルスコープ中の this 値を指:

The global variable globalThis is the new standard way of accessing the global object. It got its name from the fact that it has the same value as this in global scope.

P.S. なぜ global と呼ばないのか?global は現有のいくつかのコードに影響を与える可能性があるため、別の globalThis を起して衝突を避ける

至此、ES2020 のすべての新特性が清楚になりました

六.まとめ

ES2019 と比較して、ES2020 は大きなアップデートと言えます。動的 import、安全なチェーン操作、大きな整数のサポート……すべてが豪華ランチに追加されました

参考資料

コメント

コメントはまだありません

コメントを書く