モジュールとは何か?(スクリプトとの違い)
「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> なら、var は window に作成されますが、
モジュールではモジュールスコープ内に留まります。
グローバルスコープにしたいなら(ブラウザの場合):
// module.js
window.myGlobal = 42; // あえてグローバルにしたいときだけ、こう書くtop-level this が undefined
従来 <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.js と main2.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.jsmathUtil.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] 20index.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経由で明示される
コメント