Skip to content
uupaa edited this page Sep 6, 2018 · 12 revisions

このエントリでは、ES Modules の Syntax について説明します。

import ...export ... は最上位ブロックに記述する必要があります。
if文や関数の内部、ブロックスコープの中には記述できません。

import { bar } from "./bar.js"; // -> OK
{
  export piyo;                  // -> Syntax error (is not top level block)
}
if (flag) {
  import foo from "./foo.js";   // -> Syntax error (is not top level block)
}
import { buz } from "./buz.js"; // -> OK
function bar() {
  export let buz = 1;           // -> Syntax error (is not top level block)
}
export piyo;                    // -> OK
  • import ... は、コードの途中や末尾に記述されていてもコードの先頭に書かれている物として解釈されます。これは JavaScript における hoisting(巻き上げ) に近い振る舞いです。
  • export ... は、最上位ブロックに記述する限り、ファイルのどこにでも記述することができます。
  • export let num = 123import { num } from ... で読み込むと、変数 num は読み込み専用になります。

Named and Default

import には 名前付き( named import ) と、名前なし( default import ) の二種類が存在します。

  • import { member, ... } from path (named import)
  • import * from path (default import)

export にも named exportdefault export が存在します。
名前を特に指定せず、1つだけキーワードを export するのが default export です。
復数のキーワードを名前付きで export するのが named export です。これらの混在も可能です。

  • export ... (named export)
  • export default ... (default export)

キーワードを export せず副作用のみを期待するモジュールも記述できます(Re-exporting)。

Default export

export default ... 構文を使用した export を default export と呼びます。

default export を import するには import foo from "..."import { default: foo } from "..." 構文を使用します。

  • export default ... で名前を省略したまま export できます。

    // foo.js
    export default 123; // 値123 を default export
    // main.js
    import foo from "./foo.js"; // 123 を fooという名前で参照
    console.log(foo); // -> 123

    import foo from "./foo.js";
    import { default as foo } from "./foo.js"; でも同じ結果になります。

  • 値の他に関数やクラスも export できます。 export default はちょっと特殊な式となっており let const var を併記できません。直感的ではありませんが、なにやら理由があるようです。

    const value = 123;
    class Foo {}
    function fn() {}
    //
    export default value;                         // -> OK
    export default Foo;                           // -> OK
    export default fn;                            // -> OK
    export default 1;                             // -> OK
    export default `current time: ${Date.now()}`; // -> OK
    export default class {}                       // -> OK
    export default function() {}                  // -> OK
    export default let 1;                         // -> SyntaxError (let)
    export default const 1;                       // -> SyntaxError (const)
    export default var 1;                         // -> SyntaxError (var)

Named export

export ... 構文を使用し、名前を付けた export を named export と呼びます。

  • export ... で名前を付けて export できます。

    // mod.js
    export let foo = 1;
    export function bar() {}
    // main.js
    import { foo, bar } from "./mod.js";
    console.log(foo, bar); // -> 1, function bar
  • export { member, ... } の形で、まとめて export もできます。

    // mod.js
    let foo = 1;
    function bar() {}
    
    export { foo, bar };

Default import

  • import * as ... from ... で名前を省略して import できます。

    // mod.js
    export const foo = 123;                   // foo という名前で export
    export function bar(x) { return x * x; }  // bar という名前で export
    export class Buz {}                       // Buz という名前で export
    // main.js
    import * as mod from "./mod.js";
    console.log(mod.foo); // -> 123

