JavaScript

JavaScript詳解ガイド|型・関数・クラス・非同期処理を実例で理解する

この記事でわかること

MDNを参考にJavaScriptの型・比較・クロージャ・参照・クラス・モジュール・非同期処理を詳しく解説。実行結果つきコード、境界条件の練習、公式資料で仕組みを学べます。

JavaScriptは、値を変換し、関数で処理を組み立て、イベントや通信の結果に応じてアプリケーションを動かす言語です。このページでは、基本構文を読める段階から「なぜその結果になるのか」を説明できる段階へ進むために、型、スコープ、オブジェクト、クラス、非同期処理を順に扱います。

MDNのJavaScriptページと各トピックの公式資料を参照し、このサイト独自の例と練習問題を用意しました。言語仕様のすべてを網羅するリファレンスではなく、実装で迷いやすい仕組みをつなげて理解するための学習ガイドです。

1. 学習の進め方と実行環境

目的に合わせて読む

  • 初めてコードを書く方は、変数・条件分岐・繰り返しの入門で基本構文を試してから、このページの型と比較へ進んでください。
  • データ加工を覚えたい方は、配列・オブジェクトの章と配列操作の実践記事を組み合わせてください。
  • Reactのコードで関数や状態が分かりにくい方は、スコープ、クロージャ、オブジェクトの参照を確認してください。
  • API通信の不具合を調べたい方は、実行順序、非同期処理、エラー処理の章を読んでください。
  • クラス設計に進みたい方は、プロトタイプの章から並列タスクキューの実装へ進んでください。

JavaScript本体と実行環境のAPIを区別する

ArrayMapPromiseなどは言語の標準機能です。一方、documentやDOMイベントはブラウザーが提供するAPIです。Node.jsではDOMをそのまま利用できません。fetchはブラウザーや対応するNode.jsで利用できますが、実行環境やバージョンの確認が必要です。

以下の言語機能の例は、開発者ツールのコンソール、またはNode.jsで実行できます。各コードブロックは別の例として扱ってください。同じコンソールでconstclassを繰り返し宣言してエラーになる場合は、ページを再読み込みするか、別ファイルで実行します。DOMの例はブラウザー用、モジュールの例は指定した複数ファイル用です。

2. 型・数値変換・入力値の検証

プリミティブとオブジェクト

プリミティブにはundefinednullbooleannumberbigintstringsymbolがあります。配列や関数はオブジェクトに含まれます。変数の型が固定されるわけではなく、その時点で入っている値が型を持ちます。

typeof nullは歴史的な理由で"object"です。nullの判定にはvalue === null、配列にはArray.isArray(value)を使います。外部から受け取った値を調べるとき、typeof value === "object"だけでは配列やnullを除外できません。

文字列を数値にするときは空文字も検証する

フォームの入力値は通常文字列です。Number("")0になり、parseInt("12px", 10)12になります。入力全体が正しい数量であることを求めるなら、変換前の形式と変換後の範囲を両方調べます。

function parseQuantity(text) {
  if (typeof text !== "string" || !/^\d+$/.test(text.trim())) {
    throw new TypeError("数量は数字だけで入力してください");
  }
  const quantity = Number(text.trim());
  if (!Number.isSafeInteger(quantity) || quantity < 1) {
    throw new RangeError("数量は1以上の安全な整数にしてください");
  }
  return quantity;
}

console.log(parseQuantity(" 12 ")); // 12
console.log(Number("")); // 0
console.log(Number.isNaN(Number("12px"))); // true

この例は半角数字の正の整数だけを受け付けます。"1e3""+2"も拒否する仕様です。Numberの安全な整数の上限は2 ** 53 - 1です。大きな整数を正確に扱うBigIntもありますが、通常の数値との算術演算には明示的な型の整理が必要です。小数の丸め誤差が問題になる金額計算では、最小通貨単位の整数など、業務要件に合う表現を決めてください。

参照:MDN:JavaScriptのデータ構造

3. 比較・真偽値・既定値の違い

比較方法は目的で選ぶ

通常の値の比較には、暗黙の型変換をしない===を使うと判断しやすくなります。ただしNaN === NaNfalseです。Object.is()ではNaN同士が等しく、0-0が異なります。includes()Setは、NaN同士と0-0をそれぞれ同じと扱う比較を使います。

オブジェクトの===は、中のプロパティが一致するかではなく、同じオブジェクトを参照しているかを調べます。

console.log(10 === "10"); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(0, -0)); // false
console.log([NaN].includes(NaN)); // true
console.log({ id: 1 } === { id: 1 }); // false

