ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

ECMA-262 6th (ES 2015) のモジュールについて (入門編)

はじめに

ECMA-262 6th がリリースされて ECMAScript (JavaScript) にモジュールの機能が導入されたわけですが、実際どんなものなのかはっきりわかってなかったので ECMA-262 6th を読んでみました。 このブログ記事はそのまとめです。 実践的な話 (TypeScript でモジュールの機能を使うことについてや、既存プロジェクトへのモジュールの導入など) はまた今度書くつもりです。

この文書では歴史的背景などには触れずに坦々と ECMA-262 を読んでいくだけですので、歴史的背景などを含めた解説を読みたい方は最後に挙げている関連ページなどを読むと良いと思います。

ECMA-262 6th を読む

ECMA-262 6th (ECMAScript 2015 Language Specification) の中で、モジュールに関して述べられているところを引用しながらコメントしていきます。

Introduction

Goals for ECMAScript 2015 include providing better support for large applications, library creation, and for use of ECMAScript 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」 が含まれていますが、モジュールの導入はまさに大きなアプリケーションやライブラリ記述のよりよいサポートのためのものといえるでしょう。

モジュールの概要

Large ECMAScript 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) はグローバル環境となってます。

つまり、モジュールでの宣言はモジュール環境に閉じていて、グローバル環境からは (直接は) モジュールでの宣言を参照することはできないわけです。 一方で、モジュール環境の外側の環境はグローバル環境なので、グローバル環境での宣言をモジュールから参照することはできます。

よって、これまでモジュールを使っていないプログラムにモジュールを導入する場合は、どこからも参照されない箇所 (すなわちプログラムのエントリポイント) から少しずつモジュールにしていくという作戦をとれます。

https://docs.google.com/drawings/d/1oCy-Di73qDcmW6PsIlKk3OtYH1a5_2Vgvk6WhSiHBo8/pub?w=696&h=391

ES 2015 のモジュールの仕様が Node.js のモジュールの仕組みや AMD を元に作成されたであろう *1 ことを考えると、納得の仕様ですね。

モジュールのソースコードはモジュールコードとして読み込まれる

There are four types of ECMAScript code:

  • (略)
  • Module code is source text that is code that is provided as a ModuleBody. 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 nested FunctionDeclaration, FunctionExpression, GeneratorDeclaration, GeneratorExpression, MethodDefinition, ArrowFunction, ClassDeclaration, or ClassExpression.
ECMA-262 6th (10.2 Types of Source Code)

ソースコードの種類として、モジュールコードと呼ばれるものがあります。 ES 2015 で追加されました。 モジュールのソースコードはモジュールコードとして解釈されるわけです。

  • Module code is always strict mode code.
