なぜ 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
記事に関する報告などはこちらから