読者です 読者をやめる 読者になる 読者になる

uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかFregeとかJavaとか -

ES6のモジュールを解説してみた

js es6 javascript ECMAScript import export package module

はじめに

ES2015(以降、ES2015はES6と書く。長いので)のモジュール機能を説明する。

ES6のモジュールはNode.JSとかCommonJSのそれと近いので、それらの使用経験があれば類推できる部分が大きい。しかし、「Node.JSのモジュールの使用経験がない人」にとってはつらいかもしれない。なので、前提知識をなるべく必要としない形でES6モジュールの用法についてまとめてみる。

予備知識

JSにおいてモジュールとは何か・何だったか

もともとJSに言語仕様としてのモジュール(Javaで言うpackage/import、rubyで言うrequireなどで実現される機能)はなかった。では、従来のJSでモジュール管理はどういうものだったかというと、.jsという拡張子を持ったテキストファイルから文字列を読み込んでevalっぽいもので評価して中でexportされているオブジェクトを識別子にバインドするような処理で実現されていた。ちなみに、インポートされるシンボルで使用されるピリオド(.)は、名前空間の区切りとかではなく、objectのプロパティを実行時にたどっている。

ES6においてモジュールとは何か

前述した、モジュール機構を実装する既存のライブラリの呼び出しに対するシンタックスシュガー的なものを、ECMAScript言語仕様できっちり規定して、将来に渡っても実装に左右されないようにした。内部機構を隠蔽することにもなっており、非同期読み込みなどの効率化を可能としたり、静的解析や事前コンパイルなど最適化を可能としたり、型チェックなどにも寄与できるなど利点が大きい。ES6のモジュールは、ES6の価値の主要なものの一つである。みんなで使おう。

exportとimport

ES6モジュールを実現する構文は、exportとimportの2つの宣言である。

