2020.12.10
ざっくり ECMAScript 2020
ざっくりと ES2020 のおさらいをしたい人向け。オカタい説明は mdn さんあたりにお任せするとして、ざっくりとおさらい。ES2020 は Nullish coalescing Operator、dynamic import、Optional Chainingなどが追加されたよ
環境構築
- MacOS High Sierra
- ふる〜い MBP 使ってます
- nvm, ndenv, nodebrew のどれでもいいので node が動く環境を用意
- VSCode 使ってます。拡張はお好みで。
- node.js のバージョンは v14.7.0
- 適当なディレクトリを掘って、index.js を用意しています (ファイル名はお好みで)
- 動作確認は $ node index.js って叩いてます
最小限で動かすだけならこれだけでおっけいです。ここらへんの準備も面倒ならば codepen だの JSFiddle だの REPL だのを使ってもおk
es2020
es2019 は以下のような機能が追加されている:原文(参考 github tc39/proposals finished-proposals)
String.prototype.matchAll、dynamic import、BigInt、Promise.allSettled、globalThis、for-in mechanics、Optional Chaining、Nullish coalescing Operator、import.meta
String.prototype.matchAll
対象の文字列と正規表現のマッチを行いイテレータを返すよ。結果の値はマッチの結果を含む Array オブジェクトだよ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
const dad = 'My dad bared his bum to the moon every night.' const regex = /o/g console.log(dad.matchAll(regex)) for(let d of dad.matchAll(regex)){ console.log(d) } // Object [RegExp String Iterator] {} // [ // 'o', // index: 22, // input: 'My dad bared his bum to the moon every night.', // groups: undefined // ], // [ // 'o', // index: 29, // input: 'My dad bared his bum to the moon every night.', // groups: undefined // ], // [ // 'o', // index: 30, // input: 'My dad bared his bum to the moon every night.', // groups: undefined // ] |
dynamic import
import 関数が追加されたよ。import文とは違うよ。以下のような特徴があるよ
- import 文はモジュールなどの指定には文字列を指定していたが、import 関数は必ずしも文字列リテラルでは無いので、import(
./language-packs/${navigator.language}.js
) の様なコードが動作する - import 関数は Promise を返すよ。これは、モジュール自体と同様に、モジュールのすべての依存関係をフェッチ、インスタンス化、評価した後に作成されるよ
- スクリプトとモジュールの両方で動作するよ。スクリプトコードはモジュールの世界に簡単に非同期で入ることができるようになるよ。スクリプトとモジュールを非同期でプログラマブルに実行できちゃう。
これで場合によっては、不必要なモジュールを読み込まなくても良くなった!!!
1 2 3 4 5 6 7 |
import(`./section-modules/${link.dataset.entryModule}.js`) .then(module => { module.loadPageInto(main); }) .catch(err => { main.textContent = err.message; }); |
BigInt
BigIntは、JavascriptがNumberプリミティブで確実に表現できる最大の数である2の53乗(9,007,199,254,740,991)より大きい整数を表現する方法を提供する新しいプリミティブだよ
BigInt は整数の末尾に n を追加するか、コンストラクタを呼び出すことで作成されるよ
1 2 3 4 5 6 7 8 9 10 11 |
const c = BigInt(9007199254740992) const addN = 9007199254740992n const hugeButString = BigInt('9007199254740992'); console.log(c) console.log(addN) console.log(hugeButString) // 9007199254740992n // 9007199254740992n // 9007199254740992n |
Promise.allSettled
Promise.allSettled( ) は、配列などの反復可能なオブジェクトで引数として与えられたすべてのプロミス(resolve、reject 問わず)の結果を記述したオブジェクトの配列を返してくれるよ
この返されるオブジェクトを含む配列はそれぞれのステータスを持っていて以下の通りになっているよ
- resolve の場合: { status, value }
- reject の場合: { status, reason }
サンプルコード
1 2 3 4 5 6 7 8 9 10 11 12 |
const promises = [Promise.resolve(3), Promise.reject('foo'), Promise.resolve(5)] Promise.allSettled(promises). then((results) => { console.log(results) }); // [ // { status: 'fulfilled', value: 3 }, // { status: 'rejected', reason: 'foo' }, // { status: 'fulfilled', value: 5 } // ] |
globalThis
グローバルオブジェクトは ブラウザ と node.js で指定方法が違います。この違いを吸収するのが globalThis で、これを使うことによって環境応じてのグローバルオブジェクトの指定を変えなくても済む
1 2 3 4 5 6 7 8 9 |
console.log(global === globalThis) // node.js //true console.log(window === this) // browser console.log(window === globalThis) // browser console.log(this === globalThis) // browser // true // true // true |
for-in mechanics
for (a in b) … の順序は EnumerateObjectProperties で指定されていますが、ゆるくしか指定されていません。いわゆる実装依存なっていたところを ES2020 で明確な規則が設定されました。詳しくはドキュメントを参照してくださいな
Optional Chaining
ツリーのような構造の奥にあるプロパティ値を探す場合、中間ノードが存在するかどうかを確認しなければならないことがあったりするよね
1 |
var street = user.address && user.address.street; |
この時、NULLではない場合にのみ結果からプロパティを抽出したい場合があるよね
1 2 |
var fooInput = myForm.querySelector('input[name=foo]') var fooValue = fooInput ? fooInput.value : undefined |
?. (クエスチョン ドット:Optional Caining演算子)を使用すると、開発者はこれらのケースの多くを繰り返したり、一時変数に中間結果を代入したりすることなく処理することができるよ
1 2 |
var street = user.address?.street var fooValue = myForm.querySelector('input[name=foo]')?.value |
Nullish coalescing Operator(??)
プロパティアクセスを実行する際に、そのプロパティアクセスの結果がnullまたは未定義の場合にデフォルト値を提供したいと思うことがよくあります。現在のところ、JavaScript でこの意図を表現する典型的な方法は || 演算子を使用することです。
1 2 3 4 5 6 7 8 9 10 11 12 |
const response = { settings: { nullValue: null, height: 400, animationDuration: 0, headerText: '', showSplashScreen: false } }; const undefinedValue = response.settings.undefinedValue || 'some other default'; // result: 'some other default' const nullValue = response.settings.nullValue || 'some other default'; // result: 'some other default' |
これは、nullとundefinedの一般的なケースではうまく機能しますが、意外な結果をもたらす可能性のある偽の値 (falseになる) がいくつかあります。
1 2 3 |
const headerText = response.settings.headerText || 'Hello, world!'; // Potentially unintended. '' is falsy, result: 'Hello, world!' const animationDuration = response.settings.animationDuration || 300; // Potentially unintended. 0 is falsy, result: 300 const showSplashScreen = response.settings.showSplashScreen || true; // Potentially unintended. false is falsy, result: true |
nullary合体演算子は、これらのケースをより良く処理することを目的としており、nullary値(nullまたは未定義)に対する等値性チェックの役割を果たします。
Syntax
Nullary合体演算子は ?? (クエスチョン2つ)で表し、演算子の左辺が null もしくは undefined が評価された場合にその右辺が返されます
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const response = { settings: { nullValue: null, height: 400, animationDuration: 0, headerText: '', showSplashScreen: false } }; const undefinedValue = response.settings.undefinedValue ?? 'some other default'; // result: 'some other default' const nullValue = response.settings.nullValue ?? 'some other default'; // result: 'some other default' const headerText = response.settings.headerText ?? 'Hello, world!'; // result: '' const animationDuration = response.settings.animationDuration ?? 300; // result: 0 const showSplashScreen = response.settings.showSplashScreen ?? true; // result: false |
import.meta (翻訳ママ)
原文をそのまま翻訳しています。翻訳に致命的な問題がある場合はご一報ください。また、原稿を確認したい方はこちらからどうぞ
またこの機能についての mdn にはこの機能の実装状況が確認できます。2020年12月9日の時点では進捗 86% だそうです。(以下、mdn からの引用)
この機能はまだブラウザー間の安定性に達していないため、以下の表で、この機能の毎日の実装状況を示しています。このデータは、JavaScript の標準テストスイートである Test262 で、ナイトリービルド、または各ブラウザーの JavaScript エンジンの最新リリースで、関連する機能テストを実行することで生成されます。
新ためて import.meta
ホスト環境がモジュール内で評価するコードに対して、有用なモジュール固有の情報を提供できることがよくあります。以下にいくつかの例を示します。
モジュールの URL またはファイル名
これは、スコープ内の__filename変数(およびその対応する__dirname)を介して、Node.jsのCommonJSモジュールシステムで提供されています。これにより、以下のようなコードでモジュールファイルからの相対的なリソースを簡単に解決することができます。
1 2 3 |
const fs = require("fs"); const path = require("path"); const bytes = fs.readFileSync(path.resolve(__dirname, "data.bin")); |
ディレクトリ名が利用できなければ、fs.readFileSync(“data.bin”) のようなコードは、カレントワーキングディレクトリからの相対的なdata.binを解決してしまいます。これは、ライブラリの作者がリソースをモジュールにバンドルしている場合、CWD からの相対的な位置はどこにでもある可能性があるため、一般的には有用ではありません。
同様の使用例がブラウザにも存在し、URL がファイル名の代わりになるようになっています。
The initiating <script>
これは、グローバルな document.currentScript プロパティを介して、古典的なスクリプト (すなわち、モジュールではないスクリプト) 用のブラウザで提供されています。これは、含まれているライブラリの設定によく使われます。ページは以下のようなコードを書きます。
1 |
<script data-option="value" src="library.js"></script> |
のようなコードでdata-optionの値を調べます。
1 |
const theOption = document.currentScript.dataset.option; |
余談ですが、このために lexicall スコープされた値ではなく、実質的にグローバルな変数を使用するメカニズムには問題があります。
これは、値がトップレベルでのみ設定されることを意味し、非同期コードで使用される場合はそこに保存されなければならないことを意味します。
Am I the “main” module?
Node.jsでは、プログラムの「メイン」モジュールなのか「エントリー」モジュールなのかで分岐するのが一般的です。
1 2 3 |
if (module === process.mainModule) { // run tests for this library, or provide a CLI interface, or similar } |
つまり、インポートされたときにはライブラリとして、直接実行されたときには副次的なプログラムとして、単一のファイルを使用することができます。
この特定の定式化は、ホストが提供するスコープされた値モジュールとホストが提供するグローバル値process.mainModuleを比較することに依存していることに注意してください。
その他の雑多な使用例
- module.childrenやrequire.resolve()などの他のNode.jsユーティリティ
- モジュールが属する「パッケージ」に関する情報は、Node.js の package.json または Web パッケージを介して提供されます。
- HTMLモジュールの内部に埋め込まれたJavaScriptモジュール用のDocumentFragmentへのポインタ。
制約
この情報がホスト固有のものであることが多いことを考えると、私たちは JavaScript には、メタデータのすべての部分について JavaScript の標準化を要求するのではなく、ホストが使用できる一般的な拡張性のメカニズムを提供したいと考えています。
また、前述したように、この情報は、例えば一時的に設定されたグローバル変数を介して提供されるのではなく、辞書的に提供されるのが最善です。
提案された解決策
この提案では、それ自体がオブジェクトである import.meta meta-property を追加します。これは ECMAScript の実装によって作成され、ヌルのプロトタイプを持っています。ホスト環境は、オブジェクトに追加されるプロパティのセットを(キー/値のペアとして)返すことができます。最後に、エスケープハッチとして、オブジェクトは必要に応じてホストによって任意に変更することができます。
import.meta meta-property は、現在実行中のモジュールのメタ情報を得るためのものであり、現在実行中のクラシックスクリプトの情報に再利用されるべきではありません。
Example
以下のコードは、ブラウザの import.meta に追加することを想定しているプロパティを使用しています。
1 2 3 4 5 6 7 8 9 10 11 12 |
(async () => { const response = await fetch(new URL("../hamsters.jpg", import.meta.url)); const blob = await response.blob(); const size = import.meta.scriptElement.dataset.size || 300; const image = new Image(); image.src = URL.createObjectURL(blob); image.width = image.height = size; document.body.appendChild(image); })(); |
このモジュールがロードされると、場所に関係なく、兄弟ファイル hamsters.jpg をロードして画像を表示します。画像のサイズは、インポートに使われた script 要素を使って設定することができます。
1 |
<script type="module" src="path/to/hamster-displayer.mjs" data-size="500"></script> |
代替ソリューションの探索
ホスト固有のメタデータを提供する可能性のある方法は他にもいくつかあります。探索された主な他の方法は以下の通りです。
Using a particular module specifier
ここでの考え方は、モジュールシステムからコンテキスト固有の値を取得するメカニズムが既にあるということです。この場合、これらのプロパティを取得するための構文は、特定のモジュール指定子のトップレベルのインポートを経由することになります。
1 |
import { url, scriptElement } from "js:context"; |
これは既存のフレームワークによく合っており、ホスト側で完全に処理することができ、ECMAScript の新しい提案を必要としません (ホストはモジュールの指定子がどのように解釈されるかを制御できるので)。しかし、Web上ではWebKitからの反対があり、TC39がこのアイデアの背後で一致団結していなかったことを考えると、WebKitの反対を押し切ってみる価値はないと判断されました。特に import.meta がすぐに利用できるようにならない場合には、Node.js はまだこれを実装するかもしれません。
別の方法でレキシカル変数を導入する
この可能性は、Node.jsが現在どのように__dirname、__filename、モジュールを注入しているかと同じように、適切な情報を変数としてモジュールのスコープに何らかの形で注入することになります。そうすれば、それらは通常のスコープ内変数として使用されることになります。
1 2 |
console.log(moduleURL); console.log(moduleScriptElement); |
実装と仕様の観点からは、例えば、ホストが ECMAScript の仕様メカニズムに渡す前に、すべてのモジュールのソーステキストに適切な変数宣言を前置しておくことで、ECMAScript の仕様を変更することなくこれを行うことができます。別の方法として、ホストは、[[環境]]フィールドのスコープが若干異なる新しいタイプのモジュールレコードを導入することができます。(あるいは、ECMAScriptは、ホストがソーステキストモジュールレコードの[[環境]]の設定に介入できるように修正されました)。
委員会は、いくつかのメンバーが新しい変数でモジュールスコープを “汚染 “するのが好きではなかったので、ECMAScriptでこのアイデアを扱うことに一般的に反対しました。これは、import.metaが十分に早く進まない場合のウェブホストのためのフォールバックオプションとして残っています。