[JavaScript講座] ES Modules の基本

当ページのリンクには広告が含まれています。
目次

モジュールとは何か?(スクリプトとの違い)

「1ファイル = 1モジュール」という基本イメージ

ECMAScript Module(以下 ESM)は、

1つのファイルが1つのモジュール

という単位で、コードを分割・再利用する仕組みです。

  • それぞれのモジュールは 自分専用のスコープ を持つ
  • 他モジュールに公開したい値だけ export で明示する
  • 他モジュールの公開された値を import で使う
// mathUtil.js  ← モジュールA
export function add(a, b) {
  return a + b;
}
// main.js      ← モジュールB
import { add } from "./mathUtil.js";

console.log(add(1, 2)); // 3

スクリプトとの違い

<script src="...">(非モジュール):

  • すべて 1つのグローバル空間(window) に読み込まれる
  • どのファイルで定義した変数でも、グローバルなら全部混ざる
  • 読み込み順に実行される(順番バグが起きやすい)

<script type="module" src="...">(モジュール):

  • 各ファイルは 独立したモジュールスコープ を持つ
  • 明示的に export したものだけが他ファイルから import できる
  • 依存関係に基づいて実行順が決まる(静的解析できる)

export の基本

名前付きエクスポート

// mathUtil.js
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export function sub(a, b) {
  return a - b;
}
  • export を前に付けると、その識別子が「モジュール外部に公開」されます
  • 1つのモジュールから いくつでも 名前付きエクスポートしてよい

別の書き方(最後にまとめて export):

const PI = 3.14159;
function add(a, b) { return a + b; }
function sub(a, b) { return a - b; }

export { PI, add, sub };

リネームして公開もできる:

export { add as addNumbers, sub as subtractNumbers };

デフォルトエクスポート(1モジュールに1つまで)

// logger.js
export default function log(message) {
  console.log("[LOG]", message);
}
  • export default は 1モジュールにつき1つまで
  • 「このモジュールのメインの値はこれ」という意味合い
  • import log2 from "./logger.js"; のように{} なし&好きな名前で import できる(importする側が好きな名前を付けてよい)

クラスでもよく使う:

// User.js
export default class User {
  constructor(name) {
    this.name = name;
  }
}

まとめ & 再エクスポート(re-export)

他モジュールのエクスポートをそのまま再公開することもできます。

// mathUtil.js
export function add(a, b) { return a + b; }
export function sub(a, b) { return a - b; }
// index.js
export { add, sub } from "./mathUtil.js";
// または
export * from "./mathUtil.js"; // 名前付きエクスポートをまるごと再エクスポート

ライブラリの「エントリーポイント」などで多用されます。

import の基本

名前付きインポート

// main.js
import { add, sub } from "./mathUtil.js";

console.log(add(1, 2));
console.log(sub(5, 3));
  • import { 名前 } from "モジュールパス";
  • {} の中は エクスポート側の名前 と一致させる必要があります

リネームして受け取る:

import { add as addNumbers } from "./mathUtil.js";

addNumbers(1, 2);

デフォルトインポート

// logger.js
export default function log(message) {
  console.log(message);
}
// main.js
import log from "./logger.js"; // 中カッコなし

log("hello");
  • デフォルトエクスポートは 中カッコなし でインポート
  • 受け取る名前は自由(エクスポート側の名前に縛られない)

デフォルト+名前付きを同時にインポート:

// util.js
export default function defaultFn() {}
export function foo() {}
export function bar() {}
// main.js
import defaultFn, { foo, bar } from "./util.js";

名前全部をまとめてインポート

import * as MathUtil from "./mathUtil.js";

console.log(MathUtil.add(1, 2));
console.log(MathUtil.PI);
  • 名前付きエクスポートを オブジェクト1つにまとめて受け取る 形
  • 名前空間オブジェクトのようなイメージ

相対パス import とモジュール解決のイメージ

ブラウザの場合:基本は URL

ブラウザの ESM では、from の右側は URL として解釈 されます。