ECMA-262 6th (10.2.1 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

Import と Export

上でも述べた通り、モジュールはモジュール内の宣言を別のモジュールから参照できるように export できます。 逆にいえば、モジュールは、別のモジュールの宣言を import できます。 ここでは import と export の宣言に触れます。

Export の構文

クラスや変数の宣言と同時にそれを export することができます。 (下記の例以外にもありそうです。)

// VariableStatement
export var identifier1, identifier2 = ..., ...;

// Declaration
// ClassDeclaration
export class Identifier { ... }
// LexicalDeclaration
export let identifier3, identifier4 = ..., ...;
export const identifier5, identifier6 = ..., ...;

モジュール内で宣言した変数などの名前を指定して export することもできます。

export { name, localName as exportName, ... };

上の例では、モジュール内で宣言された name という名前の宣言を name という名前のまま export し、モジュール内で宣言された localName という宣言を exportName という名前で export します。

さらに、「default」 という特別な名前で export する構文も存在します。 (下記の例のすべての Identifier は省略可能です。)

// HoistableDeclaration[Default]
// FunctionDeclaration[Default]
export default function Identifier (...) { ... }
// GeneratorDeclaration[Default]
export default function * Identifier (...) { ... }

// ClassDeclaration[Default]
export default class Identifier { ... }

// AssignmentExpression[In] (下記以外もいろいろある)
export default (...) => { ... } // ArrowFunction
export default 100;

また、別のモジュールから import した宣言を export することもできます。

export * from "module-specifier";
export { name, localName as exportName, ... } from "module-specifier";

export する際の名前が重複しているとエラーになるようです。 (なので default export はモジュールにつき 1 つまで。)

Import の構文

まず、宣言を参照しない単純な import を示します。 これにより import 先のモジュールコードの処理が走ります。 (そのモジュールの宣言を参照することはできません。)

import "module-specifier";

Import 先モジュールが export している宣言の名前を指定してモジュール内で参照できるようにする import 方法は次の通りです。

import { importedBinding1, identifier as importedBinding2, ... } from "module-specifier";

上の例では、importedBinding1 という名前で export されている宣言を importedBinding1 という名前のまま import し、identifier という名前で export されている宣言を importedBinding2 という名前で import します。

Import 先モジュールが export しているすべての宣言を含むオブジェクトを得るような import 方法もあります。

import * as importedBinding from "module-specifier";

上の例では、importedBinding に Module Namespace オブジェクトが結び付けられます。 Module Namespace オブジェクトは、import 先モジュールが export している宣言をプロパティとして持つオブジェクトです。 (詳しくは後述。)

また、特別な名前 「default」 で export されている宣言を受け取る import 構文もあります。 この構文は上の 2 つの構文と組み合わせて使うことができます。

// モジュール "module-specifier" で default export された宣言を importedDefaultBinding という名前に結びつける
import importedDefaultBinding from "module-specifier";
// 下のように組み合わせて使うこともできる
import importedDefaultBinding, { importedBinding1, identifier as importedBinding2, ... } from "module-specifier";
import importedDefaultBinding, * as importedBinding from "module-specifier";

Module Namespace オブジェクト

Import 時に * を使うと作られる Module Namespace オブジェクトについては次のように書かれています。 このオブジェクトのプロパティを通してモジュールがエクスポートしてる宣言にアクセスできるようになる、って理解でいいと思います。

A Module Namespace Object is a module namespace exotic object that provides runtime property-based access 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 a NameSpaceImport (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 an ECMAScript 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)

モジュール読み込みについて

肝心のモジュールをどう実行するのか、あるいはモジュール内でモジュールを読み込むのはどういう処理なのか、については実装依存ぽいですね。

WHATWGLoader ってのがあるけど、これがモジュール読み込みの仕様を決めるんですかね。 『This specification describes the behavior of loading JavaScript modules from a JavaScript host environment』 ってあるのでモジュールの読み込み関係っぽいけどちゃんと読んでないのでわかんないです。

Web ブラウザでのモジュール読み込みについて (追記)

最近 HTML Standard の 8.1.3.7.1 節 「HostResolveImportedModule(referencingModule, specifier)」 でブラウザからのモジュールの参照の方法が定義されましたよ
Loader との関係はまだふわっとしていそう

というコメントを id:wakabatan から貰いました!

読んでみたところ、モジュールスクリプト自体は HTML Standard の 8.1.3.2 節 「Fetching scripts」 に書かれているようにフェッチされて、environment settings objectmodule 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)

おわり

  • 軽くググっても ES 2015 のモジュールの import や export についてよくわかんなかったので ECMA-262 を読んでまとめました。
    • 雑ですが。
    • ミス等あったらご指摘ください。
  • 現時点では直接 ES 2015 のモジュールを解釈できる ES 実行環境はないと思いますが、TypeScript のモジュールを使うにしろ *2、Babel などのトランスコンパイラでモジュールを使うにしろ、ES 2015 のモジュールについて理解しておくとモジュールを扱いやすいと思います。

Js.next: Ecmascript 6

Js.next: Ecmascript 6

関連ページ

*1:推測です

*2:TypeScript のモジュールは ES 2015 ベース