const settings = { pageSize: 0, label: "", enabled: false };
console.log(settings.pageSize || 20); // 20
console.log(settings.pageSize ?? 20); // 0
console.log(settings.label ?? "未設定"); // 空文字のまま
console.log(settings.profile?.name ?? "ゲスト"); // ゲスト

||??は代用できない

||0、空文字、falseなどの偽と評価される値も既定値へ置き換えます。??が置き換えるのはnullundefinedだけです。設定値の0を有効な値として残すなら??が適しています。?.は途中の値がnullundefinedのときに参照を止めますが、データ形式を検証する機能ではありません。

参照:MDN:等価性の比較と同一性

4. スコープ・クロージャ・関数の寿命

constはオブジェクトを凍結しない

letconstはブロックスコープを持ちます。constが禁止するのは変数への再代入です。参照先の配列へのpush()やオブジェクトのプロパティ変更まで禁止するわけではありません。

宣言前のletconstを参照するとReferenceErrorになります。関数宣言と同じ感覚で、後ろで初期化する変数を先に使わないようにしてください。

クロージャで呼び出しごとの状態を保持する

内側の関数は、作成時の外側のスコープにある変数を参照できます。外側の関数の実行が終わっても、その変数を必要とする関数が残っていれば、後から利用できます。

function createSequence(prefix) {
  let current = 0;
  return function next() {
    current += 1;
    return `${prefix}-${current}`;
  };
}

const nextOrder = createSequence("order");
const nextInvoice = createSequence("invoice");
console.log(nextOrder()); // order-1
console.log(nextOrder()); // order-2
console.log(nextInvoice()); // invoice-1

nextOrdernextInvoiceは別々のcurrentを保持します。これはブラウザー内の小さな連番には使えますが、再起動や複数端末をまたぐ一意なIDには使えません。また、クロージャは変数をその時点の値で固定する機能ではありません。共有する変数が更新されれば、後の呼び出しは更新後の値を参照します。

詳しい分割方法は関数・スコープ・モジュールの実践、仕組みはMDN:クロージャを参照してください。

5. 配列・オブジェクトと変更の影響範囲

スプレッド構文のコピーは浅い

{ ...object }[...array]は、内側のオブジェクトまで再帰的に複製しません。元データを保ってネストした値を変更するには、変更箇所までの各階層を作り直します。

const original = {
  profile: { name: "Aki", city: "Tokyo" },
  tags: ["javascript"],
};
const shallow = { ...original };
console.log(shallow.profile === original.profile); // true

const updated = {
  ...original,
  profile: { ...original.profile, city: "Osaka" },
  tags: [...original.tags, "web"],
};
console.log(original.profile.city); // Tokyo
console.log(updated.profile.city); // Osaka
console.log(updated.tags.length); // 2

配列操作は変換・選択・集計に分ける

map()は各要素の変換、filter()は条件に合う要素の抽出、reduce()は集計に使います。返される配列が新しくても、コールバックで元の要素オブジェクトを書き換えると、元データへ影響します。sort()reverse()は配列自体を変更するため、元の順序を残すならコピーしてから使います。

const orders = [
  { id: "a", status: "paid", amount: 1200 },
  { id: "b", status: "pending", amount: 500 },
  { id: "c", status: "paid", amount: 800 },
];
const paid = orders.filter(order => order.status === "paid");
const total = paid.reduce((sum, order) => sum + order.amount, 0);
const amounts = paid.map(order => order.amount);
console.log(total); // 2000
console.log([...amounts].sort((a, b) => a - b)); // [800, 1200]
console.log(amounts); // [1200, 800]

任意のキーと値の対応にはMap、重複のない値の集合にはSetが使えます。Setへ同じ内容のオブジェクトを2つ渡しても、参照が異なれば2要素です。商品IDで重複を除くなど、何を同一とするかを先に決めてください。

参照:配列・オブジェクトの実践記事MDN:JavaScriptのデータ構造

6. プロトタイプ・クラス・this

インスタンスの値と共有メソッド

プロパティを参照すると、まずそのオブジェクト自身が調べられ、見つからなければプロトタイプの連鎖が調べられます。クラスの通常のインスタンスメソッドはClassName.prototypeに置かれます。インスタンスごとの状態と、共有する操作を分けて考えると理解しやすくなります。

class Stock {
  #quantity;

  constructor(quantity = 0) {
    if (!Number.isSafeInteger(quantity) || quantity < 0) {
      throw new RangeError("在庫数は0以上の安全な整数にしてください");
    }
    this.#quantity = quantity;
  }

  get quantity() {
    return this.#quantity;
  }

