まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。
この広告は、90日以上更新していないブログに表示しています。
ECMA-262 6th がリリースされてECMAScript (JavaScript) にモジュールの機能が導入されたわけですが、実際どんなものなのかはっきりわかってなかったのでECMA-262 6th を読んでみました。 このブログ記事はそのまとめです。 実践的な話 (TypeScript でモジュールの機能を使うことについてや、既存プロジェクトへのモジュールの導入など) はまた今度書くつもりです。
この文書では歴史的背景などには触れずに坦々とECMA-262 を読んでいくだけですので、歴史的背景などを含めた解説を読みたい方は最後に挙げている関連ページなどを読むと良いと思います。
ECMA-262 6th (ECMAScript 2015 Language Specification) の中で、モジュールに関して述べられているところを引用しながらコメントしていきます。
Goals forECMAScript 2015 include providing better support for large applications, library creation, and for use ofECMAScript as a compilation target for other languages. Some of its major enhancements include modules, class declarations, lexical block scoping, iterators and generators, promises for asynchronous programming, destructuring patterns, and proper tail calls.
ECMA-262 6th (Introduction)
ES 2015 の主たる拡張の一番最初にモジュールの導入があげられています。 ES 2015 の目標に 「better sopport for large applications and library creation」 が含まれていますが、モジュールの導入はまさに大きなアプリケーションやライブラリ記述のよりよいサポートのためのものといえるでしょう。
LargeECMAScript programs are supported by modules which allow a program to be divided into multiple sequences of statements and declarations. Each module explicitly identifies declarations it uses that need to be provided by other modules and which of its declarations are available for use by other modules.
ECMA-262 6th (4.2 ECMAScript Overview)
モジュールを使うことでプログラムを複数の 「文や定義の連なり」 に分割できるので、巨大なプログラムを開発しやすくなります。 各モジュールは、別のモジュールでの宣言を明示的にimport
して使用したり、別モジュールに提供するためにモジュール内での宣言をexport
したりできます。依存先モジュールが明示的に示されるのがいいところですね。
A module environment is a Lexical Environment that contains the bindings for the top level declarations of a Module. It also contains the bindings that are explicitly imported by the Module. The outer environment of a module environment is a global environment.
ECMA-262 6th (8.1 Lexical Environment)
モジュールの環境 (module environment) は、モジュールのトップレベルの宣言のバインディングを保持したレキシカル環境 (Lexical Environment) です。 そこには明示的にインポートされた宣言も含まれています。 んでもって、モジュール環境の外側の環境 (outer environment) はグローバル環境となってます。
つまり、モジュールでの宣言はモジュール環境に閉じていて、グローバル環境からは (直接は) モジュールでの宣言を参照することはできないわけです。 一方で、モジュール環境の外側の環境はグローバル環境なので、グローバル環境での宣言をモジュールから参照することはできます。
よって、これまでモジュールを使っていないプログラムにモジュールを導入する場合は、どこからも参照されない箇所 (すなわちプログラムのエントリポイント) から少しずつモジュールにしていくという作戦をとれます。
ES 2015 のモジュールの仕様が Node.js のモジュールの仕組みやAMD を元に作成されたであろう*1 ことを考えると、納得の仕様ですね。
There are four types ofECMAScript code:
ECMA-262 6th (10.2 Types of Source Code)
- (略)
- Module code is source text that is code that is provided as aModuleBody. It is the code that is directly evaluated when a module is initialized. The module code of a particular module does not include any source text that is parsed as part of a nestedFunctionDeclaration,FunctionExpression,GeneratorDeclaration,GeneratorExpression,MethodDefinition,ArrowFunction,ClassDeclaration, orClassExpression.
ソースコードの種類として、モジュールコードと呼ばれるものがあります。 ES 2015 で追加されました。 モジュールのソースコードはモジュールコードとして解釈されるわけです。
ECMA-262 6th (10.2.1 Strict Mode Code)
- Module code is always strict mode code.
モジュールコードは常に strict モードになるみたいです。 Babel などのトランスパイラを使って ES 2015 のモジュールを現在の ES 実行環境で疑似的に実現させた場合は strict モードで動かないかもしれませんが、モジュールは strict モードで動くものとしてコードを書きましょう。 (モジュールには"use strict";
を付ける、とか。)
ちなみに TypeScript の 1.8 からは、モジュールのコンパイル後の JS には"use strict";
が付けられるようになるようです。
Modules were always parsed in strict mode as per ES6, but for non-ES6 targets this was not respected in the generated code. Starting with TypeScript 1.8, emitted modules are always in strict mode.
What's new in TypeScript · Microsoft/TypeScript Wiki · GitHub
上でも述べた通り、モジュールはモジュール内の宣言を別のモジュールから参照できるように export できます。 逆にいえば、モジュールは、別のモジュールの宣言を import できます。 ここでは import と export の宣言に触れます。
クラスや変数の宣言と同時にそれを export することができます。 (下記の例以外にもありそうです。)
// VariableStatement
export varidentifier1,identifier2 =...,...;
// Declaration
// ClassDeclaration
export classIdentifier {... }
// LexicalDeclaration
export letidentifier3,identifier4 =...,...;
export constidentifier5,identifier6 =...,...;
モジュール内で宣言した変数などの名前を指定して export することもできます。
export {name,localName asexportName,... };
上の例では、モジュール内で宣言されたname という名前の宣言をname という名前のまま export し、モジュール内で宣言されたlocalName という宣言をexportName という名前で export します。
さらに、「default」 という特別な名前で export する構文も存在します。 (下記の例のすべてのIdentifier は省略可能です。)
// HoistableDeclaration[Default]
// FunctionDeclaration[Default]
export default functionIdentifier (...) {... }
// GeneratorDeclaration[Default]
export default function *Identifier (...) {... }
// ClassDeclaration[Default]
export default classIdentifier {... }
// AssignmentExpression[In] (下記以外もいろいろある)
export default (...) => {... } // ArrowFunction
export default 100;
また、別のモジュールから import した宣言を export することもできます。
export * from "module-specifier";
export {name,localName asexportName,... } from "module-specifier";
export する際の名前が重複しているとエラーになるようです。 (なので default export はモジュールにつき 1 つまで。)
まず、宣言を参照しない単純な import を示します。 これにより import 先のモジュールコードの処理が走ります。 (そのモジュールの宣言を参照することはできません。)
import "module-specifier";
Import 先モジュールが export している宣言の名前を指定してモジュール内で参照できるようにする import 方法は次の通りです。
import {importedBinding1,identifier asimportedBinding2,... } from "module-specifier";
上の例では、importedBinding1 という名前で export されている宣言をimportedBinding1 という名前のまま import し、identifier という名前で export されている宣言をimportedBinding2 という名前で import します。
Import 先モジュールが export しているすべての宣言を含むオブジェクトを得るような import 方法もあります。
import * asimportedBinding from "module-specifier";
上の例では、importedBinding に Module Namespace オブジェクトが結び付けられます。 Module Namespace オブジェクトは、import 先モジュールが export している宣言をプロパティとして持つオブジェクトです。 (詳しくは後述。)
また、特別な名前 「default」 で export されている宣言を受け取る import 構文もあります。 この構文は上の 2 つの構文と組み合わせて使うことができます。
// モジュール "module-specifier" で default export された宣言をimportedDefaultBinding という名前に結びつける
importimportedDefaultBinding from "module-specifier";
// 下のように組み合わせて使うこともできる
importimportedDefaultBinding, {importedBinding1,identifier asimportedBinding2,... } from "module-specifier";
importimportedDefaultBinding, * asimportedBinding from "module-specifier";
Import 時に*
を使うと作られる Module Namespace オブジェクトについては次のように書かれています。 このオブジェクトのプロパティを通してモジュールがエクスポートしてる宣言にアクセスできるようになる、って理解でいいと思います。
A Module Namespace Object is a module namespace exotic object that provides runtime property-basedaccess to a module’s exported bindings. There is no constructor function for Module Namespace Objects. Instead, such an object is created for each module that is imported by an ImportDeclaration that includes aNameSpaceImport (See 15.2.2).
In addition to the properties specified in 9.4.6 each Module Namespace Object has the own following properties:
ECMA-262 6th (26.3 Module Namespace Objects)
A module namespace object is an exotic object that exposes the bindings exported from anECMAScript Module (See 15.2.3). There is a one-to-one correspondence between the String-keyed own properties of a module namespace exotic object and the binding names exported by the Module. The exported bindings include any bindings that are indirectly exported using export * export items.
ECMA-262 6th (9.4.6 Module Namespace Exotic Objects)
肝心のモジュールをどう実行するのか、あるいはモジュール内でモジュールを読み込むのはどういう処理なのか、については実装依存ぽいですね。
WHATWG にLoader ってのがあるけど、これがモジュール読み込みの仕様を決めるんですかね。 『This specification describes the behavior of loadingJavaScript modules from aJavaScript host environment』 ってあるのでモジュールの読み込み関係っぽいけどちゃんと読んでないのでわかんないです。
最近HTML Standard の 8.1.3.7.1 節 「HostResolveImportedModule(referencingModule, specifier)」 でブラウザからのモジュールの参照の方法が定義されましたよ
Loader との関係はまだふわっとしていそう
というコメントをid:wakabatan から貰いました!
読んでみたところ、モジュールスクリプト自体はHTML Standard の 8.1.3.2 節 「Fetching scripts」 に書かれているようにフェッチされて、environment settings object のmodule map に格納されて、スクリプト実行時にはフェッチ済みのものがあればそれが使われ、なければエラー、って感じのようです。 モジュールスクリプト自体は URL で一意に識別されるみたいですね。 (module map のキーが、モジュールスクリプトの URL です。)
TypeScript のモジュールの機能を使って開発した JS を web 用にデプロイすることを考えると、モジュール探索のベースとなる URL を指定できるような仕組みだと嬉しいと思うんですが、今のところそういう感じではなさそうですね。 (まあ TypeScript →JavaScript の変換のところでなんかやればいいという話ではあります。)
await
はモジュールでのみFutureReservedWordみたいです。
await is only treated as a FutureReservedWord when Module is the goal symbol of the syntactic grammar.
ECMA-262 6th (11.6.2.2 Future Reserved Words)
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。