Named import

  • import { member, ... } from ... で名前を指定して import できます。

    // mod.js
    export const foo = 123;                   // foo という名前で export
    export function bar(x) { return x * x; }  // bar という名前で export
    export class Buz {}                       // Buz という名前で export
    // main.js
    import { foo, bar, Buz } from "./mod.js";
    console.log(foo); // -> 123
  • まとめて import することもできますし、何度かに分けて import することもできます。

    import { foo, bar } from "./mod.js";
    import { Buz } from "./mod.js";
  • as キーワードを使い、別名(local name)で import できます。名前の衝突を回避できます。

    import { foo, bar, Buz } from "./mod.js";
    import { Buz as Buz2 } from "./mod.js";   // -> OK
  • 既に存在する名前で import するとSyntaxErrorになります。

    import { foo, bar, Buz } from "./mod.js";
    import { foo, bar } from "./mod.js";      // -> Uncaught SyntaxError: Identifier 'foo' has already been declared
    import { Buz } from "./mod.js";           // -> Uncaught SyntaxError: Identifier 'Buz' has already been declared

Unexported

何も export しないモジュールも定義できます。また、副作用を期待した import もできます。

// mod.js
document.body.style.backgroundColor = "red"; // 画面を赤に染める
console.log("mod3.js called"); // コンソールに文字列を出力する
// main.js
import "./mod.js"; // mod.js を import すると画面が赤くなり、コンソールに文字列がでる

Re-exporting

mod1.js を mod2.js で丸ごと export できます。

// mod1.js
export function foo() {}
export function bar() {}
// mod2.js
export * from "./mod1.js";

mod1.js で定義された名前をそのまま export せず、 as で名前を変更して export することもできます。

// mod2.js
export { foo as foo2, bar } from "./mod1.js";

Pluggable module pattern

Named export, Default export と import() を活用することで、未知のプラグインを動的に読み込むコードを記述することが可能です。

以下のコードは、GoogleMapsProvider を Named export と Default export の両方で記述しており、このように記述することで、 import ... による静的な読み込みと import() による動的な読み込みの両方から利用可能となります。
このような記述の仕方をプラガブルモジュールパターン(またはプラグインパターン)と呼んでいます。

export class GoogleMapsProvider { // [!] Named export
  load(apiKey) { // @arg GoogleMapsAPIKey - "AIzaSyBez8zjDlp3oOzy_1cVpkIHwdyH_Yrt..."
                 // @ret Promise
    return new Promise((resolve, reject) => {
      const script = document.createElement("script");
      script.onload = resolve;
      script.src = `//maps.googleapis.com/maps/api/js?key=${apiKey}&v=3&language=jp`;
      document.head.appendChild(script);
    });
  }
}
export default GoogleMapsProvider; // [!] Default export

静的に読み込む場合は以下のようにします。

import { GoogleMapsProvider } from "./plugin-dir/GoogleMapsProvider.js";

GoogleMapsProvider.load(apiKey);

プラグインとして動的に読み込む場合は、以下のようにします。

async function loadPlugin(pluginDir, pluginName) { // @arg PluginNameString - "Foo"
                                                   // @ret Promise
  const { default: Provider } = await import(`${pluginDir}/${pluginName}Provider.js`);
  return Promise.resolve(Provider);
}

const pluginDir = `./plugin-dir`;
loadPlugin(pluginDir, "GoogleMaps").then(Provider => {
  Provider.load(apiKey);
});

Tips

  • import name from ... は default export 用の構文で、
    import { name } from ... が named export 用の構文です。
  • import name from ...
    import { default as name } from ... の shorthand です。
  • export default は module の中に一つだけ記述できます。復数あるとエラーになります

named vs default

基本的な考え方として、

  • モジュールが1つの class のみを export する場合は export default を使います。
  • 1つの関数を export する場合も同様に export default を使用します。
  • プラグイン機構を組み込む場合は export ...export default ... を併用します。
  • 上記以外なら named export を使用します。

ES Modules はまだ新しくツールのサポートは改善途中にあります、 export default を使うと、IDEやトランスパイラといった関連ツールが上手く動作しないケースがあります。

また トランスパイルを必要とする CommomJS のコードが多く存在している状況においては、 default にこだわらず named を選択するという柔軟な思考も必要になるでしょう。

このエントリで紹介した構文以外でも、 default import と named import を組み合わせた書き方などが存在します。 詳しく知りたい方は、以下のエントリを参照してください。