JavaScript
目次
- 概要
- 1. JavaScriptとは何か
- 2. 実行環境と動作の仕組み
- 3. 変数・データ型・型変換
- 4. 演算子と式
- 5. 制御フロー
- 6. 関数(宣言・式・アロー)
- 7. スコープ・クロージャ・巻き上げ
- 8. オブジェクトと配列
- 9. プロトタイプとクラス
- 10. 非同期処理:コールバック→Promise→async/await
- 11. イテレータ・ジェネレータ・Symbol
- 12. モジュールシステム(ESM / CJS)
- 13. エラーハンドリング
- 14. WeakMap・WeakSet・WeakRef
- 15. Proxy・Reflect・メタプログラミング
- 16. ブラウザAPIの基礎
- 17. パフォーマンスの基礎
- 18. セキュリティの基礎
- 19. テスト戦略
- 20. ES2020〜2024の新機能
- 21. よくある落とし穴FAQ
- 22. 図解: イベントループとスタック
- 23. 実コード付きミニチュートリアル
- 24. 学習ロードマップ(30日)
- 25. 用語集
- まとめ
- 参考文献
概要
まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。
コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。
JavaScriptは、ブラウザとサーバーの両方で動き、イベントループと動的なオブジェクトモデルを中心に発展してきた言語です。
このページでは、実行環境、型変換、スコープ、クロージャ、プロトタイプ、非同期処理、モジュールを、落とし穴と設計意図の両面から整理します。
1. JavaScriptとは何か
このセクションでは「JavaScriptがなぜ生まれたのか」「なぜ動的型付けなのか」「どんな考え方で動いているのか」を丁寧に説明します。最初にここをしっかり理解しておくと、後のコードがなぜそう書くのか腑に落ちやすくなります。
JavaScript(JS)は、**「動的型付け・プロトタイプベースオブジェクト指向・マルチパラダイム」**のプログラミング言語です。1995年にブラウザのスクリプト言語として誕生し、現在は次の用途で使われています。
- フロントエンド: DOM操作、フォーム検証、アニメーション、SPA
- バックエンド: Node.js(サーバー処理、CLI、ビルドツール)
- フルスタック: Next.js(React+Node一体)
- 組み込み・エッジ: Deno、Cloudflare Workers
1-1. JavaScriptの歴史(誕生からNode.jsまで)
Brendan Eichと10日間の誕生
1995年、Netscape CommunicationsのBrendan Eichは、わずか10日間でJavaScriptの最初のプロトタイプを開発しました。当時のWebはHTMLによる静的なページが中心で、フォームのバリデーション(入力チェック)すらサーバーとの通信が必要でした。
Netscapeの目標は「デザイナーや非プログラマーがページに動きを加えられる軽量スクリプト言語」でした。当初は “Mocha” → “LiveScript” という名前で開発され、最終的にマーケティング上の理由から “JavaScript” と命名されます(当時人気だったJavaとの混同を意図した命名です。両者は設計上ほぼ無関係です)。
ブラウザ戦争(1995〜2001)
Netscape NavigatorがJavaScriptを搭載すると、競合のMicrosoft Internet Explorerも独自実装の JScript を搭載しました。両者の実装には微妙な差異があり、「IEでは動くがNetscapeでは動かない」「Netscapeでは動くがIEでは動かない」という問題が頻発しました。
これが 「クロスブラウザ問題」 の始まりであり、開発者を長年苦しめた歴史的課題です。
1995 Netscape NavigatorでJavaScript 1.0が動く
1996 MicrosoftがJScriptをIE 3.0に搭載
1997 ECMAがECMAScript 1として標準化(ブラウザ間の差異解消を目的)
2001 IE 6が圧倒的シェアを獲得。長期間更新されず「負債」に
2006 jQuery登場 → クロスブラウザ差異を吸収するライブラリが広まる
AjaxとWeb2.0(2005〜)
2005年、Jesse James Garrettが Ajax(Asynchronous JavaScript and XML) という概念を提唱します。ページ全体を再読み込みせずにデータを取得・更新する技術で、Gmail(2004)やGoogle Maps(2005)がその先行例でした。
これにより「JavaScriptは補助的なもの」という認識が一変し、JavaScriptでアプリケーションを作る時代が幕を開けます。
Node.jsの登場(2009)
2009年、Ryan Dahlが Node.js を発表します。V8エンジン(Google Chromeが使う高速JavaScriptエンジン)を使い、JavaScriptをサーバーサイドで動かすことを実現しました。
Node.js以前: JavaScript = ブラウザだけ
Node.js以後: JavaScript = フロントエンド + バックエンド + CLI + ビルドツール
Node.jsの登場により、1つの言語でフルスタック開発できる「JavaScriptエコシステム」が急速に拡大します。npm(Node Package Manager)が整備され、現在では200万以上のパッケージが公開されています。
モダンJavaScriptの確立(2015〜現在)
ES2015 (ES6) → let/const、アロー関数、クラス、Promise、モジュール
(JavaScriptの設計が大きく変わった歴史的バージョン)
ES2017 → async/await(非同期処理が劇的に書きやすくなった)
ES2020 → Optional chaining、Nullish coalescing
ES2022 → クラスフィールド・プライベートが正式仕様に
ES2024 → Promise.withResolvers、Object.groupBy
TypeScript → Microsoftが開発した静的型付けのJS上位互換言語(2012〜)
Deno → Ryan DahlがNode.jsの反省を活かして作った新しいランタイム(2020)
Bun → 高速を目指した新世代ランタイム(2023)
1-2. なぜ動的型付けか
動的型付けとは
動的型付け(Dynamically Typed)とは、変数の型がコンパイル時ではなく実行時(ランタイム)に決まる仕組みです。
// 動的型付け: 型宣言が不要、値によって型が決まる
let x = 42 // 実行時にnumberと判断される
x = 'hello' // 文字列に再代入できる(型が変わる)
x = { name: 'a' } // オブジェクトにも変えられる
x = [1, 2, 3] // 配列にも変えられる
対照的に、JavaやC++ は**静的型付け(Statically Typed)**で、コンパイル時に型が固定されます:
// 静的型付け(Java): 型を明示しなければコンパイルエラー
int x = 42;
x = "hello"; // コンパイルエラー!intに文字列は代入できない
なぜJavaScriptは動的型付けを選んだのか
Brendan EichがJavaScriptを設計した際の背景を理解することが重要です:
- 対象ユーザーはプログラマーではなかった: デザイナーやHTMLライターが使うことを想定。型宣言は学習コストが高い
- 素早さが最優先だった: 10日間という制約の中で、型システムを設計する余裕がなかった
- スクリプト言語の文化: Perl、Python、Rubyなど、当時人気のスクリプト言語も動的型付けが主流だった
動的型付けのメリットとデメリット
| 観点 | 動的型付け(JavaScript) | 静的型付け(TypeScript/Java) |
|---|---|---|
| 学習コスト | 低い(型宣言不要) | 高い(型システムの理解が必要) |
| 開発速度(プロトタイプ) | 速い(制約が少ない) | 遅い(型を書く手間) |
| バグの発見タイミング | 実行時(遅い) | コンパイル時(早い) |
| IDE支援(補完・検査) | 弱い | 強い |
| 大規模開発 | 困難(型情報がない) | 向いている |
| 柔軟性 | 非常に高い | 制約がある |
TypeScriptの登場
動的型付けの問題点(大規模コードベースでの型エラー、IDE補完の弱さ)を解決するために、Microsoftは2012年に TypeScript を開発しました。TypeScriptはJavaScriptに静的型注釈を加えたスーパーセットです。
// TypeScript: 型を明示できる
let x: number = 42
x = 'hello' // コンパイルエラー!numberにstringは代入できない
function add(a: number, b: number): number {
return a + b
}
現在、大規模なプロジェクトではTypeScriptが事実上の標準になっています。ただし、TypeScriptを理解するにはJavaScriptの基礎が不可欠です。本ガイドではJavaScriptコアに集中します。
1-3. JavaScriptの3つの特性
1. 動的型付け(Dynamically Typed)
let x = 42 // number
x = 'hello' // stringに変えられる
x = { name: 'a' } // objectにも変えられる
型を宣言しない。型はランタイムに決まる。これが柔軟さとバグの両方の源になります。TypeScriptはここに静的型注釈を加えます。
2. イベント駆動・非ブロッキングI/O
// Node.jsの例: ファイルを読んでいる間も他の処理が動く
import { readFile } from 'fs/promises'
async function main() {
const data = await readFile('./config.json', 'utf8')
console.log(data)
}
main()
console.log('ここはreadFileを待たずに実行される')
// 実行順: 「ここはreadFileを待たずに実行される」→ ファイルの内容
JavaScriptはシングルスレッドだが、I/Oは非同期で行います。これがイベントループの核心です(第2章・第22章で詳解)。
なぜシングルスレッドなのか? Brendan Eichの設計選択です。マルチスレッドにすると競合状態(race condition)やデッドロックが発生しやすく、非プログラマーには扱いにくい。代わりに「非同期コールバック」モデルを採用し、I/Oの待ち時間を有効活用するアーキテクチャにしました。
3. プロトタイプチェーン
const arr = [1, 2, 3]
arr.map(x => x * 2) // [2, 4, 6]
// arr自身にmapは定義されていない
// Array.prototype.mapを「プロトタイプチェーン」で探す
すべてのオブジェクトは「親」のプロトタイプを持ち、メソッドはそこから継承されます(第9章)。
1-4. ECMAScriptとの関係
JavaScriptの仕様は ECMA-262(ECMAScript) として標準化されています。TC39委員会が年1回のリリースサイクルで新機能を追加します。
TC39は各ブラウザベンダー(Google、Mozilla、Microsoft、Apple)やJavaScriptエンジン開発者、有名ライブラリの著者などで構成されます。新機能は「プロポーザル(提案)」として段階的に承認されます:
Stage 0: Strawperson → アイデアの段階
Stage 1: Proposal → 正式な提案、チャンピオンが担当
Stage 2: Draft → 仕様の草案、実装が始まる
Stage 3: Candidate → 実装が完成、フィードバック収集
Stage 4: Finished → 次のECMAScriptに追加確定
| 年 | バージョン名 | 代表的追加機能 |
|---|---|---|
| 1997 | ES1 | 初の標準化 |
| 1999 | ES3 | try/catch、正規表現 |
| 2009 | ES5 | strict mode、Array高階関数、JSON |
| 2015 | ES6/ES2015 | let/const、アロー関数、クラス、Promise、モジュール |
| 2016 | ES2016 | **(べき乗)、Array.includes |
| 2017 | ES2017 | async/await、Object.entries/values |
| 2018 | ES2018 | スプレッド/レストの拡張、Promise.finally |
| 2019 | ES2019 | flatMap、Object.fromEntries、optional catch |
| 2020 | ES2020 | Optional chaining、Nullish coalescing、BigInt、Promise.allSettled |
| 2021 | ES2021 | replaceAll、Promise.any、論理代入演算子 |
| 2022 | ES2022 | at()、Object.hasOwn、Top-level await、クラスフィールド正式化 |
| 2023 | ES2023 | toSorted、toReversed、findLast |
| 2024 | ES2024 | Promise.withResolvers、Object.groupBy、ArrayBuffer resize |
注意: 「ES6」と「ES2015」は同じものです。ES6までは番号で呼ばれていましたが、ES2015以降は年号で呼ぶのが正式になりました。
1-5. このセクションのまとめ
JavaScriptの歴史と特性:
誕生の背景:
1995年: Brendan Eichが10日間でプロトタイプを開発
目的: デザイナーでも使える軽量スクリプト言語
→ 動的型付け、ゆるい設計思想はここから来ている
歴史的転換点:
2005: Ajax → ブラウザでアプリを作る時代の幕開け
2009: Node.js → サーバーサイドJSの誕生
2015: ES2015 → モダンJSの礎(let/const/アロー/Promise/モジュール)
2012: TypeScript → 動的型付けの弱点を補う静的型注釈
3つの核心的特性:
1. 動的型付け → 柔軟だが実行時エラーに注意。大規模ではTypeScriptを使う
2. 非ブロッキングI/O → シングルスレッドでも高速なI/O処理
3. プロトタイプチェーン → オブジェクト継承のベース
ECMAScriptとの関係:
TC39が毎年新機能を追加
Stage 0〜4のプロセスで承認される
次のセクションでは、JavaScriptがブラウザやNode.jsでどのように実行されるかを、エンジンの内部構造から詳しく説明します。
2. 実行環境と動作の仕組み
このセクションでは「JavaScriptがどのように実行されるか」を、エンジン内部のパイプラインからイベントループの詳細まで丁寧に解説します。ここを理解することで、「なぜ非同期処理はこの順番で動くのか」「なぜJavaScriptは速いのか」が腑に落ちます。
2-1. V8エンジンとJITコンパイル
なぜJavaScriptエンジンが必要か
ブラウザやNode.jsは、JavaScriptのソースコードをそのまま実行することはできません。CPUが理解できるのはマシンコード(バイナリ命令)だけです。JavaScriptエンジンは「人間が書いたJSコード → CPUが実行できるマシンコード」への変換を担います。
主要なJavaScriptエンジン:
| エンジン | 使用場所 |
|---|---|
| V8 | Google Chrome、Node.js、Deno |
| SpiderMonkey | Mozilla Firefox |
| JavaScriptCore(Nitro) | Safari、Bun |
| Chakra | 旧Microsoft Edge(現EdgeはV8使用) |
V8のJITパイプライン(詳細)
JavaScriptはインタープリター言語と言われますが、現代のエンジン(V8)は**JITコンパイル(Just-In-Time)**を行います。
┌─────────────────────────────────────────────────────────────┐
│ V8 JITコンパイルパイプライン │
├─────────────────────────────────────────────────────────────┤
│ │
│ JSソースコード(テキスト) │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Parser │ 字句解析(トークン化)→ 構文解析 → AST生成 │
│ └──────┬──────┘ │
│ │ AST(抽象構文木) │
│ ▼ │
│ ┌─────────────┐ │
│ │ Ignition │ AST → バイトコード(インタープリタ実行) │
│ │(インタプリタ)│ 最初は遅いがすぐ動く │
│ └──────┬──────┘ │
│ │ プロファイリング情報(頻繁に実行される関数を検出) │
│ ▼ │
│ ┌─────────────┐ │
│ │ TurboFan │ ホットな関数をネイティブコードへ最適化 │
│ │(最適化JIT) │ 型情報をもとにインライン展開・投機的最適化 │
│ └──────┬──────┘ │
│ │ 型が変わった場合: 脱最適化(Deoptimization) │
│ ▼ │
│ マシンコード(CPUが直接実行) │
│ │
└─────────────────────────────────────────────────────────────┘
▲ V8 JITパイプライン:ソースコードがParser → Ignition → TurboFanを経てマシンコードに変換される。型の仮定が崩れると逆最適化(Deoptimize)が起きる。
**投機的最適化(Speculative Optimization)**とは何か:
function add(a, b) {
return a + b
}
// 100回numberを渡すと...
add(1, 2) // number + number = TurboFanが「aとbはnumber」と決め打ちで最適化
add(3, 4)
// ... 98回 ...
// 突然stringを渡すと...
add('x', 'y') // 型が違う!脱最適化(Deoptimize)してIgnitionに戻る
この仕組みから分かる実践的なヒント:同じ関数に対して常に同じ型の引数を渡すとV8が最適化しやすくなります。
なぜ「インタープリター言語」と呼ばれるのに速いのか
歴史的にJavaScriptは「遅い」と言われていました。初期の実装はバイトコードを逐次解釈するだけでした。2008年、GoogleがV8エンジンをリリースし、JITコンパイルを本格的に導入したことで、JavaScriptの実行速度は劇的に向上しました(10〜100倍)。
- 2008年前: JSは「遅いスクリプト言語」の代名詞
- 2008年後: V8登場 → Chromeの高速なJS実行が話題に
- 2009年: Node.jsがV8を採用 → サーバーサイドJSが実用的に
- 現在: 数値計算ではC++の5〜10倍程度まで近づく場合もある
2-2. イベントループの詳細
なぜイベントループが必要か
JavaScriptはシングルスレッドです。一度に1つのタスクしか実行できません。しかし、ブラウザはユーザーの操作・ネットワーク通信・タイマー・描画など、多くの並行タスクをこなす必要があります。
これを解決するのが**イベントループ(Event Loop)**です。「コールスタックが空になったらキューからタスクを取り出す」という単純なルールで、非同期処理を実現します。
イベントループの完全な動作フロー
┌─────────────────────────────────────────────────────────────┐
│ イベントループの動作 │
└─────────────────────────────────────────────────────────────┘
開始
│
▼
┌──────────────────┐
│ 同期コードを実行 │ ← スクリプト全体、または現在のタスク
│ (Call Stack) │
└────────┬─────────┘
│ Call Stackが空になった
▼
┌──────────────────┐
│ Microtask Queue │ ← Promise.then、queueMicrotask、MutationObserver
│ を全て処理 │ ※ 空になるまで繰り返す(再帰的マイクロタスクも処理)
└────────┬─────────┘
│ Microtask Queueが空になった
▼
┌──────────────────┐
│ レンダリング更新 │ ← ブラウザのみ: requestAnimationFrame → style/layout/paint
│ (必要な場合) │ (16.7ms ≈ 60fpsのタイミングで行われる)
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Macrotask Queue │ ← setTimeout、setInterval、I/O、UIイベント
│ から1つ取り出す │ ※ 1つだけ取り出して実行(複数あっても1つずつ)
└────────┬─────────┘
│
└──────────────────────────→ 最初に戻る(ループ)
重要なポイント:
- Microtaskは「今のタスクが終わったら即座に」実行される
- Macrotaskは「次のループで」実行される
- Microtaskが無限ループするとブラウザはフリーズする
2-3. Call Stack・Heap・Task Queue
Call Stack(コールスタック)
Call Stackは「現在どの関数を実行しているか」を追跡するデータ構造(LIFO: 後入れ先出し)です。
function c() {
console.log('c') // ③ cが実行される
}
function b() {
c() // ② bからcを呼ぶ
}
function a() {
b() // ① aからbを呼ぶ
}
a()
Call Stackの変化:
a() を呼ぶb() を呼ぶc() を呼ぶc() 終了b() 終了a() 終了
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ │ │ │ │ c() │ │ │ │ │ │ │
│ │ │ b() │ │ b() │ │ b() │ │ │ │ │
│ a() │ │ a() │ │ a() │ │ a() │ │ a() │ │ │
└───────┘ └───────┘ └───────┘ └───────┘ └───────┘ └───────┘
スタックオーバーフローはCall Stackが溢れたとき発生します(再帰が無限に続く場合など)。
// ❌ スタックオーバーフロー
function infinite() {
return infinite() // 自分自身を永遠に呼ぶ
}
infinite() // RangeError: Maximum call stack size exceeded
Heap(ヒープ)
Heapはオブジェクトが格納されるメモリ領域です。Call Stackに対してより広く、構造化されていないメモリ領域です。
// プリミティブ値はCall Stackに直接格納
const x = 42 // Stack: x → 42
const y = 'hello' // Stack: y → 'hello'
// オブジェクトはHeapに格納、Stackには参照(アドレス)が入る
const obj = { name: 'Alice' } // Heap: { name: 'Alice' }
// Stack: obj → [0x1234] (Heapのアドレス)
ガベージコレクション(GC): Heapに格納されたオブジェクトは、参照がなくなるとGCによって自動解放されます。V8はMark-and-Sweepアルゴリズムを使い、到達不能なオブジェクトを検出してメモリを解放します。
▲ メモリモデル:プリミティブ値(数値・文字列・boolean)はコールスタックに直接格納、オブジェクト・配列・クロージャはヒープに格納されスタックから参照される。
Task Queue(タスクキュー)
Task Queueは2種類あります:
┌──────────────────────────────────────────────────────────────┐
│ Task Queueの種類 │
├──────────────────────────┬───────────────────────────────────┤
│ Microtask Queue(優先) │ Macrotask Queue(通常) │
├──────────────────────────┼───────────────────────────────────┤
│ Promise.then/catch │ setTimeout │
│ Promise.finally │ setInterval │
│ queueMicrotask() │ setImmediate(Node.js) │
│ MutationObserver │ I/Oコールバック │
│ async/awaitの再開 │ UIイベント(click等) │
│ │ requestAnimationFrame(ブラウザ) │
└──────────────────────────┴───────────────────────────────────┘
優先順位: 同期コード > Microtask > Macrotask
// 実際の実行順序を確認する例
console.log('1: 同期開始')
setTimeout(() => console.log('6: setTimeout(Macrotask)'), 0)
Promise.resolve()
.then(() => console.log('3: Promise.then(Microtask)'))
.then(() => console.log('4: Promise.then2(Microtask)'))
queueMicrotask(() => console.log('5: queueMicrotask(Microtask)'))
console.log('2: 同期終了')
// 出力順:
// 1: 同期開始
// 2: 同期終了
// 3: Promise.then(Microtask)
// 4: Promise.then2(Microtask)
// 5: queueMicrotask(Microtask)
// 6: setTimeout(Macrotask)
2-4. ブラウザとNode.jsの違い
| 項目 | ブラウザ | Node.js |
|---|---|---|
| グローバルオブジェクト | window |
global / globalThis |
| DOM | あり | なし |
| ファイルシステム | なし | fs モジュール |
| モジュール | ESM(import/export) |
ESM + CJS(require)両対応 |
| ネットワーク | fetch(標準) |
fetch(Node 18+で標準化) |
| タイマー | setTimeout, setInterval |
同左 + setImmediate |
| イベントループ実装 | ブラウザ内蔵(各エンジン固有) | libuv(C++ライブラリ) |
| スレッド | Web Workers | Worker Threads |
| マイクロタスク追加 | queueMicrotask |
同左 + process.nextTick(最優先) |
Node.js特有の process.nextTick:
// Node.jsのみ: process.nextTickはPromise.thenよりも先に実行される
Promise.resolve().then(() => console.log('B: Promise'))
process.nextTick(() => console.log('A: nextTick'))
// 出力:
// A: nextTick ← Promiseより先!
// B: Promise
2-5. このセクションのまとめ
JavaScriptの実行モデル:
V8 JITパイプライン:
JSソース → Parser(AST生成)→ Ignition(バイトコード)
→ TurboFan(ホットパスをネイティブコードに最適化)
→ 型が変わると脱最適化(Deoptimize)してIgnitionに戻る
メモリ構造:
Call Stack → 現在実行中の関数の追跡(LIFO)
Heap → オブジェクトの格納(GCが管理)
Task Queue → 非同期タスクの待ち行列
イベントループの優先順位:
① 同期コード(Call Stackが空になるまで)
② Microtask Queue(全て処理:Promise.then、queueMicrotask)
③ レンダリング更新(ブラウザのみ・60fps)
④ Macrotaskから1つ実行(setTimeout、I/O等)
→ ① に戻る
実践的ヒント:
- 同じ関数に常に同じ型を渡す → V8の最適化が効く
- Microtaskの無限ループ → UIフリーズの原因
- 重い同期処理 → Web Workerに移す(次章で詳解)
次のセクションでは、変数宣言とデータ型を歴史的背景も含めて深掘りします。
3. 変数・データ型・型変換
このセクションでは変数宣言(var/let/const)の歴史的経緯、8種類のデータ型の深い理解、そしてJavaScript最大の落とし穴である型強制(Type Coercion)を体系的に説明します。ここを理解することで、「なぜ === を使うのか」「なぜ const が推奨されるのか」が明確になります。
3-1. var/let/constの歴史と違い
var の問題(なぜ生まれ、なぜ使われなくなったか)
var はJavaScript誕生(1995年)からES5(2009年)まで、唯一の変数宣言方法でした。当時は「変数スコープ」の設計が甘く、後に多くの問題を引き起こします。
// varの特徴
var x = 1 // 関数スコープ(ブロックスコープではない!)
var x = 2 // 再宣言可能(エラーにならない!)
// ブロックスコープを持たない問題
if (true) {
var insideIf = 'ここで宣言'
}
console.log(insideIf) // 'ここで宣言'(ブロック外からアクセスできる!)
// 有名なvarのバグ
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}
// 期待: 0 1 2
// 実際: 3 3 3(setTimeoutが実行されるときにはforが終わっている)
let の登場(ES2015)
let はES2015で追加された、ブロックスコープを持つ変数宣言です。
// letの特徴
let y = 2 // ブロックスコープ
// let y = 3 // ❌ SyntaxError: 再宣言不可
if (true) {
let insideIf = 'ここで宣言'
}
// console.log(insideIf) // ❌ ReferenceError(ブロック外からアクセス不可)
// varの問題を解決
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}
// 0 1 2(各イテレーションで独立したiを持つ)
const の登場(ES2015)
const は再代入不可の変数宣言です。現代のJavaScriptでは const をデフォルトにするのがベストプラクティスです。
const z = 3 // ブロックスコープ、再代入不可
// ❌ TypeError: 再代入不可
// z = 4
// ✅ オブジェクトの中身は変更できる(参照が変わらなければOK)
const obj = { name: 'Alice' }
obj.name = 'Bob' // ✅ これはOK(参照先の変更)
// obj = { name: 'Charlie' } // ❌ TypeError(参照の再代入は不可)
// オブジェクトの中身まで固定したい場合
const frozen = Object.freeze({ name: 'Alice', age: 30 })
frozen.name = 'Bob' // 無視される(strictモードではTypeError)
// ⚠ ネストしたオブジェクトは浅いコピーなので注意
const obj2 = Object.freeze({ user: { name: 'Alice' } })
obj2.user.name = 'Bob' // これは変更できてしまう!(浅いfreezeなので)
3種類の変数宣言の比較
| 特性 | var |
let |
const |
|---|---|---|---|
| スコープ | 関数スコープ | ブロックスコープ | ブロックスコープ |
| 再宣言 | 可能 | 不可 | 不可 |
| 再代入 | 可能 | 可能 | 不可(参照) |
| 巻き上げ(Hoisting) | される(undefined) |
される(TDZ) | される(TDZ) |
| グローバルプロパティ化 | される(window.x) |
されない | されない |
| 推奨 | 使わない | 再代入が必要な場合 | デフォルト |
なぜ const をデフォルトにするのか?
- 意図の明示: 「この変数は変わらない」ことをコードで伝える
- バグ防止: 意図しない再代入をコンパイル時にエラーとして検出
- 最適化ヒント: エンジンが「この値は変わらない」と分かれば最適化しやすい
3-2. TDZ(Temporal Dead Zone)
TDZとは何か
let と const は巻き上げ(Hoisting)されますが、宣言行に到達するまでアクセスするとエラーになります。この「宣言前のアクセス禁止ゾーン」を TDZ(Temporal Dead Zone:一時的デッドゾーン) と呼びます。
// varの巻き上げ(値はundefined)
console.log(varX) // undefined(エラーにならない!)
var varX = 5
console.log(varX) // 5
// letのTDZ(宣言前はエラー)
console.log(letX) // ❌ ReferenceError: Cannot access 'letX' before initialization
let letX = 5
console.log(letX) // ✅ 5
// constのTDZ(同様)
console.log(constX) // ❌ ReferenceError
const constX = 5
TDZが存在する理由
var は「宣言前にアクセスできるが undefined」という挙動がバグの温床でした。let/const は「宣言前にアクセスしたらエラー」という設計にすることで、開発者が「変数を使う前に必ず宣言する」という良い習慣を強制します。
// ❌ varの問題: 宣言前アクセスがサイレントに失敗する
function processUser(user) {
if (user.isAdmin) {
var role = 'admin'
}
console.log(role) // user.isAdminがfalseの場合undefined(エラーにならない!)
}
// ✅ letの場合: 宣言前アクセスは即座にエラー
function processUser(user) {
if (user.isAdmin) {
let role = 'admin'
console.log(role) // ✅ ブロック内でのみ使える
}
// console.log(role) // ❌ ReferenceError(意図しないアクセスを防ぐ)
}
▲ HoistingとTDZ:varは宣言が巻き上げられundefinedとして初期化される。let/constは宣言行までTDZ(Temporal Dead Zone)となり、アクセスするとReferenceError。function宣言は宣言+本体が完全に巻き上げられる。
クラスのTDZ
クラス宣言もTDZの影響を受けます:
// ❌ クラスは巻き上げされない(TDZ)
const obj = new MyClass() // ❌ ReferenceError
class MyClass {
constructor() { this.value = 42 }
}
// ✅ 宣言後に使う
class MyClass {
constructor() { this.value = 42 }
}
const obj = new MyClass() // ✅
3-3. 8種類のデータ型(詳細)
JavaScriptには7つのプリミティブ型と**1つの参照型(object)**があります。
| 型 | 例 | 特徴 | 追加メモ |
|---|---|---|---|
number |
42, 3.14, NaN, Infinity |
IEEE 754倍精度浮動小数点 | 整数も浮動小数点で表現 |
string |
'hello', "world", `template` |
イミュータブル | UTF-16エンコード |
boolean |
true, false |
||
null |
null |
「意図的な空」を表す | typeof null === 'object' は歴史的バグ |
undefined |
undefined |
未定義・未初期化 | 宣言のみで初期化されていない変数 |
bigint |
9007199254740991n |
任意精度整数 | ES2020+、n サフィックス |
symbol |
Symbol('id') |
一意の値 | ES2015+、プロパティキーに使用 |
object |
{}, [], function |
参照型。すべての「もの」はobject | 関数もobjectのサブタイプ |
プリミティブ型vs参照型の重要な違い
// プリミティブ型: 値で比較・コピー
let a = 42
let b = a // 値のコピー
b = 100
console.log(a) // 42(aは変わらない)
// 参照型: 参照で比較・コピー
let obj1 = { name: 'Alice' }
let obj2 = obj1 // 参照のコピー(同じオブジェクトを指す)
obj2.name = 'Bob'
console.log(obj1.name) // 'Bob'(obj1も変わってしまう!)
// 参照型の比較
{ name: 'Alice' } === { name: 'Alice' } // false(別の参照)
const ref = { name: 'Alice' }
ref === ref // true(同じ参照)
string 型の深い理解
// 文字列はイミュータブル
let str = 'hello'
str[0] = 'H' // 無視される(エラーにならないが変わらない)
console.log(str) // 'hello'
// 文字列のメソッドは常に新しい文字列を返す
const upper = str.toUpperCase() // 'HELLO'(strは変わらない)
// テンプレートリテラル(ES2015+)
const name = 'Alice'
const greeting = `Hello, ${name}!` // 'Hello, Alice!'
const multiline = `
複数行の
文字列
`.trim()
// タグ付きテンプレートリテラル(高度な使い方)
function highlight(strings, ...values) {
return strings.map((str, i) => `${str}${values[i] ? `<b>${values[i]}</b>` : ''}`).join('')
}
const html = highlight`Hello, ${name}!` // 'Hello, <b>Alice</b>!'
null と undefined の使い分け
// undefined: 意図せず「なし」(変数未定義、引数省略、プロパティなし)
let x // x === undefined
function fn(a) { return a } // fn() → undefined(引数省略)
const obj = {}
obj.missing // undefined(プロパティなし)
// null: 意図的に「なし」(「データがない」を明示的に表現)
let user = null // 「ユーザーはまだ存在しない」という意図を明示
// APIレスポンスで「値がない」ことを表すときもnullを使う
// チェックのパターン
value == null // nullかundefinedのどちらかをまとめてチェック(唯一 == を使う場面)
value === null // nullのみ
value === undefined // undefinedのみ
value != null // nullでもundefinedでもない(nullishでない)
symbol 型の用途
// Symbol: 一意の値。同じ説明でも別の値になる
const sym1 = Symbol('id')
const sym2 = Symbol('id')
sym1 === sym2 // false
// プロパティキーとして使うと「隠れたプロパティ」になる
const ID = Symbol('id')
const user = {
[ID]: 123, // Symbolキーは通常の列挙に現れない
name: 'Alice'
}
Object.keys(user) // ['name'](IDが出ない)
user[ID] // 123
// 実用例: サードパーティオブジェクトに安全にメタデータを付与
const METADATA = Symbol('metadata')
externalObject[METADATA] = { addedAt: Date.now() }
// 既存のプロパティと衝突しない
typeof で型を確認
typeof 42 // 'number'
typeof 'hello' // 'string'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof null // 'object' ← 歴史的バグ!(nullなのにobject)
typeof {} // 'object'
typeof [] // 'object' ← 配列もobject(Array.isArrayで確認)
typeof function(){} // 'function'(objectのサブタイプだが特別扱い)
typeof Symbol() // 'symbol'
typeof 42n // 'bigint'
typeof null === 'object' は仕様上のバグです。初期のJavaScriptでは型情報をポインタの下位ビットに格納しており、null(ヌルポインタ)が object と同じビットパターンになってしまいました。後方互換のため修正できません。
// 正しいnullチェック
value === null // ✅ nullのみ
typeof value === 'object' && value !== null // ✅ nullでないオブジェクト
// 配列の確認
Array.isArray([]) // ✅ true(typeofでは 'object' になってしまうので)
3-4. number型の落とし穴
IEEE 754倍精度浮動小数点の問題
// 浮動小数点誤差(JavaScriptだけでなく多くの言語に共通)
0.1 + 0.2 // 0.30000000000000004(!)
0.1 + 0.2 === 0.3 // false
// なぜか: 0.1や0.2は2進数で正確に表現できない
// 10進数の0.1 → 2進数では0.000110011001100... (無限循環)
// 対策1: 許容誤差内での比較
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true
// Number.EPSILON = 2.220446049250313e-16
// 対策2: 金額計算は整数(セント単位)で
const price = 300 // 3.00ドル = 300セント
const tax = Math.round(price * 0.1) // 30セント(四捨五入で整数に)
const total = price + tax // 330セント(整数演算は正確)
// 対策3: 表示時に丸める
(0.1 + 0.2).toFixed(2) // '0.30'(文字列)
Number((0.1 + 0.2).toFixed(2)) // 0.3(数値)
安全な整数の上限
// Number.MAX_SAFE_INTEGER = 2^53 - 1
Number.MAX_SAFE_INTEGER // 9007199254740991
// この上限を超えると区別できなくなる
9007199254740992 === 9007199254740993 // true(!区別できない)
9007199254740992 + 1 // 9007199254740992(変わらない)
// →大きな整数はBigIntを使う(ES2020+)
const big = 9007199254740993n
const big2 = BigInt('9007199254740993')
// BigIntの演算
9007199254740993n + 1n // 9007199254740994n(正確)
// 注意: numberとBigIntを混在させるとエラー
9007199254740993n + 1 // ❌ TypeError
NaN(Not a Number)
const x = parseInt('abc') // NaN(数値に変換できない)
const y = 0 / 0 // NaN
const z = Math.sqrt(-1) // NaN
// NaNのチェック
isNaN('hello') // true(文字列を強制変換してからチェック → 危険)
Number.isNaN('hello') // false ← これを使う(型変換なし)
Number.isNaN(NaN) // true ← NaNのチェックはこれだけ
// NaNは自分自身と等しくない(JavaScriptで唯一の例外)
NaN === NaN // false(!)
NaN !== NaN // true
// NaNの伝播
NaN + 1 // NaN
NaN * 10 // NaN
Math.max(NaN, 5) // NaN(NaNが含まれると結果もNaN)
3-5. 型強制(Type Coercion)落とし穴一覧
JavaScriptは演算子の文脈に応じて暗黙的に型を変換します。これが最大の混乱源の一つです。
文字列への強制変換(+ 演算子)
// + は左右どちらかがstringなら文字列結合になる
'5' + 3 // '53'(numberが文字列に変換される)
3 + '5' // '35'
'5' + true // '5true'
'5' + null // '5null'
'5' + undefined // '5undefined'
'5' + {} // '5[object Object]'
'5' + [] // '5'(空配列は空文字列に変換される)
数値への強制変換(-, *, / 演算子)
// - * / は数値変換を強制
'5' - 3 // 2(文字列が数値に変換)
'5' * '3' // 15
'5' / '2' // 2.5
true - false // 1 - 0 = 1
null * 5 // 0 * 5 = 0(nullは0に変換)
undefined * 5 // NaN(undefinedはNaNに変換)
[] - 1 // 0 - 1 = -1(空配列は0に変換)
型強制の落とし穴まとめ表
| 式 | 結果 | 理由 |
|---|---|---|
'5' + 3 |
'53' |
+ はstring優先で文字列結合 |
'5' - 3 |
2 |
- は数値変換 |
true + true |
2 |
booleanは1/0に変換 |
null + 1 |
1 |
nullは0に変換 |
undefined + 1 |
NaN |
undefinedはNaNに変換 |
[] + [] |
'' |
空配列は空文字列 |
[] + {} |
'[object Object]' |
[] → ''、{} → '[object Object]' |
{} + [] |
0 |
{} がブロックとして解釈され +[] = 0 |
0 == false |
true |
緩い比較でfalse → 0 |
'' == false |
true |
緩い比較でfalse → 0、‘’ → 0 |
null == undefined |
true |
特別ルール |
null == false |
false |
nullはnull/undefinedのみと等しい |
[] == false |
true |
[] → '' → 0、false → 0 |
[] == ![] |
true |
最も有名な混乱例 |
// [] == ![] がtrueになる理由
// ![] → false(配列はtruthyなので!でfalse)
// [] == false → [] を数値変換 → '' → 0
// falseを数値変換 → 0
// 0 == 0 → true
===(厳密等価)vs ==(緩い等価)
// === は型変換なし
0 === false // false(型が違う)
'' === false // false
null === undefined // false
// == は型変換あり(上記の落とし穴が発生)
0 == false // true
'' == false // true
null == undefined // true(唯一推奨される == の使い方)
ルール: ===(厳密等価)を常に使う。唯一の例外は value == null(nullかundefinedかを一緒にチェックしたい場合)。
falsyとtruthy
// falsy(if文でfalseとして扱われる値)
// false, 0, -0, 0n, '', "", ``, null, undefined, NaN
Boolean(false) // false
Boolean(0) // false
Boolean(-0) // false
Boolean(0n) // false(BigIntの0)
Boolean('') // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
// 残りはすべてtruthy(注意が必要なもの)
Boolean([]) // true(空配列もtruthy!)
Boolean({}) // true(空オブジェクトもtruthy!)
Boolean('false') // true(文字列 "false" はtruthy!)
Boolean('0') // true(文字列 "0" もtruthy!)
Boolean(new Boolean(false)) // true(Booleanオブジェクトはtruthy)
3-6. 明示的な型変換
暗黙の型変換に頼らず、明示的に変換することでコードを安全にします。
// 数値へ
Number('42') // 42
Number('') // 0(注意: 空文字は0!)
Number(' ') // 0(注意: スペースも0!)
Number('abc') // NaN
Number(true) // 1
Number(false) // 0
Number(null) // 0
Number(undefined) // NaN
parseInt('42px') // 42(先頭の数値だけ取得)
parseInt('0xff', 16) // 255(16進数としてパース)
parseFloat('3.14abc') // 3.14
+'42' // 42(単項+演算子、簡潔だが読みにくい)
// 文字列へ
String(42) // '42'
String(null) // 'null'(nullの文字列変換はエラーにならない)
String(undefined) // 'undefined'
(42).toString() // '42'
(255).toString(16) // 'ff'(16進数文字列に変換)
`${42}` // '42'(テンプレートリテラル)
JSON.stringify({ x: 1 }) // '{"x":1}'
// ブールへ
Boolean(value) // 明示的変換
!!value // 二重否定(簡潔な書き方)
3-7. このセクションのまとめ
変数宣言の選択:
const → デフォルト(再代入しない変数)
let → 再代入が必要な場合のみ
var → 新規コードでは使わない
TDZ(Temporal Dead Zone):
let/constは宣言前にアクセスするとReferenceError
varはundefinedになる(サイレントなバグの温床)
8種類のデータ型:
プリミティブ (7): number, string, boolean, null, undefined, bigint, symbol
参照型 (1): object(配列・関数・正規表現を含む)
型強制の落とし穴:
+ はstringがあると文字列結合になる
== は型変換するので意図しない比較になる
→ === を常に使う(例外: value == null)
→ falsyの落とし穴: [] と {} はtruthy
number型の注意点:
0.1 + 0.2 !== 0.3(浮動小数点誤差)
MAX_SAFE_INTEGER = 2^53-1(超えると区別できなくなる)
NaN === NaNはfalse → Number.isNaN() を使う
次のセクションでは、演算子と式を体系的に説明します。
4. 演算子と式
このセクションではJavaScriptの演算子を体系的に整理します。特に「短絡評価(Short-circuit Evaluation)」「Optional Chaining」「スプレッド/レスト」は現代のコードで頻繁に登場するので、動作原理まで理解します。
4-1. 算術演算子
5 + 2 // 7
5 - 2 // 3
5 * 2 // 10
5 / 2 // 2.5(整数除算はない)
5 % 2 // 1(剰余)
5 ** 2 // 25(べき乗、ES2016+)
// インクリメント/デクリメント
let i = 5
i++ // 5を返してからi=6にする(後置)
++i // i=7にしてから7を返す(前置)
i-- // 7を返してからi=6
--i // i=5にしてから5を返す
4-2. 論理演算子と短絡評価
**短絡評価(Short-circuit Evaluation)**とは、左辺の値だけで結果が決まる場合に右辺を評価しない仕組みです。これはパフォーマンス最適化だけでなく、条件付き処理のイディオムとしても使われます。
// AND: 左がfalsyなら左を返す、そうでなければ右を返す
false && 'hello' // false(左がfalsyなので右は評価しない)
'hello' && 'world' // 'world'(左がtruthyなので右を返す)
null && heavyComputation() // null(heavyComputationは呼ばれない!)
// OR: 左がtruthyなら左を返す、そうでなければ右を返す
null || 'default' // 'default'
'value' || 'default' // 'value'(左がtruthyなので右は評価しない)
// Nullish Coalescing(??): null/undefinedのみにフォールバック(ES2020+)
null ?? 'default' // 'default'
undefined ?? 'default' // 'default'
0 ?? 'default' // 0(0はfalsyだがnull/undefinedではない)
'' ?? 'default' // ''(空文字もnull/undefinedではない)
false ?? 'default' // false(falseもnullishではない)
// || と ?? の違い(重要)
const count = 0
const display = count || 'なし' // 'なし'(0はfalsyなので || のデフォルトに)
const display2 = count ?? 'なし' // 0(nullishでないので0が返る)
// 実用例: ?? は「値がないとき」のデフォルト値に適している
function createUser({ name = 'unknown', age = 0, active = true } = {}) {
return { name, age, active }
}
// デフォルト引数はundefinedの場合のみ適用される
// ageに0を渡したいとき、|| を使うとデフォルトになってしまう
論理代入演算子(ES2021+)
// ||= : 左がfalsyな場合のみ右を代入
let x = 0
x ||= 'default' // x = 'default'(0はfalsyなので)
// ??= : 左がnullishな場合のみ右を代入
let y = 0
y ??= 'default' // y = 0(0はnullishでないので変わらない)
let z = null
z ??= 'default' // z = 'default'
// &&= : 左がtruthyな場合のみ右を代入
let w = 'value'
w &&= 'new value' // w = 'new value'(左がtruthyなので)
4-3. Optional Chaining(ES2020+)
const user = {
profile: {
address: null
}
}
// 従来(ネストが深いとboilerplateになる)
const city = user && user.profile && user.profile.address && user.profile.address.city
// Optional Chaining(?.)
const city = user?.profile?.address?.city // undefined(エラーなし)
// メソッド呼び出し(?.())
user?.getProfile?.() // getProfileがなければundefined
// 配列アクセス(?.[])
const arr = null
arr?.[0] // undefined
// 実用例: APIレスポンスの深いネスト
const userName = response?.data?.user?.profile?.name ?? 'ゲスト'
4-4. スプレッド演算子とレスト構文
// スプレッド: 展開
const arr1 = [1, 2, 3]
const arr2 = [...arr1, 4, 5] // [1, 2, 3, 4, 5]
const obj1 = { a: 1 }
const obj2 = { ...obj1, b: 2 } // { a: 1, b: 2 }
// レスト: 残りをまとめる
function sum(...args) {
return args.reduce((a, b) => a + b, 0)
}
sum(1, 2, 3, 4) // 10
// 分割代入とレスト
const [first, ...rest] = [1, 2, 3, 4]
// first=1, rest=[2,3,4]
const { a, ...others } = { a: 1, b: 2, c: 3 }
// a=1, others={ b:2, c:3 }
4-5. 比較演算子と同値性
// == (緩い等価): 型変換が行われる
0 == false // true(falseが0に変換)
0 == '' // true('' が0に変換)
null == undefined // true(特別ルール)
null == 0 // false
'' == false // true
// === (厳密等価): 型変換なし(推奨)
0 === false // false(型が違う)
null === undefined // false(型が違う)
NaN === NaN // false(NaNは自分自身と等しくない!)
Number.isNaN(NaN) // true(NaNチェックの正しい方法)
// Object.is: === とほぼ同じだが2つの例外
Object.is(NaN, NaN) // true(=== と異なる)
Object.is(0, -0) // false(=== と異なる)
0 === -0 // true(浮動小数点の落とし穴)
// 比較演算子とオブジェクト
const a = { x: 1 }
const b = { x: 1 }
a === b // false(参照が異なる)
a === a // true(同じ参照)
== 型変換の全体像(重要な落とし穴)
== 演算子の型変換ルール(複雑なので === を使うことを推奨):
null == undefined → true(唯一の例外ルール)
null == 他の値 → false(常にfalse)
NaN == 何でも → false(NaNは自分自身とも不等)
片方がBoolean → ToNumberに変換
片方がString → ToNumberに変換
片方がオブジェクト → ToPrimitiveに変換
実際の変換例:
'' == false → 0 == 0 → true
'0' == false → 0 == 0 → true('0' はtrueなのに)
[] == false → '' == 0 → 0 == 0 → true
[] == ![] → [] == false → true(!)
4-6. ビット演算子・その他の演算子
// ビット演算子(低レベル処理・フラグ管理に使う)
const ADMIN = 0b0001 // 1
const EDITOR = 0b0010 // 2
const VIEWER = 0b0100 // 4
// フラグを組み合わせる(OR)
const role = ADMIN | EDITOR // 0b0011 = 3
// フラグを確認する(AND)
role & ADMIN // 1 (truthy) → ADMIN権限あり
role & VIEWER // 0 (falsy) → VIEWER権限なし
// フラグを削除する(AND NOT)
role & ~ADMIN // 0b0010 = 2 (EDITORのみ)
// よく使うビット演算トリック
Math.floor(4.7) // 4
~~4.7 // 4(double bitwise NOTでfloorと同等, NaNは0)
n >> 1 // n / 2の整数部分(右シフト)
n & 1 // nが奇数か偶数か(0: 偶数, 1: 奇数)
// typeof演算子
typeof 42 // 'number'
typeof 'hello' // 'string'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof null // 'object'(歴史的バグ)
typeof Symbol() // 'symbol'
typeof 42n // 'bigint'
typeof {} // 'object'
typeof [] // 'object'(配列もオブジェクト)
typeof function(){} // 'function'
// instanceof演算子
[] instanceof Array // true
[] instanceof Object // true(配列はObjectでもある)
new Date() instanceof Date // true
// void演算子(undefinedを返す)
void 0 // undefined
void 'anything' // undefined
// 用途: JSXで副作用だけの関数呼び出し(古いパターン)
<button onClick={() => void updateState()}>...</button>
4-7. このセクションのまとめ
演算子の重要ポイント:
論理演算子の短絡評価:
&& → 左がfalsyなら左を返す(右は評価しない)
|| → 左がtruthyなら左を返す(右は評価しない)
?? → 左がnull/undefinedのみフォールバック(0, '', falseは通過)
|| と ?? の使い分け:
count || 'なし' → 0や '' もデフォルトになる(意図しない場合がある)
count ?? 'なし' → null/undefinedのみデフォルトになる(0や '' は保持)
Optional Chaining(?.):
深いネストへの安全なアクセス
user?.profile?.address?.city → どこかがnull/undefinedならundefinedを返す
スプレッド(...)とレスト(...):
スプレッド: 配列・オブジェクトを展開(コピー、マージに使う)
レスト: 残りの引数・要素をまとめる
同値性:
== は型変換あり(複雑でバグの元)→ 基本的に使わない
=== は型変換なし(推奨)
Object.isは === と同じだがNaN === NaNがtrue、0 !== -0
オブジェクト・配列の比較は参照比較(内容比較はJSON.stringifyやstructuredClone等)
ビット演算子:
フラグ管理(権限制御)に使える
| でフラグ結合、& でフラグ確認、~& でフラグ削除
5. 制御フロー
このセクションではJavaScriptの制御フロー(条件分岐・ループ)を整理します。特に「どのループをいつ使うか」の判断基準と、よくある落とし穴(switch のフォールスルー、for...in の危険性)を理解します。
5-1. 条件分岐
if / else if / else
function classify(score) {
if (score >= 90) {
return 'A'
} else if (score >= 70) {
return 'B'
} else if (score >= 50) {
return 'C'
} else {
return 'F'
}
}
// 三項演算子(シンプルな条件)
const label = isAdmin ? '管理者' : '一般ユーザー'
// ネストした三項演算子(読みにくいので避ける)
// ❌ 読みにくい
const grade = score >= 90 ? 'A' : score >= 70 ? 'B' : score >= 50 ? 'C' : 'F'
// ✅ if/elseの方が明確
function getGrade(score) {
if (score >= 90) return 'A'
if (score >= 70) return 'B'
if (score >= 50) return 'C'
return 'F'
}
switch
function getDayName(day) {
switch (day) {
case 0: return '日曜日'
case 1: return '月曜日'
case 2: return '火曜日'
case 3: return '水曜日'
case 4: return '木曜日'
case 5: return '金曜日'
case 6: return '土曜日'
default: return '不明'
}
}
// フォールスルーの落とし穴(breakを忘れると次のケースも実行)
switch (x) {
case 1:
console.log('1です')
// breakを忘れると...
case 2:
console.log('2です') // x===1のときもここが実行される!
break
}
// 意図的なフォールスルー(複数ケースに同じ処理)
switch (color) {
case 'red':
case 'orange':
case 'yellow':
return '暖色系'
case 'blue':
case 'green':
return '寒色系'
default:
return 'その他'
}
switch は break を忘れるとフォールスルーが発生します。return を使うと break 不要です。
5-2. ループ
どのループをいつ使うか
| ループ | 用途 | 特徴 |
|---|---|---|
for |
インデックス制御 | 最も汎用的。逆順・スキップも可能 |
while |
条件が満たされる間 | 回数が不定の処理 |
for...of |
イテラブルの値 | 配列・文字列・Set・Mapに推奨 |
for...in |
オブジェクトのキー | プロトタイプキーも列挙するリスクあり |
forEach |
配列の各要素 | breakできない。非同期に注意 |
for await...of |
非同期イテラブル | AsyncGeneratorを消費するとき |
// for文(インデックス制御)
for (let i = 0; i < 5; i++) {
console.log(i)
}
// 逆順
for (let i = arr.length - 1; i >= 0; i--) {
console.log(arr[i])
}
// while文(条件が不定のとき)
let i = 0
while (i < 5) {
console.log(i++)
}
// do...while(最低1回は実行)
let attempts = 0
do {
attempts++
const success = tryOperation()
if (success) break
} while (attempts < 3)
// for...of(イテラブルの値を順に取得)
const fruits = ['apple', 'banana', 'cherry']
for (const fruit of fruits) {
console.log(fruit)
}
// インデックスも欲しい場合
for (const [index, fruit] of fruits.entries()) {
console.log(index, fruit)
}
// for...in(オブジェクトのキーを列挙)
// ⚠ プロトタイプのキーも列挙されるリスクがある
const obj = { a: 1, b: 2 }
for (const key in obj) {
if (Object.hasOwn(obj, key)) { // 自身のプロパティのみ
console.log(key, obj[key])
}
}
// ✅ Object.keys() + for...ofがより安全
for (const key of Object.keys(obj)) {
console.log(key, obj[key])
}
// forEach(配列の各要素に関数を適用)
fruits.forEach((fruit, index) => {
console.log(index, fruit)
})
// ⚠ forEachはbreakできない
// ⚠ forEach内のasync/awaitは完了を待てない(for...ofを使う)
5-3. break / continue / ラベル
// breakでループを抜ける
for (let i = 0; i < 10; i++) {
if (i === 5) break
console.log(i) // 0 1 2 3 4
}
// continueで次のイテレーションへ
for (let i = 0; i < 5; i++) {
if (i % 2 === 0) continue
console.log(i) // 1 3
}
// ラベル付きbreak(ネストループを外から抜ける)
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) break outer
console.log(i, j)
}
}
// 出力: 0,0 0,1 0,2 1,0(i=1,j=1でbreak outer)
5-4. 早期リターンパターン(Guard Clauses)
早期リターン(ガード節)は、ネストを減らしてコードを読みやすくする重要なテクニックです。
// ❌ 深いネスト(読みにくい)
function processOrder(order) {
if (order) {
if (order.items && order.items.length > 0) {
if (order.user) {
if (order.user.isActive) {
// やっと本処理
return processPayment(order)
} else {
return { error: 'ユーザーが非アクティブ' }
}
} else {
return { error: 'ユーザーがいない' }
}
} else {
return { error: '注文品がない' }
}
} else {
return { error: '注文が無効' }
}
}
// ✅ 早期リターン(読みやすい)
function processOrder(order) {
if (!order) return { error: '注文が無効' }
if (!order.items?.length) return { error: '注文品がない' }
if (!order.user) return { error: 'ユーザーがいない' }
if (!order.user.isActive) return { error: 'ユーザーが非アクティブ' }
// ガード節を全て通過したら本処理
return processPayment(order)
}
// ✅ throwでガード節
function assertPositive(n) {
if (typeof n !== 'number') throw new TypeError(`数値が必要です: ${n}`)
if (n <= 0) throw new RangeError(`正の数が必要です: ${n}`)
return n
}
5-5. このセクションのまとめ
制御フローの選択指針:
条件分岐:
if/else → 複雑な条件、複数分岐
三項演算子 → シンプルな2択(ネストしない)
switch → 同一変数の多値比較(break忘れに注意)
早期リターン(ガード節)→ ネストを減らして可読性UP
ループの選択:
for...of → 配列・Set・Map・文字列を順に処理(推奨)
for → インデックスが必要、逆順、スキップ
for...in → オブジェクトのキー(hasOwnチェック必須)
forEach → 副作用のある処理(break不可、非同期不可)
while → 回数が不定の条件ループ
for await...of → 非同期イテラブルを順に処理
よくある落とし穴:
switchのbreak忘れ → フォールスルー
for...inでプロトタイプキーを拾う → Object.keys() を使う
forEachでawait → 完了を待てない → for...ofを使う
6. 関数(宣言・式・アロー)
このセクションでは関数の3種類の書き方、this の挙動の違い、クロージャモジュールパターン、純粋関数と副作用について詳しく説明します。関数はJavaScriptの中心的な概念であり、理解の深さがコードの質を大きく左右します。
6-1. 3種類の関数定義と巻き上げの違い
関数宣言(Function Declaration)
// 関数宣言: hoisting(巻き上げ)により宣言前に呼べる
greet('Alice') // ✅ 'Hello, Alice!'(定義前でも呼べる!)
function greet(name) {
return `Hello, ${name}!`
}
関数宣言は完全に巻き上げられる(宣言も本体も)ため、ファイルのどこに書いても呼び出せます。
関数式(Function Expression)
// 関数式: 変数に代入する形。hoistingは変数の宣言のみ
// console.log(greet) // undefined(varなら)またはReferenceError(let/constなら)
const greet = function(name) {
return `Hello, ${name}!`
}
greet('Alice') // ✅ 宣言後のみ呼べる
// 名前付き関数式(Named Function Expression)
// - スタックトレースにデバッグ用の名前が表示される
// - 再帰呼び出しに使える
const factorial = function fact(n) {
return n <= 1 ? 1 : n * fact(n - 1) // 内部からfactで参照できる
}
factorial(5) // 120
// fact(5) // ❌ ReferenceError(外からは参照不可)
アロー関数(Arrow Function, ES2015+)
// アロー関数の構文バリエーション
const greet = (name) => `Hello, ${name}!` // 1行なら{}とreturn省略
const double = n => n * 2 // 引数が1つなら括弧省略可
const getObj = () => ({ key: 'value' }) // オブジェクトを返す場合は()で包む
const sum = (a, b) => {
const result = a + b
return result // 複数行は{}とreturnが必要
}
3種類の比較表
| 特性 | 関数宣言 | 関数式 | アロー関数 |
|---|---|---|---|
| 巻き上げ | 完全(宣言前に呼べる) | 変数のみ(TDZ) | 変数のみ(TDZ) |
this |
呼び出し方による | 呼び出し方による | 定義時のスコープを継承 |
arguments |
あり | あり | なし |
new でインスタンス化 |
可能 | 可能 | 不可(TypeError) |
prototype |
あり | あり | なし |
| メソッドとして使用 | 推奨 | 可能 | 非推奨(thisが変わる) |
| コールバックとして使用 | 可能 | 可能 | 推奨(thisが変わらない) |
6-2. アロー関数vs通常関数(thisの扱い)
this はJavaScriptで最も混乱しやすいトピックの一つです。通常関数とアロー関数で this の決まり方が根本的に異なります。
// thisの決まり方(通常関数)
// → 「呼び出し時点」でどのオブジェクトから呼ばれたかによって決まる
const obj = {
name: 'Alice',
// 通常関数: thisはobj(オブジェクトのメソッドとして呼ばれるとき)
greet: function() {
console.log(this.name) // 'Alice'
},
// アロー関数: thisは「定義時」の外側スコープのthis
// オブジェクトリテラルはスコープを作らないので、thisはグローバルor undefined
greetArrow: () => {
console.log(this.name) // undefined(thisはwindowまたはundefined in strict mode)
},
// コールバック内でのthis問題と解決
greetAfterDelay: function() {
// ✅ アロー関数は定義時のthis(= obj)を継承する
setTimeout(() => {
console.log(this.name) // 'Alice'(アローは外側のthisを使う)
}, 1000)
}
}
obj.greet() // 'Alice'
obj.greetArrow() // undefined(thisはobjではない)
// ❌ メソッドを変数に代入するとthisが変わる
const fn = obj.greet
fn() // undefined(thisがグローバルになる)
// ✅ bindで明示的にthisを固定
const boundFn = obj.greet.bind(obj)
boundFn() // 'Alice'
// thisの4つの決まり方(通常関数)
// 1. デフォルト: グローバル(strict modeではundefined)
function fn() { return this } // ブラウザならwindow
// 2. メソッド呼び出し: そのオブジェクト
obj.method() // this = obj
// 3. new呼び出し: 新しいオブジェクト
const instance = new Constructor() // this = 新しいインスタンス
// 4. call/apply/bind: 明示的に指定
fn.call(obj, arg1) // this = obj
fn.apply(obj, [args]) // this = obj
const bound = fn.bind(obj) // thisをobjに固定した新しい関数
// argumentsオブジェクト(通常関数のみ)
function normalFn() {
console.log(arguments[0], arguments.length) // Arguments配列ライク
// ただし真の配列ではないのでmapなどは使えない
const args = Array.from(arguments) // 配列に変換
}
// アロー関数: argumentsは存在しない
const arrowFn = () => {
// console.log(arguments) // ReferenceError(外側のargumentsがあれば参照)
}
// ✅ レスト引数を使う(アロー・通常関数どちらでも使える)
const fn = (...args) => args // 真の配列として取得できる
// newでインスタンス化できない(アロー関数のみ)
const Fn = () => {}
new Fn() // ❌ TypeError: Fn is not a constructor
6-3. デフォルト引数・レスト引数・分割代入引数
// デフォルト引数(ES2015+)
// undefinedが渡された場合のみデフォルト値が使われる
function createUser(name, role = 'user', active = true) {
return { name, role, active }
}
createUser('Alice') // { name:'Alice', role:'user', active:true }
createUser('Bob', 'admin') // { name:'Bob', role:'admin', active:true }
createUser('Charlie', undefined, false) // { name:'Charlie', role:'user', active:false }
createUser('Dave', null, false) // { name:'Dave', role:null, active:false }(nullはデフォルト使われない!)
// デフォルト引数の式(動的な値も使える)
function createId(prefix = 'id', suffix = Date.now()) {
return `${prefix}_${suffix}`
}
// レスト引数(...args)
function logAll(first, second, ...rest) {
console.log(first, second, rest) // restは真の配列
}
logAll(1, 2, 3, 4, 5) // 1 2 [3, 4, 5]
// レストは最後の引数のみ
// function bad(...a, b) {} // ❌ SyntaxError
// 分割代入を引数に使う(Named Parametersパターン)
// ✅ キーワード引数のように使える(順序を気にしなくていい)
function printUser({ name, age = 0, role = 'user' }) {
console.log(`${name} (${age}) - ${role}`)
}
printUser({ name: 'Alice', age: 30 }) // Alice (30) - user
printUser({ role: 'admin', name: 'Bob' }) // Bob (0) - admin(順序自由)
// ✅ オプション引数が多い関数に特に有効
function fetchData({
url,
method = 'GET',
headers = {},
body = null,
timeout = 5000,
retry = 0
} = {}) { // = {} で引数省略時もエラーにならない
return fetch(url, { method, headers, body })
}
fetchData({ url: '/api/users' })
fetchData({ url: '/api/users', method: 'POST', body: JSON.stringify(data) })
6-4. IIFEパターンとクロージャモジュール
IIFE(即時実行関数式)
IIFEは「定義と同時に実行する関数」です。ES2015以前、モジュールシステムがなかった時代の「スコープ汚染を防ぐ」主要パターンでした。
// 変数スコープを閉じ込める古いパターン(現在はモジュールで代替)
;(function() {
const private = 'この変数は外から見えない'
// グローバルスコープを汚染しない
})()
// アロー版
;(() => {
const local = '外から見えない'
})()
// IIFEで返り値を受け取る
const config = (function() {
const BASE_URL = 'https://api.example.com'
const TIMEOUT = 5000
return { BASE_URL, TIMEOUT } // 必要なものだけ公開
})()
config.BASE_URL // 'https://api.example.com'
// BASE_URLは外からアクセスできない
クロージャベースのモジュールパターン
IIFEとクロージャを組み合わせると、プライベート状態を持つモジュールを作れます。ES2015以降はクラスのプライベートフィールド(#field)やESMが代替手段になりますが、歴史的に重要なパターンです。
// ❌ クロージャなし: stateがグローバルに露出
let count = 0
function increment() { count++ }
function decrement() { count-- }
function getCount() { return count }
// count = 999 // 外から書き換えられてしまう
// ✅ クロージャモジュールパターン
const counter = (function() {
let count = 0 // プライベート変数(外からアクセス不可)
return {
increment() { count++ },
decrement() { count-- },
getCount() { return count },
reset() { count = 0 }
}
})()
counter.increment() // 内部のcountは1
counter.increment() // 内部のcountは2
counter.getCount() // 2
// counter.count // undefined(アクセスできない)
// ファクトリ関数パターン(複数のインスタンスを作れる)
function createCounter(initialValue = 0) {
let count = initialValue
return {
increment: () => ++count,
decrement: () => --count,
value: () => count
}
}
const counter1 = createCounter(10)
const counter2 = createCounter(0)
counter1.increment() // 11(counter2とは独立)
counter2.increment() // 1
6-5. 純粋関数と副作用
// 純粋関数(Pure Function): 同じ引数→常に同じ結果、外部を変えない
function add(a, b) {
return a + b // 外部に何も影響しない、いつ呼んでも同じ結果
}
// 副作用あり(外部の状態を変える)
let count = 0
function increment() {
count++ // 外部変数を変更(副作用)
console.log(count) // I/Oも副作用
}
// 純粋でない関数の例
function getTimestamp() {
return Date.now() // 呼ぶたびに値が変わる
}
function randomId() {
return Math.random().toString(36) // ランダム性がある
}
// 配列・オブジェクトの純粋な変換(イミュータブルパターン)
// ❌ ミューテーション(元の配列・オブジェクトを変える)
function addItem(arr, item) {
arr.push(item) // 元の配列を破壊する副作用
return arr
}
function updateUser(user, updates) {
Object.assign(user, updates) // 元のオブジェクトを変更
return user
}
// ✅ イミュータブルな変換(新しいオブジェクトを返す)
function addItem(arr, item) {
return [...arr, item] // 元のarrは変わらない
}
function updateUser(user, updates) {
return { ...user, ...updates } // 元のuserは変わらない
}
// ✅ 配列の純粋な変換メソッド
const users = [{ id: 1, active: true }, { id: 2, active: false }]
// map: 変換
const names = users.map(u => u.name) // 元のusersは変わらない
// filter: 絞り込み
const active = users.filter(u => u.active) // 元のusersは変わらない
// 特定のidを更新
const updated = users.map(u => u.id === 1 ? { ...u, active: false } : u)
純粋関数のメリット:
テストが容易(引数を渡すだけで結果を確認できる)
キャッシュ可能(同じ引数なら同じ結果なのでmemoizeできる)
予測可能(外部状態に依存しない)
並列処理に安全
6-6. このセクションのまとめ
関数の3種類と使い分け:
関数宣言 → トップレベルの名前付き関数(巻き上げあり)
関数式 → 名前付き関数式(スタックトレースに名前が出る)、変数に代入
アロー関数 → コールバック・短い変換関数(thisを継承する)
thisの決まり方(通常関数):
デフォルト → グローバル(or undefined in strict mode)
メソッド → そのオブジェクト
new → 新しいインスタンス
call/apply/bind → 明示的に指定
アロー関数のthis: 定義時の外側スコープを継承(変わらない)
IIFE: ES2015以前のスコープ隔離パターン。現在はESMで代替
クロージャモジュール:
プライベート変数を持つオブジェクトを返す
現在はクラスの #privateFieldやESMで代替
純粋関数:
同じ引数 → 同じ結果
外部状態を変えない
テスト・メモ化・並列処理に有利
次のセクションでは、スコープ・クロージャ・巻き上げをさらに深く、歴史的背景も含めて解説します。
7. スコープ・クロージャ・巻き上げ
このセクションでは「スコープ(変数が見える範囲)」「クロージャ(外側スコープへの参照の保持)」「巻き上げ(Hoisting)」を深掘りします。クロージャはJavaScriptで最も重要かつ誤解されやすい概念で、正しく理解することでバグを防ぎ、強力なパターンを使えるようになります。
7-1. スコープの種類(グローバル・関数・ブロック・モジュール)
スコープとは「変数が参照できる範囲」です。JavaScriptには4種類のスコープがあります。
// グローバルスコープ: ファイルのトップレベル(どこからでも見える)
const globalVar = 'global'
function outer() {
// 関数スコープ: 関数の中(その関数と内側から見える)
const outerVar = 'outer'
function inner() {
// 関数スコープ(ネスト): 内側の関数
const innerVar = 'inner'
console.log(globalVar) // ✅ 外側のスコープへアクセス可(スコープチェーン)
console.log(outerVar) // ✅
console.log(innerVar) // ✅
}
inner()
// console.log(innerVar) // ❌ ReferenceError(内側のスコープへアクセス不可)
}
// ブロックスコープ: {} の中(let/constのみ)
{
let blockVar = 'block'
const blockConst = 'also block'
var oldVar = 'varは関数スコープなので漏れる'
}
// console.log(blockVar) // ❌ ReferenceError
// console.log(blockConst) // ❌ ReferenceError
console.log(oldVar) // ✅ 'varは関数スコープなので漏れる'(!)
// モジュールスコープ(ES2015+)
// <script type="module"> または .mjsファイルでは
// トップレベルの変数はそのモジュール内のみで有効
// module-a.js
const privateVar = 'このモジュールのみ'
export const publicVar = '外部に公開'
// module-b.js
import { publicVar } from './module-a.js'
// privateVarは見えない(❌ ReferenceError)
スコープチェーン(Scope Chain)
スコープチェーンの仕組み:
inner() が変数xを探す:
1. inner自身のスコープを確認 → なければ
2. outerのスコープを確認 → なければ
3. グローバルスコープを確認 → なければ
4. ReferenceError
この「内側から外側へ順に探す」仕組みがスコープチェーン
▲ スコープチェーン(左):変数検索は内側から外側へ連鎖する。クロージャ(右):makeCounter() が終了してもヒープ上のcount変数はincrement() によって保持され続ける。
7-2. varの問題とブロックスコープの歴史
なぜ var は問題なのか
ES5以前、変数宣言の方法は var だけでした。var の設計には以下の問題があります:
// 問題1: ブロックスコープを持たない
for (var i = 0; i < 3; i++) {
// ...
}
console.log(i) // 3(forブロックを抜けてもアクセスできる!)
// 問題2: 再宣言できる(エラーにならない)
var x = 1
var x = 2 // ✅ エラーにならない(意図しない再宣言が起きる)
console.log(x) // 2
// 問題3: 関数全体に「宣言が」巻き上げられる
function example() {
console.log(x) // undefined(ReferenceErrorにならない!)
if (true) {
var x = 5
}
console.log(x) // 5
}
// 問題4: グローバル変数になる(ブラウザではwindowのプロパティになる)
var globalLeak = 'window.globalLeakにもなる'
window.globalLeak // ブラウザなら 'window.globalLeakにもなる'
let/const のブロックスコープ(ES2015で解決)
// ✅ ブロックスコープ
for (let i = 0; i < 3; i++) {
// ...
}
// console.log(i) // ❌ ReferenceError
// ✅ 再宣言不可
let x = 1
// let x = 2 // ❌ SyntaxError
// ✅ TDZ: 宣言前アクセスはエラー(サイレントな失敗がなくなる)
// console.log(y) // ❌ ReferenceError(TDZ)
let y = 5
7-3. 巻き上げ(Hoisting)
巻き上げとは「変数・関数の宣言がスコープの先頭に引き上げられたように振る舞う」現象です。実際にコードが移動するわけではなく、コンパイル段階で宣言が認識される仕組みです。
// 関数宣言: 宣言も本体も完全に巻き上げられる
greet('Alice') // ✅ 'Hello, Alice!'(定義前でも呼べる)
function greet(name) {
return `Hello, ${name}!`
}
// var: 宣言だけが巻き上げられる(値は巻き上げられない)
console.log(x) // undefined(ReferenceErrorではない!)
var x = 5
console.log(x) // 5
// 上記は以下と同等(実際の動作):
var x // 宣言が先頭に巻き上げられる(初期値はundefined)
console.log(x) // undefined
x = 5
console.log(x) // 5
// let/const: TDZ(Temporal Dead Zone)により巻き上げるがアクセス不可
console.log(y) // ❌ ReferenceError: Cannot access 'y' before initialization
let y = 5
// 関数式: 変数の巻き上げルールに従う
// console.log(fn) // undefined(var)or ReferenceError(let/const)
const fn = function() {} // constなのでTDZ
巻き上げまとめ:
宣言の種類 | 巻き上げられるもの | アクセスしたら
─────────────────────────────────────────
function宣言 | 宣言 + 本体 | ✅ 正常動作
var宣言 | 宣言のみ(値はundefined) | undefinedを返す
let/const宣言 | 宣言のみ(TDZ) | ❌ ReferenceError
class宣言 | 宣言のみ(TDZ) | ❌ ReferenceError
7-4. クロージャ(Closure)の深掘り
クロージャとは「関数が定義されたスコープへの参照を、関数が返された後も保持する」仕組みです。JavaScriptの最重要概念の一つです。
なぜクロージャが起きるのか
// 通常の関数: 実行が終わると変数は消える(ように見える)
function add(a, b) {
const result = a + b // 実行終了後、resultはGCされる
return result
}
// クロージャ: 関数が参照している外側の変数は消えない
function makeCounter() {
let count = 0 // ← この変数は返した関数が参照している
return function() {
return ++count // countを参照し続けているのでGCされない
}
}
const counter = makeCounter()
// makeCounterの実行は終わったが、countは生き続けている
counter() // 1
counter() // 2
counter() // 3
// クロージャの仕組み(内部表現のイメージ)
// counterが持っているのは:
// {
// fn: function() { return ++count },
// [[Scope]]: { count: 3 } ← 外側スコープへの参照
// }
// 実用例1: カウンター(カプセル化)
function makeCounter() {
let count = 0
return {
increment: () => ++count,
decrement: () => --count,
value: () => count
}
}
const counter = makeCounter()
counter.increment() // 1
counter.increment() // 2
counter.decrement() // 1
counter.value() // 1
// countは外から直接アクセスできない(カプセル化)
// 実用例2: ファクトリ関数
function createMultiplier(factor) {
return function(number) {
return number * factor // factorをクロージャで保持
}
}
const double = createMultiplier(2) // factor = 2をクロージャに閉じ込める
const triple = createMultiplier(3) // factor = 3をクロージャに閉じ込める
double(5) // 10(factorは2)
triple(5) // 15(factorは3)
// 実用例3: メモ化(memoization)
function memoize(fn) {
const cache = new Map() // cacheはクロージャで保持される
return function(...args) {
const key = JSON.stringify(args)
if (cache.has(key)) return cache.get(key) // キャッシュヒット
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
const expensiveCalc = memoize((n) => {
console.log(`計算中: ${n}`)
return n * n
})
expensiveCalc(5) // 計算中: 5 → 25
expensiveCalc(5) // (計算しない)→ 25(キャッシュから)
7-5. ループ内クロージャの落とし穴と修正
これはJavaScriptの最も有名な落とし穴の一つです。
// ❌ 問題: var + setTimeoutの組み合わせ
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}
// 期待: 0 1 2
// 実際: 3 3 3
// 理由: varのスコープは関数全体(ループ全体で1つのi)
// setTimeoutが実行されるとき(100ms後)にはforが終わっていてi=3
// ✅ 解決1: letを使う(推奨)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}
// 0 1 2(each iterationに独立したiが作られる)
// 理由: letはブロックスコープなので、各イテレーションで独立したiを持つ
// ✅ 解決2: IIFEでスコープを作る(letがない時代の解法)
for (var i = 0; i < 3; i++) {
;(function(j) {
setTimeout(() => console.log(j), 100)
})(i) // iの現在値をjにコピーしてクロージャに閉じ込める
}
// 0 1 2
// ✅ 解決3: bindを使う
for (var i = 0; i < 3; i++) {
setTimeout(console.log.bind(null, i), 100)
}
// 0 1 2
// ✅ 解決4: 配列に値を先に保存
const timeouts = [0, 1, 2].map(i => {
return () => console.log(i)
})
timeouts.forEach(fn => setTimeout(fn, 100))
// 0 1 2
7-6. クロージャによるメモリリーク
クロージャは便利ですが、不要なオブジェクトへの参照を保持し続けることでメモリリークを起こすことがあります。
// ❌ 問題: 大きなデータをクロージャが保持し続ける
function setup() {
const bigData = new Array(1000000).fill('data') // 大きな配列
return {
process: () => bigData.length // bigDataを参照し続ける
// bigData全体がGCされない!
}
}
const handler = setup()
// handlerが生きている限りbigDataもメモリに残り続ける
// ✅ 解決: 必要な値だけ取り出してクロージャに閉じ込める
function setup() {
const bigData = new Array(1000000).fill('data')
const length = bigData.length // 必要な値だけ取り出す
// bigDataはここ以降参照されないのでGC可能
return {
process: () => length // lengthだけを参照(小さな値)
}
}
// ❌ もう一つの落とし穴: イベントリスナーのリーク
function setupButton() {
const largeData = fetchLargeData() // 大きなデータ
button.addEventListener('click', () => {
// largeDataを使わないが、クロージャで参照している
console.log('clicked')
})
// buttonが消えても、largeDataはlargeDataを参照しているクロージャが
// イベントリスナーとして残っているためGCされない
}
// ✅ 解決: 不要な参照をnullに
function setupButton() {
let largeData = fetchLargeData()
const processed = processData(largeData) // 必要な処理を先に終える
largeData = null // 参照を手放す(GC可能に)
button.addEventListener('click', () => {
console.log(processed) // 処理済みの小さなデータのみ参照
})
}
// ✅ または: リスナーを削除する(推奨)
const handler = () => console.log('clicked')
button.addEventListener('click', handler)
// 不要になったら
button.removeEventListener('click', handler)
7-7. このセクションのまとめ
スコープの4種類:
グローバル → どこからでもアクセス可(汚染注意)
関数スコープ → その関数と内側からのみ
ブロックスコープ → let/constの {} 内のみ
モジュールスコープ → そのファイルのみ(ESM)
巻き上げ(Hoisting):
function宣言 → 宣言も本体も巻き上げ(どこからでも呼べる)
var → 宣言のみ巻き上げ(値はundefined)
let/const → TDZにより宣言前アクセスはReferenceError
クロージャ:
定義: 関数が外側スコープへの参照を保持する仕組み
用途: カプセル化、ファクトリ関数、メモ化、コールバック
注意: 不要なオブジェクトを保持するとメモリリーク
ループ内クロージャの落とし穴:
var + setTimeout → 同じi(ループ後の値)を参照してしまう
解決策1: letを使う(推奨)
解決策2: IIFEで各イテレーションの値をコピー
解決策3: bindでその時の値を固定
メモリリーク対策:
必要な値だけクロージャに閉じ込める(大きなデータはnullに)
イベントリスナーはremoveEventListenerでクリーンアップ
次のセクションでは、オブジェクトと配列の高階関数・分割代入・イミュータブルパターンを詳しく説明します。
8. オブジェクトと配列
このセクションではJavaScriptのデータ構造の中核であるオブジェクトと配列を体系的に解説します。特に「イミュータブルな操作」「高階関数(map/filter/reduce)」「分割代入の応用」は現代のコードで頻繁に使われます。
8-1. オブジェクトの基礎
// オブジェクトリテラル
const user = {
id: 1,
name: 'Alice',
'email-address': 'alice@example.com', // 特殊文字はクォートで
greet() { // メソッド短縮記法(ES2015+)
return `Hello, I'm ${this.name}`
}
}
// プロパティアクセス
user.name // 'Alice'(ドット記法)
user['name'] // 'Alice'(ブラケット記法)
user['email-address'] // ブラケット記法で特殊文字も
const key = 'name'
user[key] // 'Alice'(動的キー)
// 分割代入
const { id, name, greet } = user
// デフォルト値付き分割代入
const { id, name, age = 25 } = user // ageはundefinedなら25
// リネーム
const { name: userName } = user // userName = 'Alice'
8-2. オブジェクトの操作
// プロパティの存在チェック
'name' in user // true
Object.hasOwn(user, 'name') // true(ES2022+, 推奨)
user.hasOwnProperty('name') // true(古いが動く)
// プロパティの列挙
Object.keys(user) // ['id', 'name', 'email-address']
Object.values(user) // [1, 'Alice', 'alice@example.com', fn]
Object.entries(user) // [['id',1], ['name','Alice'], ...]
// オブジェクトのマージ
const defaults = { role: 'user', active: true }
const overrides = { role: 'admin' }
const merged = { ...defaults, ...overrides }
// { role: 'admin', active: true }(後ろが優先)
// Object.assign(同様だが元のオブジェクトを変える)
const result = Object.assign({}, defaults, overrides)
// プロパティの削除
delete user.id // idプロパティを削除(可能な限り避ける)
8-3. 計算プロパティ名(ES2015+)
const prefix = 'get'
const obj = {
[`${prefix}Name`]() { return 'Alice' }, // getNameメソッド
[`${prefix}Age`]() { return 30 } // getAgeメソッド
}
obj.getName() // 'Alice'
8-4. 配列の基礎と高階関数
const numbers = [1, 2, 3, 4, 5]
// map: 各要素を変換した新しい配列
numbers.map(n => n * 2) // [2, 4, 6, 8, 10]
// filter: 条件を満たす要素だけ
numbers.filter(n => n % 2 === 0) // [2, 4]
// reduce: 配列を単一の値に畳み込む
numbers.reduce((sum, n) => sum + n, 0) // 15(合計)
numbers.reduce((max, n) => Math.max(max, n), -Infinity) // 5(最大値)
// find: 最初にマッチした要素を返す(なければundefined)
numbers.find(n => n > 3) // 4
numbers.findIndex(n => n > 3) // 3(インデックス)
// some/every: 条件チェック
numbers.some(n => n > 4) // true(1つでも満たす)
numbers.every(n => n > 0) // true(全て満たす)
// flatMap: mapしてからflat
[[1,2],[3,4]].flatMap(x => x) // [1, 2, 3, 4]
[1, 2, 3].flatMap(n => [n, n * 2]) // [1, 2, 2, 4, 3, 6]
// includes: 要素の存在チェック
numbers.includes(3) // true
// at(): 末尾からのアクセス(ES2022+)
numbers.at(-1) // 5(末尾)
numbers.at(-2) // 4(末尾から2番目)
8-5. 配列のミューテーションvs非ミューテーション
const arr = [3, 1, 4, 1, 5, 9]
// ミューテーション(元の配列を変える)
arr.sort((a, b) => a - b) // [1, 1, 3, 4, 5, 9](元も変わる!)
arr.push(2) // [1, 1, 3, 4, 5, 9, 2]
arr.pop() // 2を返し、最後を削除
arr.splice(1, 2) // インデックス1から2要素削除
arr.reverse() // 逆順(元を変える)
// 非ミューテーション(新しい配列を返す)
const sorted = [...arr].sort((a, b) => a - b) // 元は変わらない
const withItem = [...arr, 2] // push相当
const withoutLast = arr.slice(0, -1) // pop相当
const reversed = [...arr].reverse() // reverse相当
// ES2023+
const sortedES2023 = arr.toSorted((a, b) => a - b)
const reversedES2023 = arr.toReversed()
8-6. 分割代入の応用
// ネストした分割代入
const {
user: {
name,
address: { city = 'Unknown' }
}
} = data
// 配列の分割代入
const [first, second, , fourth] = [1, 2, 3, 4] // 3をスキップ
const [head, ...tail] = [1, 2, 3, 4]
// head=1, tail=[2,3,4]
// 関数の戻り値を分割代入
function getRange() {
return [0, 100]
}
const [min, max] = getRange()
// swap(JavaScriptらしい書き方)
let a = 1, b = 2
;[a, b] = [b, a]
// a=2, b=1
8-7. MapとSet
通常のオブジェクトでは不十分な場合に使う特殊なコレクションです。
// Map: キーに任意の型を使えるオブジェクト(通常のオブジェクトとの違い)
const map = new Map()
// キーに文字列以外も使える
map.set('name', 'Alice')
map.set(42, 'number key')
map.set({ id: 1 }, 'object key') // オブジェクトもキーにできる
map.set(true, 'boolean key')
map.get('name') // 'Alice'
map.has('name') // true
map.size // 4
map.delete('name')
// Mapの反復
for (const [key, value] of map) {
console.log(key, value)
}
// Mapの初期化
const userMap = new Map([
['alice', { age: 30 }],
['bob', { age: 25 }]
])
// オブジェクトvs Mapの使い分け
// オブジェクト: JSONと互換、文字列キー限定、静的なデータ
// Map: 非文字列キー、頻繁な追加/削除、サイズ確認が必要な場合
// Set: 重複のないコレクション
const set = new Set()
set.add(1)
set.add(2)
set.add(2) // 重複: 無視される
set.add(3)
set.size // 3(2は1つのみ)
set.has(2) // true
set.delete(2)
// Setの反復
for (const value of set) {
console.log(value) // 1 3
}
// 配列の重複除去(最もよく使われるSetの用途)
const arr = [1, 2, 2, 3, 3, 4]
const unique = [...new Set(arr)] // [1, 2, 3, 4]
// 集合演算
const setA = new Set([1, 2, 3, 4])
const setB = new Set([3, 4, 5, 6])
// 和集合(union)
const union = new Set([...setA, ...setB]) // {1,2,3,4,5,6}
// 積集合(intersection)
const intersection = new Set([...setA].filter(x => setB.has(x))) // {3,4}
// 差集合(difference)
const difference = new Set([...setA].filter(x => !setB.has(x))) // {1,2}
8-8. reduceの深掘り
reduce は最も汎用的で強力な配列メソッドです。正しく使いこなすと複雑なデータ変換が簡潔に書けます。
// 基本: 合計
const sum = [1, 2, 3, 4, 5].reduce((acc, n) => acc + n, 0) // 15
// グループ化(Object.groupByが追加されたES2024以前はよくreduceで実装)
const items = [
{ name: 'apple', type: 'fruit' },
{ name: 'banana', type: 'fruit' },
{ name: 'carrot', type: 'vegetable' }
]
const grouped = items.reduce((acc, item) => {
const key = item.type
if (!acc[key]) acc[key] = []
acc[key].push(item)
return acc
}, {})
// { fruit: [{apple}, {banana}], vegetable: [{carrot}] }
// フラット化(flatの前時代の実装)
const nested = [[1, 2], [3, 4], [5, 6]]
const flat = nested.reduce((acc, arr) => [...acc, ...arr], [])
// [1, 2, 3, 4, 5, 6]
// 重複除去(Setを使わない場合)
const withDuplicates = [1, 2, 2, 3, 3, 4]
const deduped = withDuplicates.reduce((acc, n) => {
if (!acc.includes(n)) acc.push(n)
return acc
}, [])
// [1, 2, 3, 4]
// パイプライン(関数の連続適用)
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x)
const addOne = x => x + 1
const double = x => x * 2
const square = x => x * x
const transform = pipe(addOne, double, square)
transform(3) // ((3+1)*2)^2 = 64
8-9. Objectの静的メソッド詳解
// Object.assign: オブジェクトのマージ(破壊的)
const target = { a: 1 }
const result = Object.assign(target, { b: 2 }, { c: 3 })
// result === target === { a: 1, b: 2, c: 3 }(targetが変更される)
// Object.freeze: オブジェクトを不変にする(浅い)
const config = Object.freeze({ host: 'localhost', port: 3000 })
config.port = 8080 // エラー(strict mode)または無視
config.port // 3000(変わらない)
// ※ ネストしたオブジェクトはfreezeされない(浅いfreeze)
// Object.seal: プロパティの追加・削除を禁止(値の変更は可)
const sealed = Object.seal({ x: 1 })
sealed.x = 2 // OK
sealed.y = 3 // 無視(追加不可)
delete sealed.x // false(削除不可)
// Object.create: プロトタイプを指定してオブジェクトを作る
const base = { greet() { return `Hello, ${this.name}` } }
const alice = Object.create(base)
alice.name = 'Alice'
alice.greet() // "Hello, Alice"
// プロトタイプなしのオブジェクト(辞書として安全)
const dict = Object.create(null)
dict.key = 'value'
dict.hasOwnProperty // undefined(プロトタイプがない)
Object.hasOwn(dict, 'key') // true(安全)
// Object.fromEntries: entries配列からオブジェクトを作る
const entries = [['a', 1], ['b', 2], ['c', 3]]
Object.fromEntries(entries) // { a: 1, b: 2, c: 3 }
// Mapをオブジェクトに変換
const map = new Map([['x', 10], ['y', 20]])
Object.fromEntries(map) // { x: 10, y: 20 }
// 変換パイプライン
const obj = { a: 1, b: 2, c: 3 }
const doubled = Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, v * 2])
) // { a: 2, b: 4, c: 6 }
8-10. このセクションのまとめ
オブジェクトの操作:
プロパティアクセス: ドット記法orブラケット記法(動的キーはブラケット)
存在チェック: Object.hasOwn(obj, key) を使う(ES2022+)
列挙: Object.keys() / Object.values() / Object.entries()
マージ: { ...a, ...b }(後ろが優先)
配列の高階関数(非破壊的 → 元の配列を変えない):
map → 各要素を変換した新しい配列
filter → 条件を満たす要素のみの配列
reduce → 配列を単一の値に畳み込む
find → 最初にマッチした要素
some/every → 条件チェック(1つ/全部)
破壊的メソッド(元の配列を変える)に注意:
push, pop, shift, unshift, splice, sort, reverse
→ コピーしてから: [...arr].sort() またはES2023+ のtoSorted() 等
分割代入の活用:
関数引数に使う → Named Parametersパターン(順序自由)
デフォルト値: { name = 'default' } = {}
リネーム: { name: userName } = user
Objectの静的メソッド:
Object.assign → マージ(破壊的)
Object.freeze → 不変化(浅い)
Object.seal → 追加・削除禁止(値変更は可)
Object.create → プロトタイプ指定で作成
Object.fromEntries → entries配列からオブジェクトを作る
9. プロトタイプとクラス
このセクションではJavaScriptのオブジェクト指向の仕組みを「プロトタイプチェーン」から出発し、ES2015で導入されたクラス構文、継承、Mixinパターンまで解説します。「クラスはプロトタイプの糖衣構文」という事実を理解することで、両者の関係が明確になります。
9-1. プロトタイプチェーンの仕組み
// すべてのオブジェクトはプロトタイプを持つ
const arr = [1, 2, 3]
// arrのプロトタイプはArray.prototype
Object.getPrototypeOf(arr) === Array.prototype // true
// Array.prototypeにはmap, filterなどのメソッドが定義されている
arr.map // Function(Array.prototypeから継承)
// Array.prototypeのプロトタイプはObject.prototype
Object.getPrototypeOf(Array.prototype) === Object.prototype // true
// Object.prototypeのプロトタイプはnull(チェーンの終端)
Object.getPrototypeOf(Object.prototype) // null
// メソッドの解決順序(プロトタイプチェーン)
// arr.toString() が呼ばれると:
// 1. arr自身にtoStringがあるか? → ない
// 2. Array.prototypeにあるか? → ある! → 使う
▲ プロトタイプチェーン:arrインスタンス → Array.prototype(map, filterなど)→ Object.prototype(hasOwnPropertyなど)→ null(チェーン終端)。メソッドはこの順に検索される。
9-2. コンストラクタ関数(クラス前の書き方)
// コンストラクタ関数(クラスのES5以前の書き方)
function Person(name, age) {
this.name = name // インスタンスプロパティ
this.age = age
}
// メソッドはprototypeに定義する(インスタンスごとにコピーされない)
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`
}
const alice = new Person('Alice', 30)
alice.greet() // "Hello, I'm Alice"
// newの動作を理解する
// 1. 新しいオブジェクトを作る: {}
// 2. Object.setPrototypeOf(obj, Person.prototype) でプロトタイプを設定
// 3. this = objとして関数を実行
// 4. thisを返す(明示的にオブジェクトをreturnしない限り)
9-3. クラス構文(ES2015+)
class Animal {
// クラスフィールド(ES2022+)
#name // プライベートフィールド(#で始まる)
species // パブリックフィールド
static count = 0 // 静的フィールド
constructor(name, species) {
this.#name = name
this.species = species
Animal.count++
}
// ゲッター
get name() {
return this.#name
}
// セッター
set name(value) {
if (typeof value !== 'string') throw new TypeError('名前は文字列で')
this.#name = value
}
// インスタンスメソッド
describe() {
return `${this.#name} は ${this.species} です`
}
// 静的メソッド(インスタンスなしで呼べる)
static create(name, species) {
return new Animal(name, species)
}
// プライベートメソッド
#validate() {
return this.#name.length > 0
}
}
const dog = new Animal('ポチ', '犬')
dog.describe() // 'ポチ は 犬 です'
dog.name // 'ポチ'(getterを通じて)
dog.name = 'タロウ' // setterを通じて
Animal.count // 1(静的フィールド)
Animal.create('ミケ', '猫') // Animalインスタンス
9-4. 継承
class Dog extends Animal {
#breed
constructor(name, breed) {
super(name, '犬') // 親クラスのconstructorを呼ぶ(必須)
this.#breed = breed
}
// オーバーライド
describe() {
return `${super.describe()}(${this.#breed}種)`
}
// 追加メソッド
bark() {
return 'ワン!'
}
}
const pochi = new Dog('ポチ', 'ゴールデンレトリバー')
pochi.describe() // 'ポチ は 犬 です(ゴールデンレトリバー種)'
pochi.bark() // 'ワン!'
pochi instanceof Dog // true
pochi instanceof Animal // true(プロトタイプチェーンで判定)
9-5. Mixinパターン(多重継承の代替)
// Mixin: 複数のクラスから機能を組み合わせる
const Serializable = (Base) => class extends Base {
serialize() {
return JSON.stringify(this)
}
static deserialize(json) {
return Object.assign(new this(), JSON.parse(json))
}
}
const Loggable = (Base) => class extends Base {
log() {
console.log(`[${new Date().toISOString()}]`, this)
}
}
class User extends Serializable(Loggable(Animal)) {
constructor(name) {
super(name, '人間')
}
}
const user = new User('Alice')
user.serialize() // JSON文字列
user.log() // タイムスタンム付きログ
9-6. クラスvsプロトタイプの対応関係
「クラス構文はプロトタイプの糖衣構文」であることを確認する比較です。
// ES5プロトタイプスタイル
function PersonES5(name) {
this.name = name // インスタンスプロパティ
}
PersonES5.prototype.greet = function() {
return `Hello, I'm ${this.name}`
}
// ES2015+ クラス構文(上と完全に同等)
class PersonClass {
constructor(name) {
this.name = name
}
greet() {
return `Hello, I'm ${this.name}`
}
}
// 確認: クラスの実体はコンストラクタ関数
typeof PersonClass // 'function'
// 確認: メソッドはprototypeに定義される
PersonClass.prototype.greet // Function
Object.getPrototypeOf(new PersonClass()) === PersonClass.prototype // true
// ❌ クラスはホイスティングされない(関数宣言と異なる点)
// const p = new Person() // ReferenceError
// class Person {}
// staticメソッドとプロパティの実体
class Counter {
static #count = 0 // 静的プライベートフィールド
#id // インスタンスプライベートフィールド
constructor() {
Counter.#count++
this.#id = Counter.#count
}
static getCount() { return Counter.#count }
getId() { return this.#id }
}
const c1 = new Counter()
const c2 = new Counter()
Counter.getCount() // 2(静的メソッドはインスタンスなしで呼べる)
c1.getId() // 1
c2.getId() // 2
// 確認: staticはConstructorFunctionのプロパティになる
Counter.getCount // Function(Counterオブジェクトのプロパティ)
// c1.getCount // undefined(インスタンスにはない)
9-7. このセクションのまとめ
プロトタイプチェーン:
すべてのオブジェクトはプロトタイプを持つ
プロパティ検索: 自身 → prototype → prototype.prototype → null
Array.prototypeにmap/filter等のメソッドが定義されている
クラスはプロトタイプの糖衣構文:
class構文 → 内部ではプロトタイプベース
constructor → インスタンスプロパティの初期化
メソッド → prototypeに定義される(全インスタンスで共有)
クラスの主要機能:
#privateField → クラス外からアクセス不可(ES2022+)
static → インスタンスなしで呼べるメソッド・プロパティ
get/set → ゲッター・セッターでアクセス制御
extends / super → 継承(superで親クラスのコンストラクタを呼ぶ)
Mixinパターン:
(Base) => class extends Base { ... }
多重継承の代替(JavaScriptはsingleの継承のみ)
10. 非同期処理:コールバック→Promise→async/await
このセクションでは非同期処理の進化(コールバック → Promise → async/await)を歴史的経緯とともに解説します。Promiseの内部状態、マイクロタスクとマクロタスクの違い、AbortControllerによるキャンセルまで踏み込み、非同期処理を深く理解します。
10-1. なぜ非同期が必要か
// JavaScriptはシングルスレッド
// 重い処理をメインスレッドでやるとUIがブロックされる
// ❌ 同期でネットワーク待機(実際にはできないが概念として)
const data = networkRequest() // ここで数秒フリーズ
processData(data)
// ✅ 非同期でネットワーク待機
networkRequest(function(data) {
processData(data) // データが届いたら実行
})
doOtherWork() // networkRequestを待たずに実行
10-2. コールバック地獄(Callback Hell)
// 複数の非同期処理を順番に実行しようとすると...
getUser(userId, function(user) {
getProfile(user.id, function(profile) {
getPermissions(profile.role, function(permissions) {
updateDisplay(user, profile, permissions, function(result) {
// どんどんネストが深くなる(コールバック地獄)
if (result.error) {
handleError(result.error) // エラー処理も複雑
}
})
})
})
})
10-3. Promiseの3つの状態と内部動作
Promiseは「将来の値を表すオブジェクト」です。ES2015で導入され、コールバック地獄を解決しました。
Promiseの3状態図
┌─────────────────────────────────────────────────────────────┐
│ Promiseの状態遷移 │
├─────────────────────────────────────────────────────────────┤
│ │
│ resolve(value) │
│ ┌──────────────────────→ fulfilled(成功) │
│ │ ↓ .then(handler) が呼ばれる │
│ pending(保留中) │
│ (初期状態) │
│ │ reject(error) │
│ └──────────────────────→ rejected(失敗) │
│ ↓ .catch(handler) が呼ばれる │
│ │
│ 重要: 一度fulfilled/rejectedになると状態は変わらない │
│ │
└─────────────────────────────────────────────────────────────┘
▲ Promiseの状態機械:pendingからresolve() でfulfilled、reject() またはthrowでrejectedへ遷移する。一度確定すると不変(Immutable)。.then()・.catch()・.finally() でチェーンできる。
// Promiseの作成と状態遷移
function fetchUser(id) {
return new Promise((resolve, reject) => {
// この関数は同期的に実行される(コンストラクタの引数)
// pending状態でスタート
setTimeout(() => {
if (id > 0) {
resolve({ id, name: 'Alice' }) // → fulfilledに遷移
} else {
reject(new Error('IDが無効です')) // → rejectedに遷移
}
}, 100)
})
}
// Promiseの消費(.then/.catch/.finally)
fetchUser(1)
.then(user => {
console.log(user) // { id: 1, name: 'Alice' }
return user.name // 次の .thenに渡せる(Promiseチェーン)
})
.then(name => {
console.log(name) // 'Alice'
// returnしなければ次のthenにはundefinedが渡る
})
.catch(error => {
// .thenの中でもエラーが発生すれば .catchに流れる
console.error(error)
})
.finally(() => {
// 成否に関わらず必ず実行(ローディング終了など)
console.log('完了')
})
// Promise.resolve / Promise.reject: 即座に解決/拒否されたPromiseを作る
Promise.resolve('成功値') // fulfilledなPromise
Promise.reject(new Error('失敗')) // rejectedなPromise
// Promise.withResolvers(ES2024+): resolve/rejectを外部から呼べる
const { promise, resolve, reject } = Promise.withResolvers()
setTimeout(() => resolve('done'), 1000)
await promise // 'done'
10-4. Promiseの直列・並列実行
// 直列(前の結果を使って次を実行)
fetchUser(1)
.then(user => fetchProfile(user.id))
.then(profile => fetchPermissions(profile.role))
.then(permissions => updateDisplay(permissions))
.catch(handleError)
// Promise.all(全て完了するまで待つ。一つでも失敗したら失敗)
Promise.all([
fetchUser(1),
fetchProfile(1),
fetchPermissions('admin')
]).then(([user, profile, permissions]) => {
// 全部揃った
})
// Promise.allSettled(全て完了を待つ。成否は問わない)
Promise.allSettled([
fetchUser(1),
fetchUser(-1), // これは失敗
]).then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('成功:', result.value)
} else {
console.log('失敗:', result.reason)
}
})
})
// Promise.race(最初に完了したものの結果を使う)
Promise.race([
fetchWithTimeout(url, 5000),
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000))
])
// Promise.any(最初に成功したものを使う。全失敗ならAggregateError)
Promise.any([
fetchFromServer1(),
fetchFromServer2(),
fetchFromServer3(),
]).then(result => {
// 最初に成功したサーバーの結果
})
10-5. async/awaitの内部動作
async/awaitはPromiseをより直感的に書ける構文糖衣(Syntactic Sugar)です。内部ではPromiseを使っています。
// async関数は常にPromiseを返す
async function getUser(id) {
// awaitはPromiseが解決するまで待つ
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`)
}
const user = await response.json()
return user // Promiseでラップされて返される
}
// 直列処理
async function loadUserData(userId) {
try {
const user = await getUser(userId)
const profile = await getProfile(user.id)
const permissions = await getPermissions(profile.role)
return { user, profile, permissions }
} catch (error) {
console.error('データ取得失敗:', error)
throw error // 再throwして呼び出し元に伝える
}
}
// 並列処理(awaitをPromise.allと組み合わせる)
async function loadAll(userId) {
const [user, stats] = await Promise.all([
getUser(userId),
getStats(userId)
])
return { user, stats }
}
10-6. マイクロタスクvsマクロタスク(非同期の実行順序)
Promiseのコールバックがいつ実行されるかを理解するにはマイクロタスクとマクロタスクの違いが重要です。
// Promise.thenはマイクロタスク(setTimeoutより優先)
console.log('1: 同期開始')
setTimeout(() => console.log('5: setTimeout(Macrotask)'), 0)
Promise.resolve()
.then(() => {
console.log('3: Promise.then(Microtask)')
// thenの中で新しいPromiseを作っても、全てのマイクロタスクが終わるまで...
return Promise.resolve()
})
.then(() => console.log('4: Promise.then2(Microtask)'))
console.log('2: 同期終了')
// 出力順:
// 1: 同期開始
// 2: 同期終了
// 3: Promise.then(Microtask)
// 4: Promise.then2(Microtask)
// 5: setTimeout(Macrotask)
マイクロタスクvsマクロタスクのルール:
同期コードが終わる
↓
全てのマイクロタスクを実行(Promise.then, queueMicrotaskなど)
↓ ← マイクロタスクが追加されても全部終わるまで続ける
マクロタスクを1つ実行(setTimeout, setInterval, I/Oなど)
↓
また全マイクロタスクを実行
↓
マクロタスクを1つ実行
... 繰り返す
▲ async/await実行タイムライン:awaitにより関数が中断しコールスタックを解放する。解決後はマイクロタスクキューに追加され、setTimeout(マクロタスク)より先に再開される。
// 実践的な影響: async/awaitの実行タイミング
async function fetchAndProcess() {
const data = await fetch('/api/data') // ← ここで中断し、レスポンスを待つ
// awaitの後はマイクロタスクとして再開する
return await data.json()
}
// async/awaitを使った場合の順序
async function demo() {
console.log('A')
await Promise.resolve() // 次のマイクロタスクまで待つ
console.log('B') // マイクロタスクとして実行
}
demo()
console.log('C')
// 出力: A → C → B
// 理由: awaitで中断 → 同期コード(C)実行 → マイクロタスク(B)実行
10-7. AbortControllerによるキャンセル
非同期処理をキャンセルしたい場合(画面遷移時、重複リクエスト防止など)に AbortController を使います。
// AbortController: fetchのキャンセルに使う
const controller = new AbortController()
const signal = controller.signal
// fetchにsignalを渡す
fetch('/api/large-data', { signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('リクエストがキャンセルされました')
} else {
throw err // 他のエラーは再スロー
}
})
// 5秒後にキャンセル
setTimeout(() => controller.abort(), 5000)
// または即座にキャンセル
// controller.abort()
// async/awaitでのキャンセル
async function fetchWithCancel(url) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000) // 5秒タイムアウト
try {
const response = await fetch(url, { signal: controller.signal })
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return await response.json()
} catch (err) {
if (err.name === 'AbortError') {
throw new Error('リクエストがタイムアウトしました')
}
throw err
} finally {
clearTimeout(timeoutId) // クリーンアップ
}
}
// ReactでのuseEffectでの活用(画面遷移時のキャンセル)
useEffect(() => {
const controller = new AbortController()
async function loadData() {
try {
const data = await fetch('/api/data', { signal: controller.signal })
const json = await data.json()
setData(json)
} catch (err) {
if (err.name !== 'AbortError') {
setError(err)
}
}
}
loadData()
return () => controller.abort() // コンポーネントがアンマウントされたらキャンセル
}, [])
10-8. エラーハンドリングパターン
// パターン1: try/catch
async function safeFetch(url) {
try {
const res = await fetch(url)
return await res.json()
} catch (err) {
console.error('fetch失敗:', err)
return null // フォールバック値
}
}
// パターン2: Result型パターン
async function fetchWithResult(url) {
try {
const data = await (await fetch(url)).json()
return { ok: true, data }
} catch (error) {
return { ok: false, error }
}
}
const result = await fetchWithResult('/api/data')
if (result.ok) {
process(result.data)
} else {
handleError(result.error)
}
// パターン3: 再試行
async function fetchWithRetry(url, maxRetries = 3) {
let lastError
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fetch(url).then(r => r.json())
} catch (err) {
lastError = err
await new Promise(r => setTimeout(r, 2 ** attempt * 100)) // 指数バックオフ
}
}
throw lastError
}
10-9. よくある落とし穴
// ❌ awaitを忘れる
async function bad() {
const result = fetchUser(1) // Promiseが返るがawaitしていない
console.log(result.name) // undefined(Promiseオブジェクトにnameはない)
}
// ❌ forEachでawaitが効かない
async function bad() {
const ids = [1, 2, 3]
ids.forEach(async (id) => {
const user = await fetchUser(id) // これは並列で実行され、完了を待てない
console.log(user)
})
// forEachは内部のasync関数のPromiseを待たない
}
// ✅ for...ofを使う
async function good() {
const ids = [1, 2, 3]
for (const id of ids) {
const user = await fetchUser(id) // 直列で確実に待てる
console.log(user)
}
}
// ✅ またはPromise.allで並列
async function goodParallel() {
const ids = [1, 2, 3]
const users = await Promise.all(ids.map(id => fetchUser(id)))
users.forEach(user => console.log(user))
}
10-10. このセクションのまとめ
非同期処理の進化:
コールバック → ネストが深くなる(コールバック地獄)
Promise → チェーン構文で可読性向上
async/await → 同期的に見える書き方(内部はPromise)
Promiseの3状態:
pending → 保留中(初期状態)
fulfilled → 成功(resolve呼び出し後)
rejected → 失敗(reject呼び出し後)
※ 一度遷移した状態は変わらない
Promiseの並列実行:
Promise.all() → 全て成功するまで待つ(1つでも失敗したら失敗)
Promise.allSettled() → 全て完了するまで待つ(成否は問わない)
Promise.race() → 最初に完了したものの結果
Promise.any() → 最初に成功したものの結果
マイクロタスクvsマクロタスク:
マイクロタスク(Promise.then)はマクロタスク(setTimeout)より優先
同期コード → 全マイクロタスク → 1マクロタスク → 全マイクロタスク...
AbortController:
fetchのキャンセルに使う
タイムアウト実装、画面遷移時のリクエストキャンセルに活用
よくある落とし穴:
awaitの書き忘れ
forEach内のawait(完了を待てない → for...ofを使う)
catchしないPromise(UnhandledPromiseRejection)
11. イテレータ・ジェネレータ・Symbol
このセクションではJavaScriptの高度な反復処理機構であるイテレータ・ジェネレータ・Symbolを解説します。ジェネレータは「一時停止できる関数」という独特の概念で、ページネーションや無限シーケンスの実装に使われます。
11-1. イテレータプロトコル
なぜイテレータプロトコルが存在するか
ES2015以前、JavaScriptの反復処理はバラバラでした。配列には forEach、NodeListには for...in、文字列には添字アクセス…統一されたプロトコルがなかったためライブラリ間での相互運用が困難でした。ES2015でイテレータプロトコルが導入され、for…of / スプレッド / 分割代入 が一つのプロトコルで統一されました。
イテレータプロトコルの構造:
イテラブル(iterable)
└── [Symbol.iterator]() メソッドを持つオブジェクト
↓ 呼び出すと
イテレータ(iterator)
└── next() メソッドを持つオブジェクト
↓ 呼び出すと
IteratorResult
└── { value: any, done: boolean }
done: false → 次の値がある
done: true → 終了(valueは通常undefined)
for...ofの内部動作:
1. [Symbol.iterator]() を呼んでイテレータを取得
2. next() を繰り返す
3. done === trueになったら終了
// イテラブル: Symbol.iteratorメソッドを持つオブジェクト
// イテレータ: nextメソッドを持ち、{value, done}を返すオブジェクト
// 自作イテラブル
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from
const last = this.to
return {
next() {
if (current <= last) {
return { value: current++, done: false }
}
return { value: undefined, done: true }
}
}
}
}
for (const n of range) {
console.log(n) // 1 2 3 4 5
}
[...range] // [1, 2, 3, 4, 5]
// 組み込みイテラブルの例
// Array, String, Map, Set, NodeList, arguments, TypedArray
'hello'[Symbol.iterator] // 文字列もイテラブル
for (const ch of 'hello') console.log(ch) // h e l l o
// Mapのイテレーション
const map = new Map([['a', 1], ['b', 2]])
for (const [key, value] of map) {
console.log(key, value) // a 1, b 2
}
// 手動でイテレータを使う場合
const iter = [1, 2, 3][Symbol.iterator]()
iter.next() // { value: 1, done: false }
iter.next() // { value: 2, done: false }
iter.next() // { value: 3, done: false }
iter.next() // { value: undefined, done: true }
11-2. ジェネレータ
ジェネレータは「一時停止・再開できる関数」です。
なぜジェネレータが必要か
大量データの処理を配列で行うと、全データをメモリに乗せる必要があります。ジェネレータは「必要なときに一つずつ生成」するため、無限シーケンスや大量データを省メモリで扱えます。
通常の関数vsジェネレータ:
通常の関数:
呼び出し → 処理 → 戻り値 → 終了(再開不可)
ジェネレータ:
呼び出し → ジェネレータオブジェクトを返す(まだ実行しない)
next() → yieldまで実行 → 一時停止
next() → 次のyieldまで実行 → 一時停止
next() → 最後まで実行 → { value: undefined, done: true }
// function* でジェネレータ関数を定義
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i // yieldで値を返して一時停止
}
}
const gen = range(1, 5)
gen.next() // { value: 1, done: false }
gen.next() // { value: 2, done: false }
// ...
gen.next() // { value: 5, done: false }
gen.next() // { value: undefined, done: true }
// for...ofで使える
for (const n of range(1, 5)) {
console.log(n)
}
// 無限シーケンス(必要な分だけ取り出す)
function* fibonacci() {
let [a, b] = [0, 1]
while (true) {
yield a
;[a, b] = [b, a + b]
}
}
function take(n, iter) {
const result = []
for (const val of iter) {
result.push(val)
if (result.length === n) break
}
return result
}
take(8, fibonacci()) // [0, 1, 1, 2, 3, 5, 8, 13]
// yield* で別のイテラブルに委譲
function* chain(...iterables) {
for (const iterable of iterables) {
yield* iterable // yield* でイテラブルの要素を全部yieldd
}
}
[...chain([1, 2], [3, 4], [5])] // [1, 2, 3, 4, 5]
// ジェネレータへの値の注入(next(value))
function* accumulator() {
let total = 0
while (true) {
const value = yield total // yieldの戻り値がnext()の引数
if (value === null) break
total += value
}
}
const acc = accumulator()
acc.next() // { value: 0, done: false } - 初期化
acc.next(10) // { value: 10, done: false } - total += 10
acc.next(20) // { value: 30, done: false } - total += 20
acc.next(null) // { value: undefined, done: true }
11-3. 非同期ジェネレータ
// 非同期ジェネレータ: async function*
// yieldをawaitと組み合わせて使う
async function* paginate(url) {
let page = 1
while (true) {
const response = await fetch(`${url}?page=${page}`)
const data = await response.json()
if (data.items.length === 0) break
yield data.items // 1ページ分のデータを返して一時停止
page++
}
}
// for await...ofで使う(一ページずつ処理)
for await (const items of paginate('/api/users')) {
processItems(items)
// 次のページが必要になってはじめてfetchが実行される
}
// ストリームの処理にも使える
async function* readLines(stream) {
const reader = stream.getReader()
const decoder = new TextDecoder()
let buffer = ''
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
if (buffer.length > 0) yield buffer
break
}
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() // 最後の不完全な行を保持
for (const line of lines) {
yield line
}
}
} finally {
reader.releaseLock()
}
}
// 使い方
const response = await fetch('/api/large-file')
for await (const line of readLines(response.body)) {
console.log(line)
}
11-4. Symbol
Symbolは唯一無二の値を作ります。
なぜSymbolが必要か
オブジェクトに「隠れたプロパティ」を追加したい場合、文字列キーだと他のコードと衝突する可能性があります。特にサードパーティのオブジェクトを拡張するとき「同じキーを既に使われていた」という問題が起きていました。Symbolはこの衝突問題を解決します。
// 同じ説明でも別のSymbol
const sym1 = Symbol('id')
const sym2 = Symbol('id')
sym1 === sym2 // false(!)各Symbolは一意
// Symbolをキーにするとプロパティが「隠れる」
const ID = Symbol('id')
const user = {
[ID]: 123, // Symbolキーは通常の列挙に現れない
name: 'Alice'
}
Object.keys(user) // ['name'](IDが出ない)
user[ID] // 123(Symbolで直接アクセス)
Object.getOwnPropertySymbols(user) // [Symbol(id)]
Reflect.ownKeys(user) // ['name', Symbol(id)](全キー)
// Symbol.for: グローバルシンボルレジストリ(モジュール間で共有)
const s1 = Symbol.for('app:userId')
const s2 = Symbol.for('app:userId')
s1 === s2 // true(同じ文字列なら同じSymbol)
Symbol.keyFor(s1) // 'app:userId'(キーを逆引き)
// 組み込みSymbol(Well-known Symbols)
// Symbol.iterator: イテラブルを定義
class Range {
constructor(start, end) {
this.start = start
this.end = end
}
[Symbol.iterator]() {
let current = this.start
const end = this.end
return {
next() {
return current <= end
? { value: current++, done: false }
: { value: undefined, done: true }
}
}
}
}
[...new Range(1, 3)] // [1, 2, 3]
// Symbol.toPrimitive: 型変換をカスタマイズ
class Money {
constructor(amount, currency) {
this.amount = amount
this.currency = currency
}
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.amount
if (hint === 'string') return `${this.amount}${this.currency}`
return this.amount // 'default'
}
}
const price = new Money(100, '円')
+price // 100(numberヒント)
`${price}` // '100円'(stringヒント)
price + 50 // 150(defaultヒント)
// Symbol.hasInstance: instanceofの挙動をカスタマイズ
class EvenNumber {
static [Symbol.hasInstance](num) {
return Number.isInteger(num) && num % 2 === 0
}
}
2 instanceof EvenNumber // true
3 instanceof EvenNumber // false
Symbolの主な用途まとめ
| 用途 | 説明 |
|---|---|
| 衝突しない定数 | const ACTION_TYPE = Symbol('action') |
| オブジェクトの隠れたメタデータ | weakMap の代替として |
| プロトコルのカスタマイズ | Symbol.iterator, Symbol.toPrimitive |
| モジュール間の共有シンボル | Symbol.for('app:key') |
11-5. このセクションのまとめ
イテレータプロトコル:
イテラブル: Symbol.iteratorメソッドを持つオブジェクト
イテレータ: next() を持ち { value, done } を返すオブジェクト
for...of / スプレッド / 分割代入 で使える
ジェネレータ(function*):
yieldで値を返して一時停止
next() で再開
無限シーケンスのメモリ効率的な実装に使える
for...ofで消費できる
非同期ジェネレータ(async function*):
yieldで非同期値を返す
for await...ofで消費する
ページネーション・ストリーム処理に有効
Symbol:
一意の値(同じ説明でも別のSymbol)
プロパティキーとして使うと「隠れたプロパティ」になる
Symbol.for() でグローバルレジストリから共有
組み込みSymbol: Symbol.iterator, Symbol.toPrimitive等
12. モジュールシステム(ESM / CJS)
このセクションではJavaScriptの2つのモジュールシステム(ESMとCommonJS)の違い、使い分け、相互運用性を解説します。Node.jsプロジェクトでよくある「import/requireどちらを使うか」の答えも明確にします。
12-1. ESM(ECMAScript Modules)- 現代の標準
// 名前付きエクスポート
// math.js
export function add(a, b) { return a + b }
export function subtract(a, b) { return a - b }
export const PI = 3.14159
// デフォルトエクスポート(1ファイルに1つ)
export default class Calculator {
add(a, b) { return a + b }
}
// 名前付きインポート
import { add, subtract, PI } from './math.js'
import { add as sum } from './math.js' // リネーム
// デフォルトインポート(名前は自由)
import Calculator from './math.js'
import Calc from './math.js' // 別の名前でも可
// 全てインポート
import * as math from './math.js'
math.add(1, 2)
math.PI
// 動的インポート(条件付き・遅延読み込み)
const module = await import('./heavy-module.js')
if (condition) {
const { feature } = await import('./feature.js')
}
12-2. CommonJS(Node.jsの旧標準)
// exports/module.exportsでエクスポート
// math.cjs
function add(a, b) { return a + b }
const PI = 3.14159
module.exports = { add, PI }
// または個別に
exports.subtract = function(a, b) { return a - b }
// requireでインポート
const { add, PI } = require('./math.cjs')
const math = require('./math.cjs')
// requireはファイルパス(相対)またはnode_modules
const express = require('express')
12-3. ESM vs CJSの違い
なぜ2つのモジュールシステムが存在するか
Node.jsは2009年にCommonJS(CJS)を採用しました。当時ブラウザにはモジュールシステムがなく、Node.js独自の実装として require() が生まれました。その後ES2015でECMAScript標準としてESMが策定され、ブラウザとNode.jsの両方で使える統一されたモジュールシステムが誕生しました。現在は2つが並存しています。
| 項目 | ESM | CJS |
|---|---|---|
| 構文 | import/export |
require/module.exports |
| 評価タイミング | 静的解析(構造が決まっている) | 実行時(動的) |
| Top-level await | 可能 | 不可能 |
this |
undefined(strict mode) |
module.exports |
| ファイル拡張子 | .mjs or "type":"module" |
.cjs orデフォルト |
| Tree-shaking | 可能(静的解析で未使用コードを除去) | 困難(動的なので判断できない) |
| 循環依存 | ライブバインディング(解決できる) | 値コピー(解決が難しい) |
| キャッシュ | URLベース(クエリで無効化可) | ファイルパスベース |
// ESMの重要な特徴1: 静的解析でTree-shakingが効く
// ❌ CJS: バンドラーは何がエクスポートされるか実行時まで分からない
const utils = require('./utils') // 全部読み込む
// ✅ ESM: バンドラーは静的に解析して不要なものを除去
import { add } from './utils.js' // addだけ使うとsubtractはバンドルされない
// ESMの重要な特徴2: ライブバインディング
// math.js
export let count = 0
export function increment() { count++ }
// main.js
import { count, increment } from './math.js'
console.log(count) // 0
increment()
console.log(count) // 1(ライブバインディング!ESMは参照を共有する)
// CJSでは値コピーなので変わらない
const { count, increment } = require('./math.js')
increment()
console.log(count) // 0(値がコピーされているので変わらない)
12-4. 動的インポートとコード分割
// import() はPromiseを返す動的インポート
// 条件付きロード
async function loadFeature(featureName) {
if (featureName === 'chart') {
const { Chart } = await import('./chart.js')
return new Chart()
}
if (featureName === 'editor') {
const { Editor } = await import('./editor.js')
return new Editor()
}
}
// ルートベースのコード分割(バンドラーが最適化)
const routes = {
'/home': () => import('./pages/Home.js'),
'/profile': () => import('./pages/Profile.js'),
'/settings': () => import('./pages/Settings.js'),
}
async function navigate(path) {
const loadPage = routes[path]
if (!loadPage) return
const { default: Page } = await loadPage()
renderPage(new Page())
}
// インポートメタ情報
console.log(import.meta.url) // 現在のモジュールのURL
console.log(import.meta.env) // Viteでの環境変数
12-5. package.jsonのモジュール設定
{
"name": "my-package",
"type": "module", // このパッケージ全体をESMとして扱う
"main": "./dist/index.cjs", // CJS用エントリポイント
"module": "./dist/index.js", // ESM用エントリポイント(バンドラー向け)
"exports": {
".": {
"import": "./dist/index.js", // ESMでimportした場合
"require": "./dist/index.cjs", // CJSでrequireした場合
"types": "./dist/index.d.ts" // TypeScript用型定義
},
"./utils": {
"import": "./dist/utils.js",
"require": "./dist/utils.cjs"
}
}
}
// ESMからCJSを使う(可能)
import cjsModule from './legacy.cjs' // デフォルトエクスポートとして読み込む
// CJSからESMを使う(制限あり)
// ❌ require('./esm-module.mjs') // エラー!
// ✅ 動的importを使う
async function main() {
const { default: esmModule } = await import('./esm-module.mjs')
}
12-6. このセクションのまとめ
ESM vs CJSの選択:
新規プロジェクト → ESM(import/export)を使う
既存のNode.jsコード → CJS(require)が多い
混在する場合 → package.jsonの "type": "module" でESMに統一
ESMの特徴:
静的解析可能(バンドラーのTree-shakingが効く)
Top-level awaitが使える
ブラウザネイティブで動く(<script type="module">)
CJSの特徴:
動的requireが可能(条件付きインポートができる)
Node.jsの旧ライブラリと互換
実行時に評価される
実用上の注意:
ESMファイルからCJSをインポート可(逆は制限あり)
.mjs / .cjs拡張子で明示的に区別できる
package.jsonの "exports" フィールドで公開APIを制御
13. エラーハンドリング
このセクションではJavaScriptのエラーハンドリングを体系化します。組み込みエラー型の使い方、カスタムエラーの設計、非同期エラーの扱いを解説します。
13-1. Errorの種類
// 組み込みエラー型
new Error('汎用エラー')
new TypeError('型が違う') // 型エラー
new RangeError('範囲外') // 範囲エラー
new ReferenceError('未定義の変数') // 参照エラー
new SyntaxError('構文エラー') // 構文エラー
new URIError('不正なURI') // URIエラー
// カスタムエラー
class AppError extends Error {
constructor(message, code, statusCode = 500) {
super(message)
this.name = 'AppError'
this.code = code
this.statusCode = statusCode
// スタックトレースを正しく維持
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AppError)
}
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} (id: ${id}) が見つかりません`, 'NOT_FOUND', 404)
this.name = 'NotFoundError'
}
}
// 使い方
throw new NotFoundError('User', 123)
13-2. try / catch / finally
async function processRequest(id) {
try {
const user = await fetchUser(id)
const result = await processUser(user)
return result
} catch (error) {
// エラーの種類で分岐
if (error instanceof NotFoundError) {
return { error: 'ユーザーが見つかりません', status: 404 }
}
if (error instanceof TypeError) {
return { error: '入力形式が正しくありません', status: 400 }
}
// 想定外のエラーは再throw
throw error
} finally {
// 成否に関わらず実行(クリーンアップ)
closeConnection()
}
}
13-3. Errorオブジェクトの重要なプロパティ
try {
null.property // TypeError
} catch (error) {
error.name // 'TypeError'
error.message // "Cannot read properties of null"
error.stack // スタックトレース文字列
error.cause // 元のエラー(error chaining, ES2022+)
}
// Error Cause(ES2022+)
try {
await fetchData()
} catch (originalError) {
throw new AppError('データ取得失敗', 'FETCH_ERROR', {
cause: originalError // 元のエラーを保持
})
}
13-4. エラーの全体設計パターン
// Result型パターン(エラーを値として扱う)
// TypeScriptでよく使われるが、JSでも応用可
function divide(a, b) {
if (b === 0) {
return { ok: false, error: new RangeError('0除算は不可') }
}
return { ok: true, value: a / b }
}
const result = divide(10, 0)
if (!result.ok) {
console.error(result.error.message)
} else {
console.log(result.value)
}
// グローバルエラーハンドラー(ブラウザ)
window.addEventListener('error', (event) => {
// 捕捉されなかったエラー
console.error('Uncaught error:', event.error)
reportToSentry(event.error) // エラー監視サービスへ送信
})
window.addEventListener('unhandledrejection', (event) => {
// 捕捉されなかったPromiseの拒否
console.error('Unhandled rejection:', event.reason)
event.preventDefault() // デフォルトのコンソール出力を抑制
reportToSentry(event.reason)
})
// グローバルエラーハンドラー(Node.js)
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error)
process.exit(1) // 回復不可能なエラーとして終了
})
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason)
})
// エラーバウンダリパターン(再利用可能なエラー処理ラッパー)
async function withErrorHandling(fn, options = {}) {
const {
onError = console.error,
fallback = null,
retries = 0
} = options
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await fn()
} catch (error) {
if (attempt < retries) {
console.warn(`試行 ${attempt + 1} 失敗、リトライ中...`)
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)))
continue
}
onError(error)
return fallback
}
}
}
// 使い方
const user = await withErrorHandling(
() => fetchUser(userId),
{
retries: 3,
fallback: null,
onError: (err) => reportError(err)
}
)
13-5. このセクションのまとめ
エラーの種類:
Error / TypeError / RangeError / ReferenceError / SyntaxError
カスタムエラー: class MyError extends Errorを作る
Error Cause (ES2022+): { cause: originalError } でエラーチェーン
try/catchの使い方:
catch (error) { ... } → エラーをキャッチ
finally { ... } → 成否に関わらず実行(クリーンアップ)
instanceofで型を絞り込む → 種別ごとの処理分け
Errorオブジェクトのプロパティ:
name → エラー型名('TypeError'等)
message → エラーメッセージ
stack → スタックトレース
cause → 元のエラー(ES2022+, error chaining)
非同期エラーのベストプラクティス:
async関数内ではtry/catchを使う
catchしないPromiseはUnhandledPromiseRejectionになる
エラーは種別ごとに処理し、想定外は再スロー
グローバルエラーハンドラー:
ブラウザ: window.addEventListener('error') / 'unhandledrejection'
Node.js: process.on('uncaughtException') / 'unhandledRejection'
エラー監視サービス(Sentry等)に接続する
設計パターン:
Result型でエラーを値として扱う(throwを避ける)
withErrorHandlingラッパーでリトライ機能を追加
カスタムエラークラスで種別を明確にする
14. WeakMap・WeakSet・WeakRef
このセクションではJavaScriptのガベージコレクションと連携する特殊なコレクション型を解説します。通常のMap/Setと異なり、キー(または値)への参照が弱いため、オブジェクトがGCされると自動的に削除されます。
14-1. WeakMap / WeakSet(弱い参照)
// WeakMap: キーはオブジェクトのみ、弱い参照
const weakMap = new WeakMap()
let obj = { name: 'Alice' }
weakMap.set(obj, 'some metadata')
weakMap.get(obj) // 'some metadata'
// objへの参照がなくなるとGCされる(WeakMapは参照を保持しない)
obj = null
// GC後、weakMapからも削除される
// 用途: オブジェクトに関連データを付与(GCを妨げない)
const cache = new WeakMap()
function getDisplayName(user) {
if (cache.has(user)) return cache.get(user)
const name = `${user.firstName} ${user.lastName}`
cache.set(user, name)
return name
}
// WeakSet: 値はオブジェクトのみ
const seen = new WeakSet()
function processsOnce(obj) {
if (seen.has(obj)) return
seen.add(obj)
process(obj)
}
14-2. WeakRef(ES2021+)
// WeakRef: オブジェクトへの弱い参照(GCを妨げない)
class ExpensiveCache {
#cache = new Map()
set(key, value) {
this.#cache.set(key, new WeakRef(value))
}
get(key) {
const ref = this.#cache.get(key)
if (!ref) return undefined
const value = ref.deref() // GCされていればundefined
if (value === undefined) {
this.#cache.delete(key) // 既にGC済みなのでクリーンアップ
}
return value
}
}
14-3. FinalizationRegistry(ES2021+)
// FinalizationRegistry: GCされたオブジェクトのクリーンアップコールバック
// 用途: キャッシュのクリーンアップ、ネイティブリソースの解放
const registry = new FinalizationRegistry((heldValue) => {
// GC後に呼ばれるコールバック(タイミングは保証されない)
console.log(`クリーンアップ: ${heldValue}`)
cleanup(heldValue)
})
class ResourceHandle {
#resource
constructor(resource) {
this.#resource = resource
// このインスタンスがGCされたとき、resourceIDでコールバック
registry.register(this, resource.id, this)
}
dispose() {
// 明示的な解放(GCを待たない)
registry.unregister(this) // コールバックをキャンセル
freeResource(this.#resource)
}
}
// WeakMapとFinalizationRegistryの組み合わせ(高度なキャッシュ)
class AutoCleanupCache {
#cache = new Map() // id → WeakRef<value> のマップ
#registry = new FinalizationRegistry((id) => {
this.#cache.delete(id) // GC後にキャッシュエントリを削除
})
set(id, value) {
this.#cache.set(id, new WeakRef(value))
this.#registry.register(value, id)
}
get(id) {
const ref = this.#cache.get(id)
return ref?.deref() // GCされていればundefined
}
}
14-4. このセクションのまとめ
WeakMap:
キーはオブジェクトのみ(プリミティブ不可)
キーへの参照が弱い → キーがGCされると自動削除
イテラブルでない(size, forEachなし)
用途: オブジェクトにメタデータを付与(GCを妨げない)
WeakSet:
値はオブジェクトのみ
オブジェクトの「処理済み」フラグ管理に使える
WeakRef(ES2021+):
オブジェクトへの弱い参照
deref() で現在の値を取得(GCされていればundefined)
キャッシュの実装に使える(GCに任せる)
FinalizationRegistry(ES2021+):
GCされたオブジェクトのクリーンアップコールバック
unregister() で明示的にキャンセル可能
GCのタイミングは保証されない
通常のMap/Setとの違い:
通常のMap → GCを妨げる(キーがある限りオブジェクトは生き続ける)
WeakMap → GCを妨げない(キーがなくなれば自動削除)
いつWeakMapを使うか:
DOM要素にデータを関連付けたい(要素が削除されたら自動でデータも消える)
プライベートデータをクラスに付与したい(クラスフィールド # の代替)
キャッシュでメモリリークを防ぎたい
15. Proxy・Reflect・メタプログラミング
このセクションではJavaScriptのメタプログラミング機能であるProxyとReflectを解説します。Proxyを使うとオブジェクトへのあらゆる操作(プロパティアクセス・代入・関数呼び出し等)をインターセプトでき、Vue.jsなどのリアクティブシステムの基盤になっています。
15-1. Proxy
Proxyはオブジェクトの基本操作をインターセプトできます。
// バリデーション付きオブジェクト
function createValidatedUser(data) {
return new Proxy(data, {
set(target, prop, value) {
if (prop === 'age' && (typeof value !== 'number' || value < 0)) {
throw new TypeError('ageは0以上の数値で')
}
if (prop === 'email' && !value.includes('@')) {
throw new TypeError('emailの形式が正しくない')
}
target[prop] = value
return true // trueを返さないとTypeError
},
get(target, prop) {
if (!(prop in target)) {
throw new ReferenceError(`プロパティ ${String(prop)} は存在しません`)
}
return target[prop]
}
})
}
const user = createValidatedUser({ name: 'Alice', age: 30, email: 'alice@example.com' })
user.age = -1 // TypeError
user.email = 'invalid' // TypeError
user.unknown // ReferenceError
// 観察可能なオブジェクト(リアクティブシステムの基礎)
function observable(obj, onChange) {
return new Proxy(obj, {
set(target, prop, value) {
const old = target[prop]
target[prop] = value
if (old !== value) {
onChange(prop, old, value)
}
return true
}
})
}
const state = observable({ count: 0 }, (key, old, next) => {
console.log(`${key}: ${old} → ${next}`)
})
state.count = 1 // "count: 0 → 1"
state.count = 2 // "count: 1 → 2"
15-2. Reflect
ReflectはProxyと対になる組み込みオブジェクトです。
// Proxy内でデフォルト動作を呼ぶのに使う
const handler = {
get(target, prop, receiver) {
console.log(`プロパティ ${String(prop)} にアクセス`)
return Reflect.get(target, prop, receiver) // デフォルトのget動作
},
set(target, prop, value, receiver) {
console.log(`プロパティ ${String(prop)} を ${value} に設定`)
return Reflect.set(target, prop, value, receiver) // デフォルトのset動作
}
}
// Reflectのメソッド一覧
// Reflect.get(target, prop, receiver) → target[prop]
// Reflect.set(target, prop, value, receiver) → target[prop] = value
// Reflect.has(target, prop) → prop in target
// Reflect.deleteProperty(target, prop) → delete target[prop]
// Reflect.apply(fn, thisArg, args) → fn.apply(thisArg, args)
// Reflect.construct(Target, args) → new Target(...args)
// Reflect.ownKeys(target) → Object.getOwnPropertyNames + Symbols
// Reflect.defineProperty(target, prop, desc) → Object.defineProperty
// Reflect.getOwnPropertyDescriptor(target, prop)
// Reflect.getPrototypeOf(target) → Object.getPrototypeOf
// Reflect.setPrototypeOf(target, proto)
// Reflect.isExtensible(target)
// Reflect.preventExtensions(target)
// なぜReflectを使うか
// 1. Proxyのhandlerでデフォルト動作を安全に呼べる(thisバインディングが正しい)
// 2. 例外の代わりにbooleanを返すメソッドがある(より関数型)
// ❌ Object.definePropertyは失敗時に例外を投げる
try {
Object.defineProperty(obj, 'readOnly', { value: 1, writable: false })
} catch (e) { /* 失敗 */ }
// ✅ Reflect.definePropertyは成功/失敗をbooleanで返す
const success = Reflect.defineProperty(obj, 'readOnly', { value: 1, writable: false })
if (!success) { /* 失敗処理 */ }
15-3. Proxyの高度な応用
// 関数呼び出しのインターセプト(applyトラップ)
function withLogging(fn) {
return new Proxy(fn, {
apply(target, thisArg, args) {
console.log(`${target.name} 呼び出し: args=${JSON.stringify(args)}`)
const result = Reflect.apply(target, thisArg, args)
console.log(`${target.name} 戻り値: ${JSON.stringify(result)}`)
return result
}
})
}
const add = withLogging((a, b) => a + b)
add(1, 2) // ログ付きで実行
// コンストラクタのインターセプト(constructトラップ)
function withInstanceCount(Class) {
let count = 0
return new Proxy(Class, {
construct(target, args) {
count++
console.log(`${Class.name} インスタンス数: ${count}`)
return Reflect.construct(target, args)
}
})
}
const TrackedUser = withInstanceCount(User)
new TrackedUser('Alice') // "Userインスタンス数: 1"
new TrackedUser('Bob') // "Userインスタンス数: 2"
// hasトラップ(in演算子のインターセプト)
const range = new Proxy({ from: 1, to: 10 }, {
has(target, key) {
return +key >= target.from && +key <= target.to
}
})
5 in range // true
15 in range // false
// ネガティブインデックスのサポート(配列拡張)
function createNegativeArray(arr) {
return new Proxy(arr, {
get(target, prop, receiver) {
if (typeof prop === 'string') {
const index = +prop
if (index < 0) {
prop = String(target.length + index)
}
}
return Reflect.get(target, prop, receiver)
}
})
}
const arr = createNegativeArray([1, 2, 3, 4, 5])
arr[-1] // 5(末尾から1番目)
arr[-2] // 4(末尾から2番目)
15-4. このセクションのまとめ
Proxy:
new Proxy(target, handler) でオブジェクトの操作をインターセプト
handlerのトラップ:
get(target, prop) → プロパティ読み取り
set(target, prop, value) → プロパティ書き込み
has(target, prop) → in演算子
apply(target, thisArg, args) → 関数呼び出し
construct(target, args) → new演算子
用途: バリデーション、観察可能なオブジェクト、ロギング
Reflect:
Proxyのhandler内でデフォルト動作を呼ぶために使う
Reflect.get / Reflect.set / Reflect.hasなど
Vue.jsとの関係:
Vue 3のリアクティブシステムはProxyを使って実装されている
プロパティの変更を検知 → 依存するコンポーネントを自動更新
16. ブラウザAPIの基礎
このセクションではDOM操作・イベント・Fetch API・Web Storageなど、ブラウザ環境で頻繁に使うAPIを整理します。セキュリティの観点(innerHTMLの危険性等)も含めて解説します。
16-1. DOM操作
// 要素の取得
const el = document.getElementById('app')
const els = document.querySelectorAll('.item') // NodeList
const first = document.querySelector('.item') // 最初の一つ
// 要素の作成・追加
const div = document.createElement('div')
div.textContent = 'Hello'
div.className = 'greeting'
el.appendChild(div)
el.append('テキスト', div, anotherDiv) // 複数追加可
// 要素の削除
el.remove()
parent.removeChild(child)
// 属性の操作
div.setAttribute('data-id', '123')
div.getAttribute('data-id') // '123'
div.removeAttribute('data-id')
div.dataset.id // '123'(data-*属性の簡単アクセス)
// スタイルの操作
div.style.color = 'red'
div.style.backgroundColor = '#fff'
div.classList.add('active')
div.classList.remove('active')
div.classList.toggle('active')
div.classList.contains('active') // true/false
16-2. イベント
// addEventListener(推奨)
button.addEventListener('click', handleClick)
button.removeEventListener('click', handleClick) // 同じ関数参照が必要
// イベントオブジェクト
button.addEventListener('click', (event) => {
event.preventDefault() // デフォルト動作を止める
event.stopPropagation() // バブリングを止める
event.target // クリックされた要素
event.currentTarget // イベントリスナが付いた要素
event.clientX, event.clientY // クリック位置
})
// イベント委譲(パフォーマンスに良い)
document.querySelector('#list').addEventListener('click', (event) => {
const item = event.target.closest('.item')
if (!item) return
handleItemClick(item)
})
// カスタムイベント
const myEvent = new CustomEvent('user-updated', {
detail: { userId: 123 },
bubbles: true,
cancelable: true
})
element.dispatchEvent(myEvent)
// 受信
element.addEventListener('user-updated', (e) => {
console.log(e.detail.userId) // 123
})
16-3. Fetch API
// 基本
const response = await fetch('/api/users')
const users = await response.json()
// オプション付き
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })
})
// エラーハンドリング(fetchはHTTPエラーでPromiseを拒否しない)
async function safeFetch(url, options) {
const response = await fetch(url, options)
if (!response.ok) {
const error = await response.text()
throw new Error(`HTTP ${response.status}: ${error}`)
}
return response.json()
}
16-4. LocalStorage / SessionStorage
// LocalStorage: タブを閉じても永続
localStorage.setItem('token', 'abc123')
localStorage.getItem('token') // 'abc123'
localStorage.removeItem('token')
localStorage.clear()
// オブジェクトの保存(文字列のみ保存できるのでJSON化)
localStorage.setItem('user', JSON.stringify({ name: 'Alice' }))
const user = JSON.parse(localStorage.getItem('user'))
// SessionStorage: タブを閉じると消える
sessionStorage.setItem('tempData', 'value')
16-5. IntersectionObserver / MutationObserver
// IntersectionObserver: 要素がビューポートに入ったかを監視
// 用途: 遅延読み込み(Lazy Load)、無限スクロール、アニメーション開始
const observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// ビューポートに入った
const img = entry.target
img.src = img.dataset.src // 遅延読み込み
img.classList.add('visible')
observer.unobserve(img) // 一度だけ → 監視解除
}
})
},
{
root: null, // null = ビューポート
rootMargin: '100px', // 100px手前から発火(先読み)
threshold: 0.1 // 10%見えたら発火
}
)
// 全画像に遅延読み込みを適用
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img)
})
// 無限スクロール
const sentinel = document.getElementById('load-more')
const scrollObserver = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting) {
const items = await fetchNextPage()
appendItems(items)
}
})
scrollObserver.observe(sentinel)
// MutationObserver: DOMの変化を監視
// 用途: 動的コンテンツの監視、サードパーティコードとの連携
const mutationObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
console.log('追加されたノード:', node)
})
mutation.removedNodes.forEach(node => {
console.log('削除されたノード:', node)
})
}
if (mutation.type === 'attributes') {
console.log(`属性 ${mutation.attributeName} が変更`)
}
})
})
mutationObserver.observe(document.getElementById('app'), {
childList: true, // 子要素の追加・削除を監視
subtree: true, // 子孫全体を監視
attributes: true, // 属性の変化を監視
attributeFilter: ['class', 'style'] // 特定の属性のみ
})
// 監視を停止
mutationObserver.disconnect()
// ResizeObserver: 要素のサイズ変化を監視
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect
console.log(`要素サイズ: ${width}x${height}`)
// サイズに応じてレイアウトを調整
if (width < 600) {
entry.target.classList.add('compact')
} else {
entry.target.classList.remove('compact')
}
})
})
resizeObserver.observe(document.getElementById('widget'))
16-6. History API / URLSearchParams
// History API: SPAでのナビゲーション
// pushState: URLを変更してhistoryに追加(ページをリロードしない)
history.pushState(
{ userId: 123 }, // state(任意のオブジェクト)
'', // title(現在はほぼ無視される)
'/users/123' // URL(同一オリジンのみ)
)
// replaceState: 現在のhistoryエントリを置き換え
history.replaceState({ tab: 'profile' }, '', '/users/123?tab=profile')
// ブラウザの戻る/進むボタン
window.addEventListener('popstate', (event) => {
console.log('ナビゲーション:', event.state, location.pathname)
renderPage(location.pathname, event.state)
})
// URLSearchParams: クエリパラメータの操作
const url = new URL('https://example.com/search?q=hello&page=2')
const params = url.searchParams
params.get('q') // 'hello'
params.get('page') // '2'
params.has('sort') // false
params.set('page', '3')
params.append('tag', 'js')
params.delete('q')
url.toString() // 'https://example.com/search?page=3&tag=js'
// 現在のURLのクエリパラメータを操作
const currentParams = new URLSearchParams(location.search)
currentParams.set('sort', 'date')
history.pushState(null, '', `?${currentParams.toString()}`)
16-7. このセクションのまとめ
DOM操作:
document.querySelector/querySelectorAllでセレクタで要素取得
createElement + appendでDOM構築
textContentでテキスト設定(innerHTMLはXSSの危険)
dataset.* でdata-* 属性にアクセス
classListでCSSクラス管理
イベント:
addEventListener/removeEventListener(同じ関数参照が必要)
event.preventDefault() → デフォルト動作を止める
event.stopPropagation() → バブリングを止める
イベント委譲(event.target.closest())でパフォーマンス改善
Fetch API:
HTTPエラー(4xx/5xx)ではPromiseを拒否しない
→ response.okを必ずチェック
AbortControllerで中断できる(第10章参照)
LocalStorage vs SessionStorage:
LocalStorage: タブを閉じても永続
SessionStorage: タブを閉じると消える
どちらも文字列のみ → JSON.stringify/parseを使う
センシティブな情報は保存しない(XSSでアクセスできる)
Observer系API:
IntersectionObserver → ビューポート検出(遅延読み込み)
MutationObserver → DOM変化の監視
ResizeObserver → 要素サイズ変化の監視
履歴・URL:
history.pushState() → SPAでURLを変更(ページリロードなし)
URLSearchParams → クエリパラメータを簡単に操作
17. パフォーマンスの基礎
このセクションではJavaScriptのパフォーマンスを改善するための知識を整理します。ブラウザのレンダリングパイプライン、Reflow/Repaintの違い、Web Workersによる並列処理まで解説します。
17-1. レンダリングパイプライン基礎
ブラウザがHTMLを画面に表示するまでの処理を「レンダリングパイプライン」と呼びます。JavaScriptがどこに影響を与えるかを理解することがパフォーマンス改善の第一歩です。
┌─────────────────────────────────────────────────────────────────┐
│ ブラウザのレンダリングパイプライン │
├─────────────────────────────────────────────────────────────────┤
│ │
│ HTML解析 → DOM生成 │
│ CSS解析 → CSSOM生成 │
│ ↓ │
│ Render Tree(DOM + CSSOMの結合) │
│ ↓ │
│ Layout(各要素のサイズと位置を計算)← Reflowここで発生 │
│ ↓ │
│ Paint(ピクセルに描画)← Repaintここで発生 │
│ ↓ │
│ Composite(レイヤーを合成して画面に表示) │
│ │
│ 60fpsを維持するには1フレーム = 16.7ms以内に全処理が必要 │
│ │
└─────────────────────────────────────────────────────────────────┘
17-2. Repaint vs Reflow
Reflow(レイアウト再計算)
最もコストが高い操作です。ある要素のサイズや位置が変わると、影響を受ける全要素のレイアウトを再計算します。
// ❌ Reflowを引き起こすプロパティの変更
element.style.width = '200px' // Reflow
element.style.height = '100px' // Reflow
element.style.margin = '10px' // Reflow
element.style.padding = '5px' // Reflow
element.style.display = 'block' // Reflow
element.style.position = 'relative' // Reflow
// ❌ Reflowを引き起こすプロパティの読み取り
// これらを読み取るとブラウザは最新の値を返すために即座にReflowする
const width = element.offsetWidth // Reflow → 読み取り
const height = element.offsetHeight // Reflow → 読み取り
const rect = element.getBoundingClientRect() // Reflow
Repaint(再描画)
Reflowより軽いですが、それでもコストがかかります。
// ❌ Repaintのみを引き起こすプロパティ(Reflowは起きない)
element.style.color = 'red'
element.style.backgroundColor = 'blue'
element.style.visibility = 'hidden'
element.style.borderColor = 'green'
Compositeのみ(最も軽い)
// ✅ GPUで合成される(ReflowもRepaintも起きない)
element.style.transform = 'translateX(100px)'
element.style.opacity = '0.5'
Reflow最小化のテクニック
// ❌ 悪い例: DOMを読み書きを交互に行う(Reflowが何度も起きる)
const box = document.querySelector('.box')
box.style.width = `${box.offsetWidth + 10}px` // Reflow(読み取り)+ Reflow(書き込み)
box.style.height = `${box.offsetHeight + 10}px` // またReflow ×2
// ✅ 良い例: 読み取りをまとめて、書き込みをまとめる
const box = document.querySelector('.box')
const width = box.offsetWidth // 読み取り(1回目のReflow)
const height = box.offsetHeight // 読み取り(キャッシュ済みなのでReflowなし)
box.style.width = `${width + 10}px` // 書き込み(Reflow予約)
box.style.height = `${height + 10}px` // 書き込み(バッチ処理)
// ✅ requestAnimationFrameでまとめる
function updateLayout() {
requestAnimationFrame(() => {
// 次のフレームで全ての変更をまとめて適用
box.style.width = '200px'
box.style.height = '100px'
})
}
// ✅ CSSクラスの変更でまとめる(最も推奨)
// ❌ インラインスタイルを複数変更
element.style.width = '200px'
element.style.height = '100px'
element.style.backgroundColor = 'blue'
// ✅ CSSクラスを1回変更(内部でまとめて処理される)
element.classList.add('expanded') // CSS: .expanded { width: 200px; height: 100px; background: blue }
17-3. イベントループとパフォーマンス
// マクロタスクvsマイクロタスク(パフォーマンス観点)
console.log('1: 同期')
setTimeout(() => console.log('4: setTimeout(マクロタスク)'), 0)
Promise.resolve().then(() => console.log('3: Promise(マイクロタスク)'))
console.log('2: 同期')
// 出力順: 1 → 2 → 3 → 4
// マイクロタスク(Promise)はマクロタスク(setTimeout)より先に実行
// ❌ メインスレッドをブロックする重い処理
function heavyComputation() {
let result = 0
for (let i = 0; i < 1_000_000_000; i++) {
result += Math.sqrt(i) // 数秒かかる
}
return result
}
const result = heavyComputation() // UIがフリーズ!
// ✅ 処理を分割してyieldする
async function nonBlockingComputation() {
let result = 0
const CHUNK_SIZE = 1_000_000
for (let i = 0; i < 1_000_000_000; i += CHUNK_SIZE) {
// CHUNK_SIZEごとにイベントループに制御を返す
await new Promise(resolve => setTimeout(resolve, 0))
for (let j = i; j < Math.min(i + CHUNK_SIZE, 1_000_000_000); j++) {
result += Math.sqrt(j)
}
}
return result
}
17-4. メモ化(Memoization)
// 重い計算のキャッシュ
function memoize(fn) {
const cache = new Map()
return function(...args) {
const key = JSON.stringify(args)
if (cache.has(key)) return cache.get(key)
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
const expensiveFn = memoize(function(n) {
// 重い計算...
return n * 2
})
expensiveFn(10) // 計算される
expensiveFn(10) // キャッシュから返す(計算しない)
17-5. Debounce / Throttle
// Debounce: 一定時間待ってから実行(連続する呼び出しの最後だけ)
// 用途: 検索ボックス(入力が止まってから検索)、リサイズ後の処理
function debounce(fn, delay) {
let timeoutId
return function(...args) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
const debouncedSearch = debounce(search, 300)
input.addEventListener('input', debouncedSearch) // 300ms止まったら検索
// Throttle: 一定時間に1回だけ実行
// 用途: スクロールイベント、マウス移動イベント
function throttle(fn, interval) {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= interval) {
lastTime = now
return fn.apply(this, args)
}
}
}
const throttledScroll = throttle(handleScroll, 100)
window.addEventListener('scroll', throttledScroll) // 100msに1回だけ実行
17-6. Web Workers
Web Workersを使うと、重い処理をメインスレッドとは別のスレッドで実行できます。UIをフリーズさせずに計算集約的な処理を行えます。
// worker.js(別ファイル)
// Web Workerの中ではDOMにアクセスできない
// postMessageでメインスレッドと通信する
self.addEventListener('message', (event) => {
const { type, data } = event.data
if (type === 'COMPUTE') {
// 重い計算(UIをブロックしない)
let result = 0
for (let i = 0; i < 1_000_000_000; i++) {
result += Math.sqrt(i)
}
// 結果をメインスレッドに送る
self.postMessage({ type: 'RESULT', data: result })
}
})
// main.js(メインスレッド)
const worker = new Worker('./worker.js')
// Workerにメッセージを送る
worker.postMessage({ type: 'COMPUTE', data: null })
// Workerからの結果を受け取る
worker.addEventListener('message', (event) => {
const { type, data } = event.data
if (type === 'RESULT') {
console.log('計算結果:', data)
// UIを更新する
}
})
// エラーハンドリング
worker.addEventListener('error', (error) => {
console.error('Workerエラー:', error)
})
// 不要になったら終了
worker.terminate()
// Transferable Objects: 大きなデータの効率的な転送
// コピーではなく「所有権の移転」なのでゼロコスト
const buffer = new ArrayBuffer(1024 * 1024) // 1MB
// ❌ コピー(遅い)
worker.postMessage({ buffer }) // bufferがコピーされる
// ✅ 転送(速い)
worker.postMessage({ buffer }, [buffer]) // 所有権を移転(メインスレッドではアクセス不可に)
17-7. このセクションのまとめ
レンダリングパイプライン:
DOM生成 → Render Tree → Layout → Paint → Composite
Reflow(レイアウト再計算)が最もコスト高
Reflowを避けるには:
読み取りをまとめる → 書き込みをまとめる
transform/opacityはCompositeのみ(最も安い)
CSSクラスの変更でまとめて適用
パフォーマンス改善手法:
メモ化(memoize) → 同じ引数の計算をキャッシュ
Debounce → 入力が止まってから実行(検索)
Throttle → 一定間隔に実行を制限(スクロール)
requestAnimationFrame → 次のフレームでDOM更新をまとめる
Web Workers:
重い計算をメインスレッドから切り離す
DOMにはアクセスできない
postMessageで非同期通信
Transferable Objectsで大きなデータを効率転送
イベントループとの関係:
Microtask(Promise)→ Macrotask(setTimeout)→ レンダリング
重い同期処理 → UIフリーズ → Workerに移す
次のセクションではセキュリティの基礎を解説します。
18. セキュリティの基礎
このセクションではJavaScript開発者が知るべきセキュリティの基本を解説します。XSS(クロスサイトスクリプティング)とプロトタイプ汚染は特に重要な2大リスクです。
18-1. XSS(Cross-Site Scripting)対策
// ❌ 危険: ユーザー入力をそのままHTMLに
element.innerHTML = userInput // XSS脆弱性!
// ✅ 安全: textContentを使う
element.textContent = userInput // HTMLとして解釈されない
// ❌ 危険: URLをそのまま使う
link.href = userInput // javascript:alert(1) などが挿入できる
// ✅ 安全: URLを検証
function sanitizeUrl(url) {
try {
const parsed = new URL(url)
if (!['http:', 'https:'].includes(parsed.protocol)) {
return '#'
}
return parsed.toString()
} catch {
return '#'
}
}
// HTMLをサニタイズする場合はDOMPurifyを使う
import DOMPurify from 'dompurify'
element.innerHTML = DOMPurify.sanitize(userHtml)
18-2. プロトタイプ汚染(Prototype Pollution)
// ❌ 危険なパターン
function merge(target, source) {
for (const key in source) {
target[key] = source[key] // __proto__ などが混入する可能性
}
}
// 攻撃例
merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'))
({}).isAdmin // true(全オブジェクトが汚染される!)
// ✅ 安全なマージ
function safeMerge(target, source) {
for (const key of Object.keys(source)) { // for...inではなくObject.keys
if (key === '__proto__' || key === 'constructor') continue
target[key] = source[key]
}
}
18-3. CSRF・クリックジャッキング対策
// CSRF(Cross-Site Request Forgery)対策
// CSRFトークンをリクエストに含める
async function safeFetch(url, options = {}) {
const token = document.querySelector('meta[name="csrf-token"]')?.content
return fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token, // CSRFトークンをヘッダーに
...options.headers
},
credentials: 'same-origin' // 同一オリジンのCookieのみ送信
})
}
// SameSite Cookie(サーバー側の設定)
// Set-Cookie: session=abc; SameSite=Strict; Secure; HttpOnly
// SameSite=Strict → 同一サイトのリクエストのみCookieを送信
// SameSite=Lax → 安全なトップレベルナビゲーションでは送信
// Secure → HTTPSのみ
// HttpOnly → JavaScriptからアクセス不可
// Content Security Policy(CSP)でXSSを防ぐ
// HTTPヘッダー:
// Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com
// または<meta>タグで
// <meta http-equiv="Content-Security-Policy" content="default-src 'self'">
// 安全な乱数生成(Math.random()は予測可能なので暗号用途には不可)
// ❌ 危険: 予測可能
const token = Math.random().toString(36).slice(2)
// ✅ 安全: 暗号的に安全な乱数
function generateSecureToken(length = 32) {
const array = new Uint8Array(length)
crypto.getRandomValues(array) // Web Crypto API
return Array.from(array, b => b.toString(16).padStart(2, '0')).join('')
}
generateSecureToken() // 暗号的に安全な64文字のhex文字列
// データの検証(Zodを使った例)
import { z } from 'zod'
const UserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150),
})
function processUserInput(rawData) {
const result = UserSchema.safeParse(rawData)
if (!result.success) {
throw new TypeError(`バリデーションエラー: ${result.error.message}`)
}
return result.data // 検証済みのデータ
}
18-4. このセクションのまとめ
XSS対策:
innerHTMLにユーザー入力を入れない → textContentを使う
link.hrefにユーザー入力を入れない → URLを検証する
HTMLを表示したい場合: DOMPurifyでサニタイズ
CSP(Content-Security-Policy)ヘッダーで外部スクリプトをブロック
プロトタイプ汚染 対策:
for...inでのマージを避ける → Object.keys() を使う
__proto__, constructor, prototypeキーを除外する
JSON.parseする前にスキーマを検証する
Object.create(null) でプロトタイプなしのオブジェクトを使う
CSRF対策:
X-CSRF-Tokenヘッダーをリクエストに追加
SameSite=Strict Cookieを使用
credentials: 'same-origin' で同一オリジンのみCookieを送信
その他のセキュリティ基本:
eval() は使わない(任意コード実行の危険)
dangerouslySetInnerHTML(React)は最小限に
math.random() を暗号用途に使わない → crypto.getRandomValues()
入力バリデーション: Zod等でスキーマ定義
依存パッケージの脆弱性をnpm auditで定期確認
19. テスト戦略
このセクションではJavaScriptのテスト戦略をVitest / Jestを例に解説します。単体テスト・モック・非同期テストのパターンを体系化します。
19-1. Vitest / Jestでの基本
import { describe, it, expect, beforeEach, vi } from 'vitest'
// テストの構造
describe('add関数', () => {
it('正の数を足せる', () => {
expect(add(1, 2)).toBe(3)
})
it('負の数も扱える', () => {
expect(add(-1, -2)).toBe(-3)
})
})
// 非同期テスト
describe('fetchUser', () => {
it('正常系: ユーザーを取得できる', async () => {
const user = await fetchUser(1)
expect(user).toMatchObject({ id: 1, name: expect.any(String) })
})
it('異常系: 存在しないIDでエラー', async () => {
await expect(fetchUser(-1)).rejects.toThrow('IDが無効です')
})
})
// モック
describe('sendNotification', () => {
const mockFetch = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
global.fetch = mockFetch
})
it('APIを1回呼ぶ', async () => {
mockFetch.mockResolvedValue({ ok: true, json: () => ({}) })
await sendNotification('user1', 'メッセージ')
expect(mockFetch).toHaveBeenCalledOnce()
expect(mockFetch).toHaveBeenCalledWith('/api/notify', expect.objectContaining({
method: 'POST'
}))
})
})
19-2. テストの種類とピラミッド
テストピラミッド:
/\
/E2E\ 少ない(遅い・コストが高い)
/------\ Playwright, Cypress
/ 統合 \
/----------\ Fetchのモック、DBとの連携テスト
/ 単体テスト \ 多い(速い・独立している)
/--------------\ Vitest, Jest, 純粋な関数テスト
単体テスト(Unit Test):
→ 関数・クラス単体を独立してテスト
→ 外部依存はモック化
→ 速い(ミリ秒)
統合テスト(Integration Test):
→ 複数のモジュールを組み合わせてテスト
→ 実際のDBやAPIを使う場合もある
E2Eテスト(End-to-End):
→ ブラウザを実際に動かしてテスト
→ ユーザーの操作を自動化
19-3. スナップショットテストとテスト設計
// スナップショットテスト
import { describe, it, expect } from 'vitest'
describe('formatUser', () => {
it('ユーザーを正しくフォーマット', () => {
const result = formatUser({ id: 1, name: 'Alice', age: 30 })
expect(result).toMatchInlineSnapshot(`
{
"displayName": "Alice",
"isAdult": true,
"label": "ID:1 - Alice",
}
`)
// 初回実行時にスナップショットを作成
// 次回以降は変化がないことを確認
})
})
// テストダブル(Test Double)の種類
// 1. Stub: 固定値を返す
const fetchUserStub = vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
// 2. Spy: 実際の関数を実行しつつ呼び出しを記録
const consoleSpy = vi.spyOn(console, 'log')
myFunction()
expect(consoleSpy).toHaveBeenCalledWith('expected output')
// 3. Mock: 実装を完全に置き換え
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
updateUser: vi.fn().mockResolvedValue({ success: true })
}))
// テストユーティリティ関数
// 良いテストは: 独立・明確・高速
// ❌ 悪いテスト: 外部状態に依存
let database = []
it('ユーザーを追加できる', () => {
database.push({ name: 'Alice' })
expect(database).toHaveLength(1) // 他のテストの影響を受ける
})
// ✅ 良いテスト: 独立している
it('ユーザーを追加できる', () => {
const db = [] // テスト内で完結
db.push({ name: 'Alice' })
expect(db).toHaveLength(1)
})
// テストのセットアップ・クリーンアップ
describe('UserService', () => {
let service
beforeEach(() => {
service = new UserService() // 各テスト前に初期化
})
afterEach(() => {
service.cleanup() // 各テスト後にクリーンアップ
})
it('ユーザーを取得できる', async () => {
const user = await service.findUser(1)
expect(user).toBeDefined()
})
})
// カバレッジの設定(vitest.config.ts)
// coverage: { reporter: ['text', 'html'], threshold: { lines: 80 } }
19-4. このセクションのまとめ
テストの構造(Vitest / Jest):
describe() → テストグループ
it()/test() → 個別のテストケース
expect() → アサーション
beforeEach/afterEach → セットアップ・クリーンアップ
マッチャーの種類:
toBe() → 厳密等価(===)
toEqual() → 深い等価(オブジェクト比較)
toMatchObject() → 部分一致
toMatchInlineSnapshot() → スナップショット
toThrow() → エラーが投げられる
toHaveBeenCalledWith() → モック関数の呼び出し確認
非同期テスト:
async/awaitでテスト関数を書く
rejects.toThrow() でPromiseの失敗をテスト
モック(vi.fn() / jest.fn()):
外部APIやDBをモックして単体テスト
mockResolvedValue() で非同期のモックを設定
vi.spyOn() で実関数を監視しつつ呼び出し記録
vi.mock() でモジュール全体を置き換え
テストピラミッド:
単体テスト(多い・速い)→ 統合テスト → E2Eテスト(少ない・遅い)
カバレッジ目標: ビジネスロジックは80%以上
20. ES2020〜2024の新機能
このセクションではES2020からES2024まで追加された重要な新機能を整理します。それぞれの機能が「何を解決するために追加されたか」の背景も説明します。
20-1. ES2020
ES2020は「null/undefinedの安全な扱い」と「大きな整数」が主な追加でした。
// ✅ Optional Chaining(?.): null/undefinedに安全にアクセス
// 以前: user && user.profile && user.profile.address && user.profile.address.city
user?.profile?.address?.city // undefinedを返す(エラーにならない)
// メソッド呼び出し
user?.getName?.() // getNameが存在すれば呼ぶ
// 配列
arr?.[0] // arrがnull/undefinedでも安全
// ✅ Nullish Coalescing(??): null/undefinedのみフォールバック
// || はfalsy全部(0, '', false, NaN)をフォールバックにしてしまう
const port = config.port || 3000 // ❌ portが0の場合もフォールバック
const port = config.port ?? 3000 // ✅ null/undefinedのみフォールバック
// ✅ BigInt: 大きな整数を扱う
// Number.MAX_SAFE_INTEGER = 9007199254740991(約9千兆)を超えると精度が失われる
9007199254740991 + 1 === 9007199254740991 + 2 // true(精度が失われている)
const big = 9007199254740993n // BigIntリテラル(nサフィックス)
BigInt(9007199254740993) // BigIntコンストラクタ
big + 1n // 9007199254740994n(正確)
// BigIntと普通のNumberは混在不可
big + 1 // TypeError
big + 1n // OK
// ✅ Promise.allSettled: 全て完了(成功・失敗問わず)まで待つ
// Promise.allは一つでも失敗したら拒否される
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/broken') // これが失敗しても他の結果は得られる
])
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log(result.value)
} else {
console.error(result.reason)
}
})
// ✅ globalThis: 環境に依存しないグローバルオブジェクト
// ブラウザ: window, Node.js: global, Web Worker: self
globalThis.setTimeout // どの環境でも動く
// ✅ String.prototype.matchAll: 全マッチを反復
const text = 'apple 1, banana 2, cherry 3'
const matches = [...text.matchAll(/(\w+) (\d+)/g)]
matches[0] // ['apple 1', 'apple', '1', index: 0, ...]
matches[1] // ['banana 2', 'banana', '2', ...]
// matchと違い、キャプチャグループも繰り返し取れる
20-2. ES2021
ES2021は「文字列操作の改善」と「代入演算子の拡充」が主な追加でした。
// ✅ String.prototype.replaceAll: 全置換
// replaceは正規表現のgフラグなしでは最初の1つしか置換しない
'hello world hello'.replace('hello', 'hi') // 'hi world hello'(1つだけ)
'hello world hello'.replaceAll('hello', 'hi') // 'hi world hi'(全て)
// ✅ Promise.any: 最初に成功したPromiseを返す
// Promise.raceは最初に完了(成功・失敗問わず)したものを返す
// Promise.anyは最初に「成功した」ものを返す(全失敗時はAggregateError)
try {
const first = await Promise.any([
fetch('/api/endpoint1'), // 失敗
fetch('/api/endpoint2'), // 成功 ← これが返される
fetch('/api/endpoint3'), // 成功(遅い)
])
} catch (error) {
if (error instanceof AggregateError) {
console.log('全て失敗:', error.errors)
}
}
// ✅ Logical Assignment Operators: 論理演算子と代入の組み合わせ
// config.timeoutがfalsy (0, '', null, undefined) なら3000を代入
config.timeout ||= 3000
// config.debugがtruthyなら 'verbose' を代入
config.debug &&= 'verbose'
// config.retriesがnullかundefinedなら3を代入
config.retries ??= 3
// ✅ Numeric Separators: 数値の可読性向上
const million = 1_000_000 // 1,000,000
const bytes = 0xFF_00_FF // 16進数も区切れる
const pi = 3.141_592_653
// ✅ WeakRef, FinalizationRegistry(ES2021)
// WeakRef: 第14章で詳述
// FinalizationRegistry: GC後のクリーンアップコールバック
const registry = new FinalizationRegistry((heldValue) => {
console.log(`GCされた: ${heldValue}`)
})
let obj = { data: 'important' }
registry.register(obj, 'obj-cleanup-token')
obj = null // GC後にコールバックが呼ばれる(タイミングは保証なし)
20-3. ES2022
ES2022は「クラス機能の完成」と「配列・文字列への便利メソッド追加」が主な内容でした。
// ✅ クラスフィールドとプライベート(Stage 4確定)
class Counter {
// パブリックフィールド(インスタンスプロパティ)
count = 0
label = 'counter'
// プライベートフィールド(# プレフィックス)
#secret = 'internal'
#listeners = []
// スタティックフィールド
static defaultLabel = 'default'
static #instanceCount = 0
constructor() {
Counter.#instanceCount++
}
// プライベートメソッド
#notify() {
this.#listeners.forEach(fn => fn(this.count))
}
// プライベートフィールドの存在チェック
static isCounter(obj) {
return #secret in obj // in演算子でプライベートフィールドをチェック
}
increment() {
this.count++
this.#notify()
}
}
// ✅ Array.prototype.at / String.prototype.at: 末尾からのアクセス
const arr = [1, 2, 3, 4, 5]
arr.at(0) // 1(先頭)
arr.at(-1) // 5(末尾)
arr.at(-2) // 4(末尾から2番目)
// arr[arr.length - 1] より簡潔
'hello'.at(-1) // 'o'
// ✅ Object.hasOwn: Object.prototype.hasOwnPropertyの改良版
// hasOwnPropertyはObject.prototypeを持たないオブジェクトでエラーになる可能性
const obj = Object.create(null) // プロトタイプなし
obj.hasOwnProperty // undefined(エラーになる)
Object.hasOwn(obj, 'key') // 安全(staticメソッドなので)
// ✅ at() for TypedArrayも追加
new Uint8Array([1, 2, 3]).at(-1) // 3
// ✅ Top-level await(ESMのみ)
// モジュールのトップレベルでawaitが使える
// 以前: 即時実行async関数が必要だった
// (async () => { const data = await fetch(...) })()
const config = await fetch('/api/config').then(r => r.json())
export const apiBaseUrl = config.baseUrl
20-4. ES2023
ES2023は「配列の非破壊的操作メソッド」が主な追加でした。関数型プログラミングスタイルを推進します。
// ✅ 非破壊的配列操作メソッド(immutableパターンに最適)
const arr = [3, 1, 4, 1, 5]
// sort() は破壊的(元の配列を変更する)
// toSorted() は非破壊的(新しい配列を返す)
arr.toSorted() // [1, 1, 3, 4, 5](元のarrは変わらない)
arr.toSorted((a, b) => b - a) // [5, 4, 3, 1, 1](降順)
arr.toReversed() // [5, 1, 4, 1, 3](非破壊的reverse)
// splice() は破壊的
// toSpliced(start, deleteCount, ...items) は非破壊的
arr.toSpliced(1, 2, 99) // [3, 99, 5](index1から2個削除して99を挿入)
arr.toSpliced(1, 0, 99) // [3, 99, 1, 4, 1, 5](挿入のみ)
// with(index, value): 特定インデックスを変更した新配列
arr.with(2, 99) // [3, 1, 99, 1, 5]
arr.with(-1, 99) // [3, 1, 4, 1, 99](負のインデックスも可)
// ✅ findLast / findLastIndex: 末尾から検索
[1, 2, 3, 4, 5].findLast(x => x % 2 === 0) // 4(末尾から最初の偶数)
[1, 2, 3, 4, 5].findLastIndex(x => x % 2 === 0) // 3
// ✅ Array.prototype.toReversedはTypedArrayにも追加
new Uint8Array([1, 2, 3]).toReversed() // Uint8Array [3, 2, 1]
// ✅ Hashbang Grammar(シバン行のサポート)
// Node.jsスクリプトの先頭に # ! で始まるコマンドを書ける
// #!/usr/bin/env node
20-5. ES2024
ES2024は「Promiseの使いやすさ向上」と「グルーピング」が主な追加でした。
// ✅ Promise.withResolvers: Promiseとresolve/rejectを同時に取り出す
// 以前は外部からresolveを使うには変数に保存するトリックが必要だった
let resolve, reject
const promise = new Promise((res, rej) => {
resolve = res
reject = rej
})
// ES2024: スッキリ書ける
const { promise, resolve, reject } = Promise.withResolvers()
// 使用例: イベント駆動の非同期処理
function waitForClick(button) {
const { promise, resolve } = Promise.withResolvers()
button.addEventListener('click', resolve, { once: true })
return promise
}
// ✅ Object.groupBy / Map.groupBy: グルーピング
const products = [
{ name: 'apple', category: 'fruit', price: 100 },
{ name: 'banana', category: 'fruit', price: 80 },
{ name: 'carrot', category: 'vegetable', price: 60 },
{ name: 'spinach', category: 'vegetable', price: 120 },
]
const byCategory = Object.groupBy(products, p => p.category)
// {
// fruit: [{ name: 'apple', ... }, { name: 'banana', ... }],
// vegetable: [{ name: 'carrot', ... }, { name: 'spinach', ... }]
// }
// 以前はreduceで書いていた(verboseだった)
const byCategory = products.reduce((groups, p) => {
;(groups[p.category] ??= []).push(p)
return groups
}, {})
// Map.groupBy: キーがオブジェクトの場合はMapを使う
const expensive = { label: '高い', threshold: 100 }
const cheap = { label: '安い', threshold: 0 }
const byPrice = Map.groupBy(products, p => p.price >= 100 ? expensive : cheap)
// ✅ RegExp v flag(Unicodeセット記法)
// /vフラグで文字クラスのセット演算が可能
const re = /[\p{Emoji}--\p{Number}]/v // 数字絵文字を除く絵文字にマッチ
20-6. 新機能の使用可能チェックリスト
| 機能 | ES年 | ブラウザ対応 | Node.js |
|---|---|---|---|
Optional chaining ?. |
2020 | 全モダンブラウザ | 14+ |
Nullish coalescing ?? |
2020 | 全モダンブラウザ | 14+ |
| BigInt | 2020 | Chrome 67+, FF 68+, Safari 14+ | 10+ |
| Promise.allSettled | 2020 | 全モダンブラウザ | 12+ |
クラスプライベートフィールド # |
2022 | Chrome 74+, FF 90+, Safari 14.1+ | 12+ |
| Array.at() | 2022 | 全モダンブラウザ | 16.6+ |
| Top-level await | 2022 | 全モダンブラウザ(ESMのみ) | 14.8+ |
| Array.toSorted() 等 | 2023 | Chrome 110+, FF 115+, Safari 16+ | 20+ |
| Object.groupBy | 2024 | Chrome 117+, FF 119+, Safari 17.4+ | 21+ |
| Promise.withResolvers | 2024 | Chrome 119+, FF 121+, Safari 17.4+ | 22+ |
20-7. このセクションのまとめ
ES2020の重要な追加:
Optional chaining (?.) → null/undefinedに安全にアクセス
Nullish coalescing (??) → null/undefinedのみフォールバック(|| より精確)
BigInt → 安全な整数範囲を超えた計算
Promise.allSettled → 成功・失敗問わず全完了を待つ
ES2021の重要な追加:
replaceAll() → 文字列全置換
Promise.any → 最初に成功したPromise
論理代入演算子(||=, &&=, ??=)
ES2022の重要な追加:
クラスプライベートフィールド(#)の正式化
Array/String.at() → 末尾からのインデックスアクセス
Object.hasOwn() → hasOwnPropertyの安全な代替
Top-level await(ESMのみ)
ES2023の重要な追加:
toSorted/toReversed/toSpliced/with → 非破壊的配列操作
findLast/findLastIndex → 末尾からの検索
ES2024の重要な追加:
Promise.withResolvers → resolve/rejectを外部から操作
Object.groupBy → 配列のグルーピング(reduceの代替)
21. よくある落とし穴FAQ
このセクションではJavaScript開発者がよく遭遇する落とし穴と、その解決策をQ&A形式で整理します。特に this の挙動、参照型の比較、非同期処理の順序は繰り返し悩まれるポイントです。
Q1. this が期待した値でない
class Button {
label = 'Click me'
// ❌ 問題: コールバックとして渡すとthisが変わる
handleClick() {
console.log(this.label) // undefinedになる場合がある
}
}
const btn = new Button()
element.addEventListener('click', btn.handleClick) // thisがelementになる
// ✅ 解決1: アロー関数でバインド
class Button {
label = 'Click me'
handleClick = () => { // アロー関数は定義時のthisを使う
console.log(this.label) // 'Click me'
}
}
// ✅ 解決2: bindで明示的にバインド
element.addEventListener('click', btn.handleClick.bind(btn))
Q2. 配列の比較が false になる
// 配列はオブジェクト(参照型)なので参照比較になる
[1, 2, 3] === [1, 2, 3] // false(別のメモリアドレス)
// 内容の比較
JSON.stringify([1, 2, 3]) === JSON.stringify([1, 2, 3]) // true(単純な場合)
// より正確な深い比較はlodash等を使う
import { isEqual } from 'lodash'
isEqual([1, 2, 3], [1, 2, 3]) // true
Q3. 非同期処理の順序がおかしい
// ❌ Promiseを待っていない
let result
fetchData().then(data => { result = data })
console.log(result) // undefined(まだ届いていない)
// ✅ async/awaitで待つ
async function main() {
const result = await fetchData()
console.log(result) // データが届いている
}
Q4. メモリリーク
// ❌ イベントリスナーを削除しない
function ComponentA() {
window.addEventListener('resize', handleResize)
// コンポーネントが消えてもリスナーが残り続ける!
}
// ✅ クリーンアップする
function ComponentA() {
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize) // Reactならuse效果のreturn
}
Q5. null と undefined の混在
// 統一ルール
// undefined: 意図しない「なし」(変数未定義、オプション引数の省略)
// null: 意図的な「なし」(データがないことを明示)
// チェック
value == null // nullかundefinedのどちらかをまとめてチェック
value === null // nullのみ
value === undefined // undefinedのみ
value != null // nullでもundefinedでもない
Q6. for...in で配列を反復したらバグが出た
const arr = [1, 2, 3]
arr.extraProp = 'oops' // 配列にプロパティを追加(珍しくないケース)
// ❌ for...inは全プロパティを列挙する(プロトタイプチェーンも含む)
for (const key in arr) {
console.log(key) // '0', '1', '2', 'extraProp'(!)
}
// ✅ 配列の反復はfor...ofを使う
for (const value of arr) {
console.log(value) // 1, 2, 3
}
// ✅ またはforEach / map / filterを使う
arr.forEach(v => console.log(v))
// for...inは「オブジェクトのキー列挙」用
const obj = { a: 1, b: 2, c: 3 }
for (const key in obj) {
if (Object.hasOwn(obj, key)) { // プロトタイプのプロパティを除外
console.log(key, obj[key])
}
}
Q7. parseInt の結果がおかしい
// ❌ parseIntは基数(radix)がないと問題が起きる場合がある
parseInt('08') // ブラウザ依存(8進数として解釈する場合あり)
parseInt('0x10') // 16(0xプレフィックスは16進数)
// ✅ 必ず基数を指定する
parseInt('10', 10) // 10(10進数)
parseInt('10', 2) // 2(2進数)
parseInt('10', 16) // 16(16進数)
parseInt('08', 10) // 8(問題なし)
// 浮動小数点数のパース
parseFloat('3.14') // 3.14
Number('3.14') // 3.14(より厳密)
+'3.14' // 3.14(単項+演算子でも変換できる)
// ❌ 注意: parseIntは小数点以下を切り捨てる
parseInt('3.14') // 3(parseFloatを使うべき)
// 数値チェック
Number.isInteger(3) // true
Number.isInteger(3.1) // false
Number.isFinite(Infinity) // false
Number.isNaN(NaN) // true
isNaN('hello') // true(グローバルのisNaNは型変換する)
Number.isNaN('hello') // false(文字列はNaNではない)
Q8. スプレッド構文でオブジェクトをコピーしたらネストが共有された
const original = {
name: 'Alice',
address: { // ネストされたオブジェクト
city: 'Tokyo',
country: 'Japan'
}
}
// ❌ シャローコピー: ネストは参照が共有される
const copy = { ...original }
copy.name = 'Bob' // original.nameは変わらない
copy.address.city = 'Osaka' // original.address.cityも変わる!
// ✅ ディープコピー: structuredClone(ES2022+)
const deepCopy = structuredClone(original)
deepCopy.address.city = 'Osaka' // originalは変わらない
// 注意: structuredCloneは関数・DOM要素・クロージャはコピーできない
// ✅ JSON roundtrip(簡易版、制限あり)
const deepCopy2 = JSON.parse(JSON.stringify(original))
// 制限: undefined, 関数, Date, RegExp, Map, Setは正確にコピーできない
// ✅ lodashのcloneDeep(より完全)
import { cloneDeep } from 'lodash'
const deepCopy3 = cloneDeep(original)
Q9. Promise.allの途中でエラーが出たら全部失敗する
// ❌ 一つでも失敗すると全て失敗
const results = await Promise.all([
fetchUser(1), // 成功
fetchUser(2), // 失敗 → 全部の結果が得られない
fetchUser(3), // 成功
])
// ✅ Promise.allSettledで全結果を取得
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(2),
fetchUser(3),
])
const users = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value)
const errors = results
.filter(r => r.status === 'rejected')
.map(r => r.reason)
// ✅ 個別にtry/catchしてからPromise.all
const results = await Promise.all([
fetchUser(1).catch(e => ({ error: e })),
fetchUser(2).catch(e => ({ error: e })),
fetchUser(3).catch(e => ({ error: e })),
])
Q10. typeof null === 'object' はなぜか
// これはJavaScriptの有名なバグで、ES1から存在する(後方互換性のため修正不可)
typeof null // 'object'(バグ!nullはオブジェクトではない)
typeof undefined // 'undefined'
typeof 42 // 'number'
typeof 'hello' // 'string'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof 42n // 'bigint'
typeof function(){} // 'function'
typeof {} // 'object'
typeof [] // 'object'(配列もオブジェクト)
// nullチェックの正しい方法
value === null // ✅ nullのみ
value == null // nullまたはundefined
value !== null // nullでない
typeof value === 'object' && value !== null // ✅ nullを除くオブジェクトチェック
// 配列チェック
Array.isArray([]) // ✅ true
typeof [] === 'object' // true(だが配列かどうかは分からない)
[] instanceof Array // true(ただしフレームが異なると失敗する場合あり)
Q11. 非同期処理でループを使うとき
const userIds = [1, 2, 3, 4, 5]
// ❌ forEachはasync/awaitに対応していない
userIds.forEach(async (id) => {
const user = await fetchUser(id) // このawaitはforEachに伝わらない
console.log(user)
})
// forEachが終わっても全fetchが完了していない
// ✅ for...ofで順番に処理
for (const id of userIds) {
const user = await fetchUser(id)
console.log(user)
}
// ✅ 並列処理したい場合: Promise.all + map
const users = await Promise.all(
userIds.map(id => fetchUser(id))
) // 全て並列実行、全て完了したら次へ
// ✅ 並列数を制限したい場合
async function processWithConcurrency(items, fn, limit = 3) {
const results = []
for (let i = 0; i < items.length; i += limit) {
const batch = items.slice(i, i + limit)
const batchResults = await Promise.all(batch.map(fn))
results.push(...batchResults)
}
return results
}
const users = await processWithConcurrency(userIds, fetchUser, 3)
Q12. デストラクチャリングのデフォルト値が効かない
// デストラクチャリングのデフォルト値はundefinedのみ適用される
const { name = 'ゲスト', age = 0 } = { name: null, age: 0 }
// name = null(nullはデフォルト値を使わない)
// age = 0(0はデフォルト値を使わない)
// nullを「なし」として扱いたい場合
const { name: rawName, age: rawAge = 0 } = data
const name = rawName ?? 'ゲスト' // null/undefinedの場合デフォルト
// ネストしたデストラクチャリング
const { address: { city = '東京', country = '日本' } = {} } = user
// addressがundefinedでも { city, country } のデストラクチャリングが安全
// 配列のデストラクチャリング
const [first = 0, second = 0, ...rest] = [1]
// first = 1, second = 0(デフォルト), rest = []
Q13. クロージャで意図しない値が共有される(ループ変数の落とし穴)
// ❌ varはブロックスコープを持たないので全てのクロージャでiを共有
const fns = []
for (var i = 0; i < 3; i++) {
fns.push(() => console.log(i))
}
fns[0]() // 3(iは最終値の3)
fns[1]() // 3
fns[2]() // 3
// ✅ letは各イテレーションで新しいバインディングを作る
const fns2 = []
for (let i = 0; i < 3; i++) {
fns2.push(() => console.log(i))
}
fns2[0]() // 0
fns2[1]() // 1
fns2[2]() // 2
// ✅ IIFEで即座に値をキャプチャする(varを使う場合)
const fns3 = []
for (var i = 0; i < 3; i++) {
fns3.push(((j) => () => console.log(j))(i))
}
22. 図解: イベントループとスタック
このセクションでは、イベントループの完全な動作をコード例と図で整理します。PromiseのマイクロタスクキューがMacrotaskキューよりも優先される理由も含めて解説します。
22-1. コールスタック・キュー・イベントループ(完全版)
╔═══════════════════════════════════════════════════════════════════╗
║ JavaScript実行モデル(完全版) ║
╠══════════════╦══════════════════════╦═══════════════════════════╣
║ Call Stack ║ Web APIs / libuv ║ Task Queues ║
║ ║ ║ ║
║ main() ║ setTimeout(fn, 0) ║ ┌─────────────────────┐ ║
║ fetchUser() ║ fetch() → response ║ │ Microtask Queue (優先) │ ║
║ parseJSON() ║ addEventListener ║ │ Promise.then │ ║
║ ║ setInterval ║ │ Promise.catch │ ║
║ ║ I/O操作 ║ │ queueMicrotask │ ║
║ ║ ║ │ MutationObserver │ ║
║ ║ ║ └─────────────────────┘ ║
║ ║ ║ ║
║ ║ ║ ┌─────────────────────┐ ║
║ ║ ║ │ Macrotask Queue │ ║
║ ║ ║ │ setTimeout │ ║
║ ║ ║ │ setInterval │ ║
║ ║ ║ │ I/O callbacks │ ║
║ ║ ║ │ UIイベント │ ║
║ ║ ║ └─────────────────────┘ ║
╚══════════════╩══════════════════════╩═══════════════════════════╝
イベントループの正確な流れ:
1. Call Stackが空になる
2. Microtask Queueを全て処理(空になるまで繰り返す。再帰的マイクロタスクも処理)
3. ブラウザのみ: レンダリング更新(requestAnimationFrame → Layout → Paint)
4. Macrotask Queueから1つ取り出して実行(全部ではなく1つだけ!)
5. 1に戻る
▲ イベントループの全体像:コールスタック(LIFO)→ ヒープ(オブジェクト格納)→ Web APIs(非同期処理)→ マイクロタスクキュー(高優先・全件処理)→ マクロタスクキュー(低優先・1件ずつ処理)。矢印の順序がイベントループの1サイクル。
22-2. Promiseのマイクロタスクキューの位置
なぜPromise.thenはsetTimeoutより先に実行されるのか。その理由はマイクロタスクキューが「同期コードが終わった直後に全て処理される」からです。
// Promiseマイクロタスクの詳細な実行タイミング
console.log('① 同期: スクリプト開始')
setTimeout(() => {
console.log('⑦ マクロタスク: setTimeout')
}, 0)
// → Macrotask Queueに追加
fetch('/api') // ネットワーク要求
.then(() => console.log('⑤ Promise.then (fetch成功後)'))
// → ネットワーク応答後にMicrotask Queueに追加
Promise.resolve()
.then(() => {
console.log('③ Promise.then 1回目')
return Promise.resolve() // 新しいPromiseを返すと...
})
.then(() => {
console.log('④ Promise.then 2回目(前のthenの後)')
// → 前のthen完了後にMicrotask Queueに追加される
})
queueMicrotask(() => {
console.log('② queueMicrotask')
})
// → 直接Microtask Queueに追加
console.log('② 同期: スクリプト終了')
// 実際の出力:
// ① 同期: スクリプト開始
// ② 同期: スクリプト終了
// ② queueMicrotask
// ③ Promise.then 1回目
// ④ Promise.then 2回目
// (⑤ はfetchの完了タイミング次第)
// ⑦ マクロタスク: setTimeout
22-3. async/awaitの実行フロー
async/awaitはPromiseの糖衣構文です。実行フローを理解すると「なぜawaitの後のコードが後で実行されるのか」が分かります。
async function main() {
console.log('A: async関数の開始(同期)')
await Promise.resolve() // ← ここで中断(マイクロタスクとして再スケジュール)
// awaitの後はMicrotaskとして再開する
console.log('C: awaitの後(マイクロタスク)')
}
main()
console.log('B: main()呼び出しの後(同期)')
// 出力:
// A: async関数の開始(同期)
// B: main()呼び出しの後(同期)
// C: awaitの後(マイクロタスク)
// 理由:
// main() が呼ばれる → Aを出力 → awaitで中断(Microtask Queueに追加)
// → main()の呼び出しから戻る → Bを出力
// → 同期コード終了 → Microtask Queueを処理 → Cを出力
// より複雑な例
async function fetchUserData(id) {
const response = await fetch(`/api/users/${id}`) // ① 中断
const user = await response.json() // ② 中断
return user
}
async function main() {
console.log('1: 開始')
fetchUserData(1) // Promiseが返るがawaitsしない
.then(user => console.log('6: ユーザー:', user))
console.log('2: fetchUserData呼び出し後')
await new Promise(resolve => setTimeout(resolve, 0)) // ③ マクロタスクまで待つ
console.log('5: setTimeout後')
}
main()
console.log('3: main()後')
// setTimeoutコールバック実行
// console.log('4: setTimeout')
22-4. Node.jsのprocess.nextTickの位置
Node.jsでは process.nextTick がPromise.thenよりも前に実行されます。
// Node.jsのみ
setImmediate(() => console.log('D: setImmediate(Macrotask)'))
setTimeout(() => console.log('E: setTimeout(Macrotask)'), 0)
Promise.resolve().then(() => console.log('C: Promise.then(Microtask)'))
process.nextTick(() => console.log('B: nextTick(最優先Microtask)'))
console.log('A: 同期')
// 出力(Node.js):
// A: 同期
// B: nextTick(最優先Microtask)← Promise.thenより先!
// C: Promise.then(Microtask)
// D: setImmediate or E: setTimeout(順序は不定)
Node.jsの優先順序:
① 同期コード
② process.nextTick(nextTick Queue)← 独自の最優先キュー
③ Promise.then / queueMicrotask(Microtask Queue)
④ setTimeout(fn, 0) / setImmediate(Macrotask Queue)
22-5. まとめ:実行順序チートシート
同期コード → process.nextTick → Promise.then → queueMicrotask
↓ ↓
(Call Stack) (Microtask Queue - 全て処理)
↓
レンダリング更新(ブラウザのみ)
↓
setTimeout / setInterval / I/O
(Macrotask Queue - 1つだけ)
↓
またMicrotask Queueを全て処理
...ループ
23. 実コード付きミニチュートリアル
このセクションでは、本ガイドで学んだJavaScriptの概念を実際のコードで統合します。フィルタリング機能付きタスク管理アプリを段階的に構築し、各ステップで使っている概念を解説します。
23-1. 完成形のイメージ
作るもの: フィルタリング + ローカルストレージ永続化付きタスク管理アプリ
┌─────────────────────────────────────────┐
│ タスク管理 │
│ │
│ [タスクを入力... ] [追加] │
│ │
│ [全て] [未完了] [完了済み] │
│ │
│ ☐ 設計書を書く [削除] │
│ ☑ ~~ミーティング準備~~ [削除] │
│ ☐ コードレビュー [削除] │
│ │
│ 3件中2件が未完了 │
└─────────────────────────────────────────┘
使用する概念:
- クラスとプライベートフィールド
- クロージャとモジュールパターン
- イミュータブルな配列操作
- ESM(import/export)
- イベント処理
- LocalStorage
- カスタムエラー
23-2. Step 1: プロジェクト構造の設計
task-app/
├── index.html
├── style.css
├── app.js ← エントリーポイント
├── store.js ← 状態管理(TaskStoreクラス)
├── render.js ← UI描画
└── storage.js ← LocalStorageラッパー
設計方針:
store.js: ビジネスロジックと状態管理(UIに依存しない)render.js: DOM操作(Storeの状態を受け取って表示)storage.js: 永続化ロジックを分離(テストしやすい)
23-3. Step 2: 永続化レイヤー(storage.js)
// storage.js
// LocalStorageのラッパー(型安全でエラーに強い)
export class Storage {
#key
constructor(key) {
this.#key = key
}
save(data) {
try {
localStorage.setItem(this.#key, JSON.stringify(data))
} catch (error) {
// ストレージ容量超過など
console.warn('ストレージへの保存に失敗:', error)
}
}
load() {
try {
const raw = localStorage.getItem(this.#key)
return raw ? JSON.parse(raw) : null
} catch (error) {
// JSON.parseエラーなど
console.warn('ストレージからの読み込みに失敗:', error)
return null
}
}
clear() {
localStorage.removeItem(this.#key)
}
}
ポイント: try/catch でLocalStorageが使えない環境(プライベートブラウジングの一部)にも対応しています。
23-4. Step 3: 状態管理(store.js)
// store.js
// タスクの状態管理(イミュータブルパターン + Observerパターン)
import { Storage } from './storage.js'
// カスタムエラー
export class TaskError extends Error {
constructor(message, code) {
super(message)
this.name = 'TaskError'
this.code = code
}
}
// フィルタの種類
export const FILTER = {
ALL: 'all',
ACTIVE: 'active',
COMPLETED: 'completed'
}
export class TaskStore {
// プライベートフィールド
#tasks = []
#filter = FILTER.ALL
#listeners = new Set()
#storage
constructor(storageKey = 'tasks') {
this.#storage = new Storage(storageKey)
this.#loadFromStorage()
}
// ゲッター: フィルタ済みタスクを返す
get filteredTasks() {
switch (this.#filter) {
case FILTER.ACTIVE:
return this.#tasks.filter(t => !t.done)
case FILTER.COMPLETED:
return this.#tasks.filter(t => t.done)
default:
return [...this.#tasks] // コピーを返す
}
}
get allTasks() {
return [...this.#tasks]
}
get filter() {
return this.#filter
}
get stats() {
const total = this.#tasks.length
const completed = this.#tasks.filter(t => t.done).length
const active = total - completed
return { total, completed, active }
}
// タスクの追加(イミュータブル)
add(text) {
const trimmed = text.trim()
if (!trimmed) {
throw new TaskError('タスクのテキストは必須です', 'EMPTY_TEXT')
}
if (trimmed.length > 200) {
throw new TaskError('タスクは200文字以内で入力してください', 'TEXT_TOO_LONG')
}
const task = {
id: crypto.randomUUID(), // ユニークID
text: trimmed,
done: false,
createdAt: new Date().toISOString()
}
this.#tasks = [...this.#tasks, task] // イミュータブルな追加
this.#save()
this.#notify()
return task
}
// 完了/未完了の切り替え(mapでイミュータブルに)
toggle(id) {
this.#tasks = this.#tasks.map(t =>
t.id === id ? { ...t, done: !t.done } : t
)
this.#save()
this.#notify()
}
// テキストの編集
edit(id, newText) {
const trimmed = newText.trim()
if (!trimmed) {
throw new TaskError('タスクのテキストは必須です', 'EMPTY_TEXT')
}
this.#tasks = this.#tasks.map(t =>
t.id === id ? { ...t, text: trimmed } : t
)
this.#save()
this.#notify()
}
// タスクの削除(filterでイミュータブルに)
remove(id) {
this.#tasks = this.#tasks.filter(t => t.id !== id)
this.#save()
this.#notify()
}
// 完了済みをまとめて削除
clearCompleted() {
this.#tasks = this.#tasks.filter(t => !t.done)
this.#save()
this.#notify()
}
// フィルタの変更
setFilter(filter) {
if (!Object.values(FILTER).includes(filter)) {
throw new TaskError(`不正なフィルタ: ${filter}`, 'INVALID_FILTER')
}
this.#filter = filter
this.#notify()
}
// Observerパターン
subscribe(listener) {
this.#listeners.add(listener)
// unsubscribe関数を返す(クロージャでlistenerを保持)
return () => this.#listeners.delete(listener)
}
// プライベートメソッド
#notify() {
// 全リスナーに最新の状態を通知
this.#listeners.forEach(fn => fn({
tasks: this.filteredTasks,
filter: this.#filter,
stats: this.stats
}))
}
#save() {
this.#storage.save(this.#tasks)
}
#loadFromStorage() {
const saved = this.#storage.load()
if (Array.isArray(saved)) {
this.#tasks = saved
}
}
}
23-5. Step 4: UI描画(render.js)
// render.js
// DOM操作のみを担当(Storeの状態を受け取って表示する)
import { FILTER, TaskError } from './store.js'
export function renderApp(container, store) {
// DOMツリーを構築
container.innerHTML = `
<div class="task-app">
<header>
<h1>タスク管理</h1>
<div class="add-form">
<input type="text" class="task-input" placeholder="タスクを入力...">
<button class="add-btn">追加</button>
</div>
</header>
<nav class="filter-bar">
<button data-filter="${FILTER.ALL}" class="filter-btn active">全て</button>
<button data-filter="${FILTER.ACTIVE}" class="filter-btn">未完了</button>
<button data-filter="${FILTER.COMPLETED}" class="filter-btn">完了済み</button>
</nav>
<main>
<ul class="task-list"></ul>
</main>
<footer>
<span class="stats"></span>
<button class="clear-btn">完了済みを削除</button>
</footer>
</div>
`
// 要素の参照を取得
const input = container.querySelector('.task-input')
const addBtn = container.querySelector('.add-btn')
const taskList = container.querySelector('.task-list')
const statsEl = container.querySelector('.stats')
const clearBtn = container.querySelector('.clear-btn')
const filterBtns = container.querySelectorAll('.filter-btn')
// タスク追加
function handleAdd() {
try {
store.add(input.value)
input.value = ''
input.focus()
} catch (error) {
if (error instanceof TaskError) {
alert(error.message)
} else {
throw error
}
}
}
addBtn.addEventListener('click', handleAdd)
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') handleAdd()
})
// フィルタボタン(イベント委譲)
container.querySelector('.filter-bar').addEventListener('click', (e) => {
const btn = e.target.closest('[data-filter]')
if (!btn) return
store.setFilter(btn.dataset.filter)
})
// 完了済み削除
clearBtn.addEventListener('click', () => store.clearCompleted())
// 描画関数
function render({ tasks, filter, stats }) {
// タスクリストを再描画
taskList.innerHTML = ''
if (tasks.length === 0) {
taskList.innerHTML = '<li class="empty">タスクがありません</li>'
} else {
tasks.forEach(task => {
taskList.append(createTaskItem(task))
})
}
// 統計
statsEl.textContent = `${stats.total}件中${stats.active}件が未完了`
// フィルタボタンのアクティブ状態
filterBtns.forEach(btn => {
btn.classList.toggle('active', btn.dataset.filter === filter)
})
// 完了済みがない場合は削除ボタンを非表示
clearBtn.style.display = stats.completed > 0 ? 'block' : 'none'
}
function createTaskItem(task) {
const li = document.createElement('li')
li.className = `task-item ${task.done ? 'done' : ''}`
li.dataset.id = task.id
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.checked = task.done
checkbox.addEventListener('change', () => store.toggle(task.id))
const label = document.createElement('label')
label.textContent = task.text
// ダブルクリックで編集モード
label.addEventListener('dblclick', () => {
const newText = prompt('タスクを編集:', task.text)
if (newText !== null) {
try {
store.edit(task.id, newText)
} catch (error) {
if (error instanceof TaskError) alert(error.message)
}
}
})
const deleteBtn = document.createElement('button')
deleteBtn.textContent = '削除'
deleteBtn.className = 'delete-btn'
deleteBtn.addEventListener('click', () => store.remove(task.id))
li.append(checkbox, label, deleteBtn)
return li
}
// Storeを購読
const unsubscribe = store.subscribe(render)
// 初期描画
render({
tasks: store.filteredTasks,
filter: store.filter,
stats: store.stats
})
// クリーンアップ関数を返す
return () => unsubscribe()
}
23-6. Step 5: エントリーポイント(app.js)とHTML
// app.js
import { TaskStore } from './store.js'
import { renderApp } from './render.js'
// DOMが準備できてから実行
document.addEventListener('DOMContentLoaded', () => {
const store = new TaskStore('my-tasks')
const cleanup = renderApp(document.getElementById('app'), store)
// ページ離脱時のクリーンアップ(必要な場合)
window.addEventListener('beforeunload', cleanup)
})
<!-- index.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>タスク管理</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 40px auto; padding: 0 16px; }
.task-app { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
header { padding: 16px; border-bottom: 1px solid #eee; }
h1 { margin: 0 0 12px; }
.add-form { display: flex; gap: 8px; }
.task-input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.add-btn { padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; }
.filter-bar { display: flex; gap: 8px; padding: 12px 16px; border-bottom: 1px solid #eee; }
.filter-btn { padding: 4px 12px; border: 1px solid #ddd; border-radius: 16px; background: white; cursor: pointer; }
.filter-btn.active { background: #4CAF50; color: white; border-color: #4CAF50; }
.task-list { list-style: none; padding: 0; margin: 0; }
.task-item { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-bottom: 1px solid #eee; }
.task-item.done label { text-decoration: line-through; color: #999; }
.task-item label { flex: 1; cursor: pointer; }
.delete-btn { padding: 4px 8px; background: #ff5252; color: white; border: none; border-radius: 4px; cursor: pointer; }
footer { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; }
.stats { color: #666; font-size: 14px; }
.clear-btn { background: none; border: 1px solid #ddd; padding: 4px 8px; cursor: pointer; border-radius: 4px; }
.empty { padding: 32px; text-align: center; color: #999; }
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="./app.js"></script>
</body>
</html>
23-7. このチュートリアルで学んだこと
このアプリを通じて、以下の概念が実際にどう使われるかを見ました:
| 概念 | 使用箇所 | 読み方 |
|---|---|---|
| クラスとプライベートフィールド | TaskStore の #tasks, #listeners |
状態の隠蔽 |
| イミュータブルな配列操作 | map, filter, スプレッド演算子 |
変更前後を明確にする |
| クロージャ | subscribe のunsubscribe関数 |
状態を閉じ込める |
| カスタムエラー | TaskError extends Error |
失敗の種類を表す |
| ESM | import/export |
依存関係を明示する |
| イベント委譲 | .filter-bar のクリックイベント |
DOMイベントを集約する |
| LocalStorage | Storage クラス |
永続化境界を分ける |
| Observerパターン | #listeners と #notify() |
状態変更を通知する |
次のステップ: このアプリを拡張してみましょう。
- [ ] タスクの優先度(高/中/低)を追加する
- [ ]
fetchでサーバーAPIと同期する - [ ] Web Workerでタスクのソートを並列処理する
- [ ] Vitestでユニットテストを書く
24. 学習ロードマップ(30日)
このセクションでは、JavaScriptをゼロから体系的に学ぶための30日プランを提供します。各週に具体的な目標と確認事項を設けています。
Week 1: 言語の基礎(Day 1〜7)
| 日 | 内容 | 確認事項 |
|---|---|---|
| 1-2 | 変数(let/const)、型、型変換、=== vs == |
8種類の型を説明できる? falsy値を全て言える? |
| 3-4 | 関数(宣言・式・アロー)、スコープ、this |
3種類の関数の違いは? thisがどう変わるか説明できる? |
| 5-7 | 配列・オブジェクト、分割代入、スプレッド | map/filter/reduceを使いこなせる? |
Week 1のチェックリスト:
- [ ]
0.1 + 0.2 === 0.3がfalseになる理由を説明できる - [ ]
constとletの違いと、TDZを説明できる - [ ] 通常関数とアロー関数の
thisの違いを説明できる - [ ]
map/filter/reduceを使ってデータ変換ができる - [ ] 分割代入と スプレッド演算子を使いこなせる
Week 2: 非同期とモジュール(Day 8〜14)
| 日 | 内容 | 確認事項 |
|---|---|---|
| 8-10 | コールバック → Promiseの3状態と消費 | Promiseのチェーン(.thenの連鎖)を書ける? |
| 11-12 | async/await、エラーハンドリング、AbortController | try/catchの設計ができる? |
| 13-14 | ESモジュール(import/export)、クロージャ | named exportとdefault exportの違いは? |
Week 2のチェックリスト:
- [ ] コールバック地獄をPromiseで書き直せる
- [ ]
Promise.all/Promise.allSettledの違いを説明できる - [ ]
async/awaitが内部でPromiseを使っていることを理解している - [ ] マイクロタスクとマクロタスクの実行順序を説明できる
- [ ] ESMのnamed/default export/importを使い分けられる
- [ ] クロージャを使ったカプセル化ができる
Week 3: 応用(Day 15〜21)
| 日 | 内容 | 確認事項 |
|---|---|---|
| 15-17 | クラス、プロトタイプチェーン、継承 | クラスがプロトタイプの糖衣構文であることを理解している? |
| 18-19 | イテレータ、ジェネレータ | ジェネレータを使った無限シーケンスを作れる? |
| 20-21 | WeakMap/WeakSet、Proxy基礎 | WeakMapとMapの違いは? |
Week 3のチェックリスト:
- [ ]
classを使って継承とsuperを実装できる - [ ] プライベートフィールド(
#field)と通常フィールドの違いを説明できる - [ ]
function*とyieldでジェネレータを書ける - [ ]
Symbol.iteratorを使ってカスタムイテラブルを作れる - [ ] WeakMapを使ったメタデータパターンを実装できる
Week 4: 実用・品質(Day 22〜30)
| 日 | 内容 | 確認事項 |
|---|---|---|
| 22-24 | イベントループ、Reflow/Repaint、Web Workers | DebounceとThrottleの違いは? |
| 25-26 | セキュリティ(XSS、プロトタイプ汚染)、エラー設計 | XSSの原因と対策を説明できる? |
| 27-30 | テスト(Vitest)、第23章のチュートリアルを完成させる | モックを使ったテストを書ける? |
Week 4のチェックリスト:
- [ ] マイクロタスク/マクロタスクの実行順序を図で説明できる
- [ ] Reflowを引き起こすDOM操作と引き起こさない操作を区別できる
- [ ] Web Workerを使って重い処理をオフロードできる
- [ ]
innerHTMLの危険性と安全な代替手段を説明できる - [ ] カスタムエラークラスを設計し、テストを書ける
- [ ] 第23章のタスク管理アプリを拡張できる
30日後の到達目標
できるようになること:
✅ JavaScriptの型システムと型変換を完全に理解
✅ 非同期処理(Promise/async/await)を自在に書ける
✅ クロージャ・スコープ・プロトタイプを説明できる
✅ イベントループの動作を図で説明できる
✅ XSS・プロトタイプ汚染を防ぐコードが書ける
✅ Vitestでユニットテストが書ける
次のステップ:
→ TypeScript(静的型付けで大規模開発に対応)
→ React/Vue(コンポーネントベースUI)
→ Node.js(サーバーサイドJS)
→ Jest/Vitestの深掘り(TDD)
25. 用語集
このセクションでは本ガイドで登場した重要な用語をまとめます。面接や技術的なディスカッションで使える説明を意識しています。
| 用語 | 一言説明 | 詳細セクション |
|---|---|---|
| ホイスティング(Hoisting) | 宣言が先頭に巻き上げられる挙動。varと関数宣言で発生。let/constはTDZ |
本文参照 |
| TDZ(Temporal Dead Zone) | let/constが宣言前にアクセスできない禁止ゾーン |
本文参照 |
| クロージャ(Closure) | 関数が定義されたスコープへの参照を保持する仕組み | 本文参照 |
| プロトタイプチェーン | プロパティ・メソッドを親オブジェクトから継承する仕組み | 本文参照 |
| イベントループ(Event Loop) | Call Stackが空になったらQueueからタスクを取り出す仕組み | 本文参照 |
| マクロタスク | setTimeout, setInterval, I/O, UIイベントなどのタスク | 本文参照 |
| マイクロタスク | Promise.then, queueMicrotaskなど(マクロタスクより優先) | 本文参照 |
| GC(Garbage Collection) | 到達不能なオブジェクトのメモリを自動解放する仕組み | 本文参照 |
| 純粋関数(Pure Function) | 同じ引数→常に同じ結果、副作用なしの関数 | 本文参照 |
| 副作用(Side Effect) | 関数の外部状態を変える処理(I/O、状態変更、ネットワーク等) | 本文参照 |
| イミュータブル(Immutable) | 変更不可。変更時は新オブジェクトを生成するパターン | 本文参照 |
| TC39 | ECMAScript仕様を管理する委員会。年1回リリース | 本文参照 |
| JIT(Just-In-Time) | 実行時にホットな関数をネイティブコードに最適化するコンパイル | 本文参照 |
| AST(Abstract Syntax Tree) | コードの構造を木構造で表現したもの。パーサが生成 | 本文参照 |
| TDZ | Temporal Dead Zone。let/const宣言前の禁止ゾーン |
本文参照 |
| スコープチェーン | 変数を内側から外側へ順に探す仕組み | 本文参照 |
| Reflow | 要素のサイズ・位置変更に伴うレイアウト再計算(高コスト) | 本文参照 |
| Repaint | 見た目の変更(色等)に伴う再描画(Reflowより低コスト) | 本文参照 |
| Web Worker | メインスレッドと独立したスレッドでJSを実行する仕組み | 本文参照 |
| Babel | 新しいJS構文を古いブラウザ向けに変換するトランスパイラ | - |
| Polyfill | 新しいAPIを古い環境で模倣する実装 | - |
| Tree Shaking | 使われていないコードをバンドル時に除去する最適化 | - |
| XSS(Cross-Site Scripting) | ユーザー入力を通じて悪意あるスクリプトを注入する攻撃 | 本文参照 |
| プロトタイプ汚染 | __proto__等を通じてObject.prototypeを改ざんする攻撃 |
本文参照 |
| デバウンス(Debounce) | 連続する呼び出しの最後だけ実行するパターン | 本文参照 |
| スロットル(Throttle) | 一定時間に1回だけ実行を制限するパターン | 本文参照 |
| メモ化(Memoization) | 同じ引数の計算結果をキャッシュして再計算を省くパターン | 本文参照 |
| イテラブル(Iterable) | Symbol.iteratorメソッドを持ち、for...ofで反復できるオブジェクト |
本文参照 |
| ジェネレータ(Generator) | function*とyieldで一時停止・再開できる関数 |
本文参照 |
| WeakMap | キーがオブジェクトのみ、GCを妨げない弱い参照を持つMap | 本文参照 |
| Proxy | オブジェクトの基本操作をインターセプトできる仕組み | 本文参照 |
| ESM(ECMAScript Modules) | import/export構文による現代のモジュールシステム |
本文参照 |
| CJS(CommonJS) | require/module.exportsによるNode.js旧来のモジュールシステム |
本文参照 |
| AbortController | fetchなどの非同期処理をキャンセルするための仕組み |
本文参照 |
公式リンク
- MDN Web Docs - JavaScript
- ECMAScript仕様
- TC39 Proposals
- Can I use - ブラウザ対応状況の確認
- Node.jsドキュメント
- Vitestドキュメント
- JavaScript.info - 深い解説の英語リソース
まとめ
JavaScriptは、ブラウザとサーバーの両方で動く実行環境、イベントループ、プロトタイプ、非同期処理、モジュールシステムを理解すると見通しが良くなります。柔軟さが大きい分、型、状態、依存、セキュリティ境界を明示し、必要に応じてTypeScriptやテストで補強することが重要です。