JavaScript:モジュール(import / export)
1学習の目的
- export と import でコードを複数ファイルに分割できるようになる。1ファイル1000行のコードから卒業できる。
- 設定値と処理を別ファイルに切り出し、変更に強い構造を作れるようになる。実務のフロントエンド開発はすべてこの形で書かれている。
2基礎解説
| やりたいこと | 書き方 | 備考 |
|---|---|---|
| 公開する | export const withTax = … | 名前付きエクスポート |
| 取り込む | import { withTax } from "./utils.js" | 波カッコが必要 |
| 代表を1つ公開 | export default function … | 1ファイルに1つだけ |
| 代表を取り込む | import greet from "./utils.js" | 波カッコなし |
- export を付けたものだけが外から見える。付けていない変数や関数は、そのファイルの中に閉じたまま。これが「隠せる」というモジュールの価値。
- 名前付きは import { a, b } と波カッコあり、デフォルトは import x と波カッコなし。この違いが最初の混乱ポイント。
- パスの ./ は必須。import … from "utils.js" と書くと外部ライブラリを探しに行ってしまう。ブラウザでは拡張子 .js も省略できない。
- HTMLから読み込むときは <script type="module" src="main.js"> とtype="module" が必須。付け忘れると import でエラーになる。
- import { withTax as tax } で別名を付けられる。名前が衝突したときや、短くしたいときに使う。
現場使用例:共通処理を utils.js に、設定値を config.js に、API通信を api.js に分離。ReactやVueも、すべてこの仕組みの上に成り立っている。
// ===== utils.js =====
export const withTax = (price, rate = 0.1) => Math.round(price * (1 + rate));
export const shippingFee = (price) => (price >= 5000 ? 0 : 600);
const INTERNAL = "外からは見えない"; // export なし
export default function greet(name) {
return `こんにちは、${name}さん`;
}
// ===== main.js =====
import greet, { withTax, shippingFee } from "./utils.js";
console.log(greet("田中")); // こんにちは、田中さん
console.log(withTax(1000)); // 1100
console.log(shippingFee(4800)); // 600
// 別名を付けることもできる
import { withTax as tax } from "./utils.js";
<!-- HTML側は type="module" が必須 -->
<script type="module" src="main.js"></script>
3基本ドリル(10問)
add がエクスポートされ add(3,7) が 10
const の前に export を付けるだけ。export const add = (a, b) => a + b;
export const add = (a, b) => a + b;
export を付けるだけで外から使えるようになる。付けなければそのファイル専用の関数として隠される。
A. import add from "./utils.js" B. import { add } from "./utils.js" C. import * add from "./utils.js" D. require("./utils.js") 選択★☆☆無料
解答 == B
名前付きエクスポートには何が必要か。
B
名前付きは波カッコ、デフォルトは波カッコなし。Aはデフォルトエクスポート用の書き方で、この場合は undefined になる。
出力 == 10 / 6(2行)
import { add, sub } from "./utils.js"; のようにカンマで並べる。
import { add, sub } from "./utils.js";
console.log(add(3, 7));
console.log(sub(10, 4));
必要なものだけを名指しで取り込むのが名前付きインポート。何を使っているかがファイルの先頭を見れば分かる。
____ const TAX_RATE = 0.1;
TAX_RATE がエクスポートされ 0.1
「輸出する」という意味の英単語。
export
設定値も export できる。関数だけでなく、定数やオブジェクトも同じように公開できる。
A. <script async> B. <script type="module"> C. <script defer> D. 特に不要 選択★☆☆無料
解答 == B
モジュールであることをブラウザに伝える必要がある。
B
これを忘れると「Cannot use import statement outside a module」というエラーになる。モジュールを使い始めたら必ず付ける。
出力 == こんにちは、田中さん
export default で公開し、import hello from "./greet.js" と波カッコなしで受け取る。
// greet.js
export default function greet(name) {
return `こんにちは、${name}さん`;
}
// main.js
import hello from "./greet.js";
console.log(hello("田中"));
デフォルトエクスポートは受け取り側が好きな名前を付けられる。1ファイルに1つだけ、そのファイルの「主役」を公開するときに使う。
import { withTax ____ tax } from "./utils.js";
console.log(tax(1000));
出力 == 1100
「〜として」を意味する2文字の英単語。
as
名前が長いときや、別のファイルの関数と名前が衝突したときに使う。エクスポート側で export { a as b } と書くこともできる。
A. 自動的に公開される B. そのファイルの中だけで使える C. エラーになる D. グローバル変数になる 選択★★☆無料
解答 == B
export の有無が公開・非公開の境界になる。
B
これがモジュールの最大の利点。内部でしか使わない処理を隠せるので、外から誤って壊されることがなくなる。
出力 == ゼロカラ商店(税率10%)
税率のパーセント表示は TAX_RATE * 100 で計算する。
// config.js
export const SHOP_NAME = "ゼロカラ商店";
export const TAX_RATE = 0.1;
// main.js
import { SHOP_NAME, TAX_RATE } from "./config.js";
console.log(`${SHOP_NAME}(税率${TAX_RATE * 100}%)`);
変わりうる値をコードから追い出すのが設定分離の目的。税率が変わっても config.js の1行を直すだけで、全ファイルに反映される。
出力 == 10
import * as utils from "./utils.js"; と書くと名前空間としてまとめて取り込める。
import * as utils from "./utils.js"; console.log(utils.add(3, 7));
どのファイル由来かが呼び出し側で分かるのが利点。ただし使わないものまで読み込むので、通常は名前付きインポートのほうが好まれる。
4実践シナリオ(5問)
出力2行が完全一致
両方とも名前付きエクスポートにする。main.js では for...of で回す。
// utils.js
export const withTax = (price, rate = 0.1) => Math.round(price * (1 + rate));
export const shippingFee = (price) => (price >= 5000 ? 0 : 600);
// main.js
import { withTax, shippingFee } from "./utils.js";
for (const p of [4800, 12000]) {
console.log(`${p.toLocaleString()}円 → 税込${withTax(p).toLocaleString()}円 / 送料${shippingFee(p)}円`);
}
JS08で作った関数を別ファイルに移すだけでモジュールになる。こうしておけば他の画面からも同じ計算を呼べる。
出力 == 1100
price.js の先頭で import { TAX_RATE } from "./config.js"; と書く。モジュールは別のモジュールを取り込める。
// config.js
export const TAX_RATE = 0.1;
// price.js
import { TAX_RATE } from "./config.js";
export const calcTotal = (amount) => Math.round(amount * (1 + TAX_RATE));
// main.js
import { calcTotal } from "./price.js";
console.log(calcTotal(1000));
main.js は config.js の存在を知らなくてよい。税率の管理は price.js に任せきりにできるので、呼び出し側がシンプルになる。これが階層的な設計。
出力 == 現在値:3
count には export を付けない。increment と getCount だけをエクスポートする。
// counter.js
let count = 0;
export const increment = () => {
count++;
return count;
};
export const getCount = () => count;
// main.js
import { increment, getCount } from "./counter.js";
increment();
increment();
increment();
console.log(`現在値:${getCount()}`);
count を直接書き換えられないのが要点。外部から count = 999 とされる心配がなく、増やす手段は increment だけに限定できる。これをカプセル化と呼ぶ。
main.js で ("", "a@b.com") と ("田中", "abc") と ("田中", "a@b.com") の3通りを出力すること。 コーディング★★★無料
出力3行が完全一致
validateAll の中で早期リターンする。エラーメッセージは「名前が未入力です」「メール形式が不正です」。
// validator.js
export const validateName = (name) => name !== "";
export const validateEmail = (email) => email.includes("@");
export default function validateAll(name, email) {
if (!validateName(name)) {
return "名前が未入力です";
}
if (!validateEmail(email)) {
return "メール形式が不正です";
}
return "OK";
}
// main.js
import validateAll from "./validator.js";
console.log(validateAll("", "a@b.com"));
console.log(validateAll("田中", "abc"));
console.log(validateAll("田中", "a@b.com"));
細かい検証は名前付き、まとめ役はデフォルトという使い分け。呼び出し側は validateAll だけ知っていればよく、個別の検証関数はテスト時などに必要なら取り込める。
main.js で [82, 45, 91, 68] を渡し、「合計286 / 平均71.5 / 最高91」と1行で出力すること。 コーディング★★★無料
出力 == 合計286 / 平均71.5 / 最高91
average は sum を内部で使える(同じファイル内なので import 不要)。小数1桁は toFixed(1)。
// stats.js
export const sum = (nums) => nums.reduce((s, n) => s + n, 0);
export const average = (nums) => (sum(nums) / nums.length).toFixed(1);
export const max = (nums) => Math.max(...nums);
// main.js
import { sum, average, max } from "./stats.js";
const scores = [82, 45, 91, 68];
console.log(`合計${sum(scores)} / 平均${average(scores)} / 最高${max(scores)}`);
同じファイル内なら import せずに呼べる。average が sum を使っているが、これは内部の実装詳細なので外からは意識しなくてよい。
5仕上げ課題
① config.js — SHOP_NAME = "ゼロカラ商店" / TAX_RATE = 0.1 / MEMBER_RATE = 0.1 をエクスポート
② billing.js — config を取り込み、3つの関数をエクスポート
・subtotal(item) … 明細1件の小計(price × qty)
・applyDiscount(amount, isMember) … 会員なら MEMBER_RATE ぶん引く(Math.round)
・withTax(amount) … TAX_RATE ぶん加算(Math.round)
③ format.js — yen(n) をエクスポート(数値を「128,000円」の形式の文字列にする)
④ main.js — すべてを取り込んで請求書を出力
const items = [
{ name: "ノートPC", price: 128000, qty: 1 },
{ name: "マウス", price: 3200, qty: 2 },
{ name: "USBメモリ", price: 1500, qty: 3 },
];
const isMember = true;
出力仕様(8行):
=== ゼロカラ商店 請求書 ===
ノートPC:128,000円 × 1 = 128,000円
マウス:3,200円 × 2 = 6,400円
USBメモリ:1,500円 × 3 = 4,500円
---
小計:138,900円
会員割引後:125,010円
消費税込:137,511円
出力8行が完全一致
main.js では3つのファイルすべてから import する。yen() を使うと toLocaleString() を毎回書かずに済む。billing.js は config.js を取り込むが、main.js から見れば billing.js の内部事情なので気にしなくてよい。
// config.js
export const SHOP_NAME = "ゼロカラ商店";
export const TAX_RATE = 0.1;
export const MEMBER_RATE = 0.1;
// billing.js
import { TAX_RATE, MEMBER_RATE } from "./config.js";
export const subtotal = (item) => item.price * item.qty;
export const applyDiscount = (amount, isMember) =>
isMember ? Math.round(amount * (1 - MEMBER_RATE)) : amount;
export const withTax = (amount) => Math.round(amount * (1 + TAX_RATE));
// format.js
export const yen = (n) => `${n.toLocaleString()}円`;
// main.js
import { SHOP_NAME } from "./config.js";
import { subtotal, applyDiscount, withTax } from "./billing.js";
import { yen } from "./format.js";
const items = [
{ name: "ノートPC", price: 128000, qty: 1 },
{ name: "マウス", price: 3200, qty: 2 },
{ name: "USBメモリ", price: 1500, qty: 3 },
];
const isMember = true;
console.log(`=== ${SHOP_NAME} 請求書 ===`);
let total = 0;
for (const item of items) {
const line = subtotal(item);
total += line;
console.log(`${item.name}:${yen(item.price)} × ${item.qty} = ${yen(line)}`);
}
const discounted = applyDiscount(total, isMember);
const taxed = withTax(discounted);
console.log("---");
console.log(`小計:${yen(total)}`);
console.log(`会員割引後:${yen(discounted)}`);
console.log(`消費税込:${yen(taxed)}`);
JS08と出力はまったく同じだが構造が違うのがこの課題の要点。動作は変わらないのに、直す場所が役割ごとに決まっている。
税率を変えるなら config.js の1行。計算ロジックの修正は billing.js だけ。金額の表示形式(円→ドルなど)を変えるなら format.js の1行。表示の並び順を変えるなら main.js だけ——4つのファイルが互いに干渉しない。
さらに main.js は config.js から SHOP_NAME しか取り込んでいない点にも注目してほしい。TAX_RATE は billing.js が内部で使うだけで、呼び出し側は税率の存在すら知らなくてよい。この「知らなくてよいことを隠す」設計が、ファイルが100個に増えても破綻しない土台になる。
次章ではクラスを学ぶ。データと処理をひとつにまとめる、もうひとつの分割の方法を扱う。