export
使用される側のモジュール中で使用し、「そのモジュールで定義している値やオブジェクトのうち、モジュール外から、モジュールを利用する側からどれを利用可能とするか」を指定する。
import
モジュールを使用しようとするコード側で以下を指定する
  • どのモジュールを使うかの指定
  • 指定モジュール内でexport指定されている何かを、どのように識別子にバインドするか

    モジュール利用の流れ

    CommonJS、ES6に共通して、JSにおけるモジュールの利用は、以下の2つの段階に区切って考えるとわかりやすい。

    • (1)定義使用とするモジュール側で、外部に提供したい値の集合やオブジェクトにexport指定することで、値やオブジェクトを「返す」ように定義し、
    • (2)importの構文で指定された方法で、その「返された何か」を識別子に適切にバインドする。

    (1)の段階でのモジュールからの値の返し方には、以下の3つがある。

    1. named export: 複数値をobjectで返す
    2. default export: 単一値を返す
    3. named exportと, default exportの混在: 単一値と複数値の両方を返す

    以降、値がどう返されるかをそれぞれ見ていく。

    export編

    1. named export: 複数値をobjectで返す

    export var foo = "abc"
    export const bar = "def"
    export function hoge() { return  "ABC" };
    

    この場合、

    {
       foo: "abc",
       bar: "def",
       hoge: function(){ return "ABC" },
       __esModule: true
    }
    

    というようなobjectを返す。exportは、個々の変数定義の箇所ではなく、以下のようにまとめて指定することもできる。

    var foo = "abc"
    const bar = "def"
    function hoge() { return  "ABC" };
    exports { a, bar, hoge }
    

    ちなみに {a, bar, hoge}はES6で導入された記法で、{a:a, bar:bar, hoge:hoge}の略記法である。

    以下のようにasでリネームしたものを公開することもできる。

    var foo = "abc"
    const bar = "def"
    function hoge() { return  "ABC" };
    exports { a as X, bar as Y, hoge as Z }
    

    2. default export: 単一値を返す

    export default class MyClass { // 名前のあるクラス
      foo() { return "FOO" }
    }
    

    この場合、関数値としてのMyClass関数を単一値として返す。

    export default class { // 無名クラス
      foo() { return "FOO" }
    }
    

    この場合、関数値としての無名クラスを単一値として返す。

    export default 123
    

    この場合、値123を単一値として返す。

    export default {indent:1}
    

    この場合、objectを単一値として返す。

    上記のように、default exportでは任意の型の値、クラスや数値、objectなどを返すことができる。返すことができる値は単一値であるから1つだけで、export defaultが1つのJSファイル中に複数あれば、後勝ちで上書きされる。

    3. named export/default exportの混在: 単一値と複数値の両方を返す

    export default class MyClass {
      foo() { return "FOO" }
    }
    export var foo = "abc"
    export const bar = "def"
    export function hoge() { return  "ABC" };
    

    上では、関数値としてのMyClass関数を単一値として返すのに加え、a,bar,hoge複数値として表現するobjectを返す。

    具体的にはどんな値が返るのか?

    ES6でコンパイルしたモジュールでは、モジュールの返り値は常にobjectであり、単一値はプロパティ"default"の値として返る*1

    {
      "default": 単一値, // default exportされた値
      複数値1の名前: 複数値1, // named exportされた値1
      複数値2の名前: 複数値2, // named exportされた値2
      複数値3の名前: 複数値3, // named exportされた値3
      __esModule: true
    }
    

    つまり、単一値のexportは、複数値の特別な場合として実現されていて、exportされる単一値というのは、実際には複数値として返されるobjectの"default"というプロパティの値のことである。"default"プロパティの存在は、import文のシンタックスシュガーで隠蔽される。

    importしたモジュールをexport

    export {x} from "mod";
    export {v as x} from "mod";
    export * from "mod";
    

    説明は略。

    import編

    import文について説明する。import文は単一値を受けとるか、複数値を受けとるか、その両方を受けとるか、に応じた4パターンがある。

    項番 書式 対象モジュール
    1 import 単一値を受けとる識別子 from "モジュール指定" import A from "module" named exportが使用されたモジュール
    2 import 複数値を受けとる指定 from "モジュール指定"
    • (1)複数値を{識別子1,識別子2..}で受ける。分割代入に似た形式。
    • (2)複数値を{識別子1 as 別名1,識別子2 as 別名2..}で受ける。
    • (3)複数値全体を単一のobjectで受ける(名前空間import)。
    • (1)import {a,b} from "module"
    • (2)import {a as X,b as Y} from "module"
    • (3)import * as X from "module"
    default exportが使用されたモジュール
    3 import 単一値を受けとる識別子, 複数値を受けとる指定 from "モジュール指定"
    • 2の(1)-(3)パターンに、単一値を受けとる識別子を追加したもの
    • (1)import A,{a,b} from "module"
    • (2)import A,{a as X,b as Y} from "module"
    • (3)import A,* as X from "module"
    named exportとdefault exportの両方が使用されたモジュール
    4 import "モジュール指定" import "module" 副作用を期待するモジュール

    importにおけるモジュール指定の方法

    from句でモジュールを指定する。呼び出す側のJSファイルと同じ、もしくはサブディレクトリに配置されているJSファイルで定義されているモジュールを読み込むには、「./」や「../」で始まるファイル名の相対指定で指定すればよい(拡張子不要)。なおNode.jsではnode_modules配下のモジュールは「./」では始めないくてよい。この仕組みはNodeの仕様だと思うが、ES6でも定義された仕様なのかは知らない。ちなみにWebPack配下だと、import文でnpmに含まれているCSSが読み込めるよねー。偉い。すごい。

    「named系」の流れと「default系」の流れ

    結局、JSのモジュールの使用は、named系とdefault系の独立した2系列があり、以下の区別である。

    named系
    モジュールは名前と値のペアの集合を返す。named importを通じて、元の名前をそのままもしくはasでリネームして使用する。
    default系
    モジュールは単一値を返す。default importを通じてimportする側が新たに名前をつけ、元の名前を意識せずに使用する(名前は元々無いかもしれない)。もっとも、単一値としてobjectを返すこともできるので、スコープ的に使用するobjectを返して、そのプロパティの名前を使うということもできるし普通である。
    named/default混在系
    jQueryの$やjQueryのようにメインのオブジェクトが単一でリネームで衝突回避できるようになっていて欲しいためにトップレベルのメインオブジェクトをdefault exportにして、その他を選別的にnamed exportするケースなどが想定ユースケースの一つ。

    
 図にまとめるとこうである。

    ちなみにCommonJSで定義されているのはnamed系に対応する方式(exportsのプロパティに代入)だけで、default exportの用法に近い方式(module.exportsに代入)はNode.JSの機能らしい。

    使い分け

    default exportでもobjectを返せば名前と値の複数値を返すことができるし、named exportでも1つのキーで単一値を返せる。なので両者の機能範囲は実は重なる。

    実際、いずれの形式でも使用できるように提供されているライブラリもある。例えばReactがそうである:

    import React from "react"
    
    export default class Hoge extends React.Component {
    }
    
    import {Component} from "react"
    
    export default class Hoge extends Component {
    }
    

    前者だと、常に修飾して使用するので使用箇所でやや煩雑だが、わかりやすいかも。Javaで言うFQCNで使用するイメージ。

    後者だと、使用するクラスが増えるたびに列挙が増やしていかなければならないので面倒ではあるし、修飾無しなので、衝突の恐れもある。babelはnamed import時の名前衝突をエラーにしてくれるので都度asをすれば良い、という考えもあるかもしれないが、アドホックにたまたま衝突したことを理由として名前を変え、さらにどう変えるべきかを考えるのはうれしいことではない。メリットとしては、ソース冒頭を見るだけでこのコードはどんなクラスを使っているかがわかる。

    JSの仕様策定者は、default exportをより推奨したいと考えていて、記法もより簡潔にしたとのこと。

    例をベースにした詳しい説明

    複数値のimportの例

    named export、すなわちモジュールが複数値で値を返す場合、その値は具体的にはobjectで返されている。値はobjectのプロパティ名に対応する名前を持つ(named exportとも呼ばれる所以)。

    (例1-1)

    // module1.js
    export const 1
    export var x = [1,2,3]
    
    import {x, y} from "./module1"
    
    console.assert(x==1)
    console.assert(y.toString()=="1,2,3")
    

    表記上は、モジュールが返すオブジェクトが{a,hoge}に分配代入されるイメージだが、実際の分配代入ではない(ネスト等はできない)。

    (例1-2)

    // module2.js
    export default 1
    
    import X from "./module2"
    
    console.assert(X==1)
    

    モジュールが返すオブジェクトがXに代入される。

    (例1-3)

    // module3.js
    export const x = 1
    export var y = [1,2,3]
    
    import {x as foo, y as bar} from "./module3"
    
    console.assert(foo==1)
    console.assert(bar.toString()=="1,2,3")
    

    右辺がobjを返すとして、以下のイメージ(実体は少々違う)。

    X = obj.a
    Y = obj.hoge

    単一値のimportの例

    default export、すなわち単一値を返すモジュールはimport x from ..のように指定するとその単一値がxに保持される。

    (例2-1)

    // module4.js
    export default 1
    
    import x from "./module4"
    
    console.assert(x==1)
    

    module4の返す値がxに代入される。xは任意につけてよい名前である。

    複数値、単一値両方の混在の例

    単一値と複数値の両方を返すモジュールからの値を以下のように受けとることができる。fooには単一値が代入され、{x, y}には複数値が分配される。

    (例2-2)

    // module5.js
    export default 1
    export var x = 2
    export var y = "abc"
    
    import foo,{x,y} from "./module5"
    
    console.assert(foo==1)
    console.assert(x==2)
    console.assert(y=="abc")
    

    トピックス

    defaultとnamedが一致しなかったら?

    default exportの指定のみしかない単一値のみを返すモジュールを、named importすると、例えば {foo, bar} fromでimportすると、foo, barはundefinedとなる。

    named exportの指定のみしかない、複数値を返すモジュールを、default importすると、例えば X from ..でimportすると、Xはundefinedになる。

    import * as Xとimport Xの違い

    import * as X from "..."は、named系であり、named exportされたモジュール(複数値を表現するobject)の指定を期待している。「ばらばらで来たものをまとめる」というイメージ。もしfromにdefault exportされたモジュールのみが指定されれば、Xには{default: 単一値 }というオブジェクトが得られる。

    import Xはdefault系であり、default exportされたモジュール(単一値を表現している)を期待している。「単一値ならそのまま、まとまって来たものなら後でバラしてつかう」というイメージ。もしfromにnamed exportされた複数値を表現するobjectのみを返すモジュールが指定されれば、Xにはundefinedが得られる。

    まとめ

    ES6のモジュールは良く考えられてうまくできているよー。

    参考

    http://d.hatena.ne.jp/jovi0608/20111226/1324879536 http://www.2ality.com/2014/09/es6-modules-final.html

    *1:なお、Node.jsでは、import defaultに相当する場合、実際に単一値を返すのが規約である(module.exportsに代入する)。babelでは、このようなNode.jsのモジュールもES6のimport構文で扱えるように、__esModuleを含まない場合、{ default: 単一値 }にラッピングするようなコードにコンパイルされる。

    広告を非表示にする