  reserve(count) {
    if (!Number.isSafeInteger(count) || count < 1) {
      throw new RangeError("予約数は1以上の安全な整数にしてください");
    }
    if (count > this.#quantity) {
      throw new RangeError("在庫が不足しています");
    }
    this.#quantity -= count;
    return this.#quantity;
  }
}

const stock = new Stock(5);
console.log(Object.getPrototypeOf(stock) === Stock.prototype); // true
console.log(Object.hasOwn(stock, "reserve")); // false
const reserve = stock.reserve.bind(stock);
console.log(reserve(2)); // 3
console.log(stock.quantity); // 3

#quantityはクラスの外から直接操作できないプライベートフィールドです。検証を通るメソッドに更新処理を集めることで、不正な状態を作りにくくしています。在庫不足で例外が発生した場合、減算の前に止まるため在庫は変わりません。ただしこのクラスだけで、複数サーバーから同時に行う実在庫の更新を排他制御できるわけではありません。

メソッドを取り出すとthisが変わる

stock.reserve(2)ではthisstockです。const reserve = stock.reserveとして単独で呼ぶと、この関係は失われます。例ではbind(stock)で結び直しました。アロー関数は自分専用のthisを持たず、外側のthisを参照します。単に通常の関数の短縮形と考えると間違えやすい点です。

extendsによる継承は、既存の型として扱える関係に使います。処理を再利用したいだけなら、別オブジェクトを受け取って呼ぶ構成も検討できます。クラス・継承の実践ではstaticsuper、シリアライズも扱っています。

参照:MDN:継承とプロトタイプチェーン

7. モジュールで責務と依存関係を分ける

名前付きエクスポートの最小構成

同じフォルダーにprice.mjsmain.mjsを作成します。Node.jsではnode main.mjsで実行できます。ブラウザーで使う場合はHTTPサーバーから配信し、type="module"を付けたscriptmain.mjsを読み込みます。

price.mjsは計算と入力検証を担当します。

export function subtotal(unitPrice, quantity) {
  if (!Number.isSafeInteger(unitPrice) || unitPrice < 0 ||
      !Number.isSafeInteger(quantity) || quantity < 0) {
    throw new RangeError("単価と数量は0以上の安全な整数にしてください");
  }
  const result = unitPrice * quantity;
  if (!Number.isSafeInteger(result)) {
    throw new RangeError("計算結果が安全な整数の範囲を超えました");
  }
  return result;
}

main.mjsは必要な機能を読み込んで利用します。

import { subtotal } from "./price.mjs";

console.log(subtotal(1200, 3)); // 3600

ファイル分割で注意すること

名前付きインポートの名前は、エクスポートされた名前と対応させます。ブラウザーの相対パスには拡張子を含めてください。モジュールは自動的にstrictモードで動き、トップレベルの変数がグローバル変数になることも防げます。

循環参照があると、相手側の初期化前に値を読んで失敗する場合があります。共通の計算を下位のモジュールへ移し、画面表示や通信から独立させると依存関係とテストが整理しやすくなります。

参照:MDN:JavaScriptモジュール

8. 非同期処理とイベントループ

Promiseの処理はいつ実行されるのか

ブラウザーでは、実行中の同期処理が終わった後に、Promiseの反応などのマイクロタスクが処理されます。setTimeout()のコールバックは、その後のタスクとして実行されます。次の例はブラウザーのコンソールで試してください。

console.log("1: start");
setTimeout(() => console.log("4: timer"), 0);
Promise.resolve().then(() => console.log("3: promise"));
console.log("2: end");
// 1: start → 2: end → 3: promise → 4: timer

setTimeout(..., 0)は即時実行の指定ではありません。実行までの時間は、他の処理や環境によって変わります。async関数にしただけで重い計算が別スレッドに移るわけでもありません。長い同期ループは画面操作を妨げるため、処理の分割やWeb Workerなどを検討します。

直列実行と並行実行を選ぶ

ある処理の結果を次の処理が必要とする場合は、順番にawaitします。独立した処理は、先に開始してPromise.all()で待つことができます。返る配列は完了順ではなく、渡した順です。

function waitFor(value, milliseconds) {
  return new Promise(resolve => {
    setTimeout(() => resolve(value), milliseconds);
  });
}

async function loadDashboard() {
  const [profile, messages] = await Promise.all([
    waitFor({ name: "Aki" }, 20),
    waitFor(["Hello"], 5),
  ]);
  return `${profile.name}: ${messages.length}件`;
}

loadDashboard().then(console.log).catch(console.error); // Aki: 1件

