なぜ ES2022 で文字列リテラルを使った import/export ができるようになるのか
Published on
11 月 11 日に、以前から一部で注目されていたある Pull Request が tc39/ecma262 にマージされました。
この Pull Request がマージされたことで、識別子ではなく文字列リテラルを使った import/export が可能になりました。
この仕様変更はプロポーザルという形で扱われてはいませんが、構文上の影響があるので、JavaScript ユーザーとして知っておくに越したことはないものになります。
概要
まず具体例を示します。
今回の変更によって、次のように import/export する際の名前として文字列リテラルを使えるようになります。
const foo = "foo";
export { foo as "😃 hey hey" };
import { "😃 hey hey" as foo } from "./module.js";
console.log(foo); // foo
基本的にはこれだけです。
詳解
ここからは仕様上の用語を使って解説をします。
この変更が入る前の ECMAScript では ImportSpecifier で as を使う場合 as の左側は IdentifierName でなければいけませんでした。
また ExportSpecifier は、単一の IdentifierName もしくは、as を使う場合は as の左側と右側は両方とも IdentifierName でなければいけませんでした。
今回の変更によって、新たに ModuleExportName という構文が追加されました。ModuleExportName は、IdentifierName もしくは StringLiteral の形をとります。
たとえば、識別子 foo や 文字列リテラル "😃 hey hey" は ModuleExportName です。
そして、ImportSpecifier で as を使う場合 as の左側は ModuleExportName の形をとるようになりました。したがって、次の例の import 文はすべて構文として妥当です。
import { foo } from "mod";
import { foo as bar } from "mod";
import { "😃 hey hey" as baz } from "mod";
ExportSpecifier は、単一の ModuleExportName もしくは、 as を使う場合 as の左側と右側は両方とも ModuleExportName の形をとるようになりました。したがって、次の例の export 文はすべて構文として妥当です。
export { foo } from "mod";
export { "😃 hey hey" } from "mod";
export { foo as foo } from "mod";
export { foo as "😃 hey hey" } from "mod";
export { "😃 hey hey" as foo } from "mod";
export { "😃 hey hey" as "😃 hey hey" } from "mod";
ただし ExportSpecifier の ModuleExportName を StringLiteral にできるのは、その ExportSpecifier を含む ExportDeclaration に FromClause が存在する場合のみです。
たとえば、次のコードは ExportDeclaration に FromClause が存在しないので ExportSpecifier で StringLiteral を使うことはできません。
// できない
export { "😃 hey hey" };
一方で、次のコードは FromClause が存在するので、ExportSpecifier で StringLiteral を使うことができます。
// できる
export { "😃 hey hey" } from "mod";
文字列の制約
StringLiteral は通常の JavaScript の文字列リテラルです。たとえば "foo" とか "bar" みたいな形をしたものです。
ModuleExportName は StringLiteral を含むので、全ての文字列リテラルを ModuleExportName として使えるようにみえますが、実際には少々異なります。ModuleExportName として使える StringLiteral には制限があります。
ModuleExportName として使える StringLiteral は、Well-Formed Code Unit Sequence でなければいけません。
このことは、Module Semantics の Eary Errros 内の https://tc39.es/ecma262/#_ref_6583 に記載されています。
Well-Formed Code Unit Sequence とは
JavaScript の文字列は UTF-16 でエンコードされます。そのため、実際には JavaScript の文字列というのは 16 ビットの整数で表現される Unicode のコードユニットの並びでしかありません。
UTF-16 では基本的に 1 文字につき 16 ビットで表現されます。しかし、Unicode の BMP(基本多言語面)に収まらない文字は 16 ビットのコードユニットを二つ並べたペアで表現します。
たとえば、ひらがなの あ は BMP に含まれており、一つのコードユニット(0x3042)で表されます。
console.log("\u3042"); // あ
一方で、𠮟(叱 ではないことに注意) は BMP に含まれないので、二つのコードユニット(0xD842 と 0xDF9F)で表されます。このようなコードユニットのペアを、サロゲートペアといいます。
console.log("\uD842\uDF9F"); // 𠮟
前述のとおり、JavaScript の文字列は 16 ビットの整数で表現されるコードユニットの並びでしかありません。したがって、𠮟 を構成する二つのコードユニットである 0xD842 と 0xDF9F のうち一つだけを含む文字列も作ることができます。
const str = "\uD842";
しかし、0xD842 単体に対応する文字は Unicode には存在しません。
このような、対になっていないサロゲートペアを含むような文字列は Well-Formed Code Unit Sequence ではありません。
逆に、対になっていないサロゲートペアを許容しないような文字列を Well-Formed Code Unit Sequence といいます。つまり、大雑把にいえば「ちゃんと文字になっているコードユニットで構成された文字列」ということです。
ちなみに、このような Well-Formed な文字列は WebIDL では USVString と呼ばれています。
新しい Abstract Operation IsStringWellFormedUnicode
この仕様の変更に伴って、IsStringWellFormedUnicode という新しい Abstract Operation が追加されました。
この Abstract Operation は、引数の文字列が Well-Formed Code Unit Sequence かどうかを判定します。
前述した ModuleExportName のための Early Errors では、この IsStringWellFormedUnicode Abstract Operation を使って StringLiteral が Well-Formed Code Unit Sequence かどうかの判定を行います。そしてもし Well-Formed Code Unit Sequence でなければ Syntax Error になります。
仕様変更のモチベーション
実はこの仕様の変更は、今の Web の仕様ではほとんど役に立つことはありません。
この変更が行われたモチベーションは、将来的に WebAssembly の Module との相互運用性を向上させるためです。
この背景を理解するために、おさえておくべき前提が二つあります。
1つ目は、WebAssembly の Module では関数を export するときに文字列で名前をつけるということです。
たとえば次の例では $add という関数を "add" という名前で export しています。
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "add" (func $add))
)
2つ目は、WebAssembly の Module を JavaScript から import できるようにしたい、という動きがあるということです。WebAsembly/esm-integration などで、その動きを見ることができます。
簡単にいえば、次のようにして簡単に WebAssembly の Module を JavaScript から扱えるようにしたいということです。
import { add } from "foo.wasm";
console.log(add(1, 2)); // 3
現在の WebAssembly および ECMAScript の仕様では、このような形で JavaScript 側から WebAssembly の Module を読み込むことはできません。
これらを前提として上で、次の例について考えます。
この例は前述したものとほとんど変わりませんが、export の後ろが "add" ではなく "+" になっています。export の後ろには文字列を置くことができるので、これは妥当な Module です。
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "+" (func $add))
)
将来、WebAssembly の Module を JavaScript から import できるようになったときに、このモジュールから + 関数を named import したいとします。
しかし、+ は IdentifierName ではないので、今までの ECMAScript の仕様では named import できませんでした。
// できない
import { + } from "foo.wasm";
// できない
import { + as add } from "foo.wasm";
今回の変更によって ImportSpecifier の as の左側に StringLiteral を置けるようになったことで、次のように書るようになりました。
// ES2022 でできる
import { "+" as add } from "foo.wasm";
console.log(add(1, 2)); // 3
このような書き方は ES2022 では構文上は妥当ですが、実際にはまだ WebAssembly の import はできません。
また、ModuleExportName の StringLiteral が Well-Formed Code Unit Sequence でなければならないという制約が存在するのも、WebAssembly との相互運用のためです。
WebAssembly のテキストフォーマットで export の後に続く文字列は Well-Formed Code Unit Sequence でなければいけないので、それと統一させたのでしょう。
参考リンク
- TC39
- Babel
- MDN
- ECMA262
- Unicode
- WebAssembly
記事に関する報告などはこちらから