import { add } from "./mathUtil.js";   // 相対URL(同じディレクトリ)
import { foo } from "../lib/foo.js";    // 一つ上のディレクトリ
import config from "/config/app.js";    // ルートからの絶対パス
  • ./:現在のファイルと同じディレクトリ
  • ../:1つ上
  • / 始まり:オリジンルートから(https://example.com/.../

注意(ブラウザ):

'react' のような 拡張子なし&スラッシュなしの「ベアインポート」 は、
素のブラウザでは解決できません(import maps やバンドラが必要)。
学習段階では、ブラウザで直接動かすときは必ず ./../ 付きで書く、と覚えると安全です。

Node.js の場合

Node.js では:

  • ./foo.js../bar.mjs のような相対パス
  • 'fs''path' のような組み込みモジュール名
  • 'react' のようなパッケージ名(node_modules から解決)

など、環境独自のルールで解決されます。

学習段階では、

モジュールパスの解決は「環境依存」であり、ブラウザとNodeではルールが違う

くらいのイメージで十分です。

<script type=”module”> と従来のスクリプトの違い

HTML側の書き方

<!-- 従来スクリプト -->
<script src="main.js"></script>

<!-- モジュール -->
<script type="module" src="main.js"></script>

またはインラインでも:

<script type="module">
  import { startApp } from "./app.js";
  startApp();
</script>

type=”module”の実行タイミング(defer 相当)

<script type="module"> は基本的に、自動的にdefer挙動になります。

  • HTML のパースが終わったあとで実行される
  • 複数ある場合、HTML に書かれた順番で実行される
  • 中で import されている依存モジュールも、必要に応じて先に読み込まれる

従来スクリプト:

  • 上から順に「読み込み完了したらすぐ実行」
  • <script> の位置によっては、HTML パースをブロックする

CORS やローカルファイルの注意

  • モジュールは CORS チェックが厳しい(別オリジンのJSを直接 import できないなど)
  • file:// でHTMLを直接開くと、モジュール読み込みがエラーになることが多い
    → 簡易的なHTTPサーバーを立てて http://localhost:... で開くのが基本

モジュールスコープ・strict mode・this の違い

モジュールは自動で strict mode

モジュール(ESM)では、ファイル全体が自動的に "use strict" 扱い になります。

  • "use strict" を先頭に書かなくても strict mode
  • つまり:
    • 暗黙のグローバル変数生成の禁止
    • this の扱いの変化(後述)
    • 一部の古い構文の禁止 など

例:暗黙のグローバルはエラー

// module.js(type="module" から読み込む想定)
x = 10; // ReferenceError(暗黙のグローバルは禁止)

モジュールスコープとグローバル

モジュール内の top-level で宣言した const / let / var は、モジュール専用スコープ に閉じ込められます。

// util.js (モジュール)
const secret = "hidden";
export const publicValue = 123;

// main.js (モジュール)
import { publicValue } from "./util.js";

console.log(secret);      // ReferenceError(同じページでも見えない)
console.log(publicValue); // 123

従来の <script> なら、varwindow に作成されますが、
モジュールではモジュールスコープ内に留まります。

グローバルスコープにしたいなら(ブラウザの場合):

// module.js
window.myGlobal = 42; // あえてグローバルにしたいときだけ、こう書く

top-level thisundefined

従来 <script>(非モジュール):

// script.js
console.log(this === window); // true(ブラウザ)

モジュール(type="module" 経由で読み込まれる JS):

// module.js
console.log(this);            // undefined
console.log(this === window); // false
  • モジュールのトップレベル this は 常に undefined
  • これは strict mode かつモジュールスコープであることの特徴の1つ

関数内の this は、呼び出し方次第で変わります。

モジュールの読み込みとキャッシュ

モジュールは1回だけ評価され、あとはキャッシュ

// counter.js
console.log("counter.js が評価された");

let count = 0;
export function inc() {
  count++;
  console.log(count);
}
// main1.js
import { inc } from "./counter.js";
inc();
// main2.js
import { inc } from "./counter.js";
inc();

main1.jsmain2.js を別々に読み込んでも:

  • counter.js 自体は 最初の1回だけ 実行される
  • 以降は 同じモジュールインスタンスが共有される(シングルトン)

エクスポートは「ライブバインディング」

// state.js
let value = 0;
export function set(v) {
  value = v;
}
export function get() {
  return value;
}
// main.js
import { set, get } from "./state.js";

console.log(get()); // 0
set(10);
console.log(get()); // 10
  • エクスポートされた変数はライブバインディング
    • エクスポート元の変数に対するエイリアス
    • import側からは再代入できない(読み取り専用)
  • モジュール側が値を更新すると、インポート側から見ても変わる

例:モジュール分割の実践イメージ

構成

/project
  index.html
  main.js
  mathUtil.js
  logger.js

mathUtil.js

export function add(a, b) {
  return a + b;
}

export function mul(a, b) {
  return a * b;
}

logger.js

export default function log(message) {
  console.log("[LOG]", message);
}

main.js

import log from "./logger.js";
import { add, mul } from "./mathUtil.js";

log(add(2, 3)); // [LOG] 5
log(mul(4, 5)); // [LOG] 20

index.html

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>ESM Example</title>
  </head>
  <body>
    <script type="module" src="./main.js"></script>
  </body>
</html>
  • main.js だけを <script type="module"> で読み込む
  • main.js から import することで、他ファイルの機能を使う
  • 依存関係はすべて import / export 経由で明示される

<<前へ(エラーと例外処理)

>>次へ(動的インポートとモジュール情報)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次