Promise.all()はどれか1つが拒否されると拒否されますが、他の処理を自動で中止しません。成功・失敗をすべて集める目的にはPromise.allSettled()があります。大量のリクエストをまとめて開始すると接続先へ負荷がかかるため、クラスで作る並列数制限付きタスクキューのように開始数を制御します。

参照:MDN:JavaScript実行モデル

9. 通信結果とエラーを境界で扱う

HTTPエラーとデータ形式のエラーを分ける

fetch()は、通常、HTTPの404500だけではPromiseを拒否しません。response.okを確認してからJSONを読み、必要なデータ形式も検証します。以下は通信処理を引数で置き換えられる例です。

async function fetchArticleTitle(url, { signal, fetcher = fetch } = {}) {
  const response = await fetcher(url, { signal });
  if (!response.ok) {
    throw new Error(`記事の取得に失敗しました: HTTP ${response.status}`);
  }
  const data = await response.json();
  if (data === null || typeof data !== "object" || Array.isArray(data) ||
      typeof data.title !== "string" || data.title.trim() === "") {
    throw new TypeError("記事のtitleが正しくありません");
  }
  return data.title;
}

const mockFetch = async () => ({
  ok: true,
  status: 200,
  json: async () => ({ title: "JavaScriptを学ぶ" }),
});
fetchArticleTitle("/api/article", { fetcher: mockFetch })
  .then(console.log)
  .catch(console.error); // JavaScriptを学ぶ

この例の呼び出しはモックを使うため実通信を行いません。実際のAPIに接続するときはfetcherを省略し、存在するURLを指定してください。response.json()自体も、不正なJSONなどで拒否される可能性があります。

中止と後始末を設計する

AbortControllersignalを渡すと、中止を通知できます。ただしクライアントが通信を中止しても、サーバーの処理や更新が取り消されるとは限りません。ローディング表示などの後始末はfinallyで行い、通信失敗と利用者が意図した中止を必要に応じて分けます。

catchで何もせず例外を消すと、呼び出し元が成功だと誤解します。扱える場所で表示・再試行を行うか、上位へ伝えてください。リクエストIDなど調査に必要な情報を残す一方、パスワードやトークンはログに含めないようにします。

実API、HTTPステータス、中止の具体例は非同期処理・fetchの実践、APIの仕様はMDN:フェッチAPIの使用を参照してください。

10. イテレーターとジェネレーター

必要になった値を順に生成する

ジェネレーター関数はfunction*で定義し、yieldで値を渡して中断できます。呼び出すとイテレーターが返り、for...ofなどで順番に値を受け取れます。

function* pageNumbers(total) {
  if (!Number.isSafeInteger(total) || total < 0) {
    throw new RangeError("ページ数は0以上の安全な整数にしてください");
  }
  let index = 0;
  while (index < total) {
    index += 1;
    yield index;
  }
}

const iterator = pageNumbers(3);
console.log(iterator.next()); // { value: 1, done: false }
console.log([...iterator]); // [2, 3]
console.log([...iterator]); // []
console.log([...pageNumbers(3)]); // [1, 2, 3]

遅延評価と再利用の注意点

配列を先にすべて作らず、必要な分だけ生成できます。ただしスプレッド構文で配列へ展開すれば、結局すべての値を保持します。無限に続くジェネレーターを展開すると終わりません。

同じイテレーターは消費した位置から再開します。最初から読み直したい場合は新しく作成してください。また、ジェネレーター内の検証は関数呼び出し時ではなく、最初に実行を進めるnext()などのタイミングで行われます。

参照:MDN:イテレーターとジェネレーター

11. 正規表現・日時・国際化

正規表現は入力仕様に合わせて使う

正規表現は文字列の検索・抽出・置換に使えます。入力全体の形式を確認したい場合、開始の^と終了の$も指定します。次の例はYYYY-MM形式の月を受け付けます。

function parseYearMonth(text) {
  if (typeof text !== "string") return null;
  const match = /^(\d{4})-(0[1-9]|1[0-2])$/.exec(text.trim());
  if (!match) return null;
  return { year: Number(match[1]), month: Number(match[2]) };
}

console.log(parseYearMonth("2026-09")); // { year: 2026, month: 9 }
console.log(parseYearMonth("2026-13")); // null

これは年と月の形式検査です。日付まで扱うなら、月ごとの日数やうるう年の検証も必要です。gyフラグを付けた正規表現はlastIndexを持ち、繰り返しのtest()exec()で結果が変わることがあります。単発の入力検証では、目的に不要なフラグを付けないようにします。

日時の保存と表示を分ける

日時を表すときは「ある瞬間」なのか「誕生日のような暦の日付」なのかを先に決めます。Dateである瞬間を扱う場合、タイムゾーンを含むISO形式などで入力を明確にし、表示はIntl.DateTimeFormatで地域とタイムゾーンを指定します。

const instant = new Date("2026-09-19T00:30:00Z");
const formatter = new Intl.DateTimeFormat("ja-JP", {
  timeZone: "Asia/Tokyo",
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
  hourCycle: "h23",
});
console.log(formatter.format(instant)); // 2026/09/19 09:30 相当の表示

区切りや空白は実装のロケールデータに依存するため、表示文字列をそのまま機械的な比較や保存に使わないでください。タイムゾーンを省略すると、実行環境の設定により日付や時刻が変わることがあります。

参照:MDN:正規表現MDN:Intl.DateTimeFormat

12. DOMイベントとメモリー管理

使い終わったイベントを解除する

JavaScriptは不要になったメモリーを自動回収しますが、参照が残っているオブジェクトが必ず不要だと判断されるわけではありません。イベントやタイマーが大きなデータを参照し続ける構成では、画面を閉じるときの後始末も必要です。

次のHTMLをページに置きます。

<button id="add-one" type="button">1増やす</button>
<output id="count" aria-live="polite">0</output>

要素の読み込み後に、次のスクリプトを実行します。AbortControllerで登録したイベントをまとめて解除します。

function mountCounter(button, output) {
  const controller = new AbortController();
  let count = 0;
  output.textContent = "0";
  button.addEventListener("click", () => {
    count += 1;
    output.textContent = String(count);
  }, { signal: controller.signal });
  return () => controller.abort();
}

const button = document.querySelector("#add-one");
const output = document.querySelector("#count");
if (!button || !output) throw new Error("カウンターの要素がありません");
const unmountCounter = mountCounter(button, output);
// この画面を取り外すタイミングで unmountCounter() を呼ぶ。

文字列の表示とHTMLの挿入を分ける

利用者の入力を単なる文字として表示する場合はtextContentを使います。未検証の文字列をinnerHTMLへ渡すと、意図しないHTMLを解釈させる危険があります。イベントを再登録するときは、以前の登録を解除する責任も決めてください。

この例はイベントの後始末を示すもので、DOM要素そのものを削除する処理ではありません。タイマーは対応するclearTimeout()clearInterval()、購読処理はライブラリーの解除関数など、各APIに合う方法で停止します。

参照:DOM・イベントの実践記事MDN:メモリー管理MDN:addEventListener()

13. 練習問題と不具合の調べ方

境界条件で理解を確認する

  • 数量変換:"""0""-1""12px"、安全な整数を超える文字列が拒否されることを確認してください。
  • 状態管理:Stockの在庫が3のときに4を予約しても、在庫が3のまま残ることを確認してください。
  • 非同期処理:モックのokfalseにし、HTTPエラーが呼び出し元に届くことを確認してください。次にtitleを数値へ変えて形式エラーを確認します。
  • イテレーター:pageNumbers(-1)を呼ぶだけの場合と、返った値のnext()を呼ぶ場合で、例外のタイミングを比べてください。
  • DOM:ボタンを2回押し、unmountCounter()を実行してからもう一度押します。表示が2のままなら解除できています。

エラーメッセージから調査範囲を絞る

ReferenceErrorなら変数名、スコープ、初期化順を確認します。TypeErrorなら、受け取った値の型、nullundefined、メソッドの呼び出し方を確認します。例外のスタックにある最初の自分のコードへ移動し、開発者ツールのブレークポイントで直前の値を観察してください。

期待値だけでなく、空配列、未入力、最大値、中止、失敗後の状態も確かめます。短い再現コードで原因を絞ってから、実際のアプリへ修正を戻すと判断しやすくなります。

14. 次に読む記事とリファレンスの使い方

手を動かして理解をつなげる

基本の確認にはJavaScript入門、関数の整理には関数・スコープ・モジュール、画面づくりにはDOM・イベントが対応しています。クラスと非同期処理を組み合わせる最終課題として、並列タスクキューに進んでください。

MDNで仕様と対応状況を確認する

MDNのガイドで仕組みを理解した後、個別のリファレンスで引数、戻り値、例外、破壊的な変更の有無、ブラウザー互換性を確認します。新しい機能を採用するときは、利用者のブラウザーやサーバーの実行環境で動くかを確かめてください。

MDN:JavaScriptを入口に、必要な項目を調べながら小さく実行してみる習慣が、入門から複雑な実装へ進む土台になります。

y.
WRITTEN BY

y_ymo10

SEの部屋で、JavaScript・TypeScript・React.js・Next.jsの開発ノートを公開しています。

ほかの開発ノートを読む →