← Node.js言語ページに戻る
🟢

Node.js言語 文法解説

Node.jsの基礎から、イベントループの内部動作、データ構造・アルゴリズムの実装まで、基本情報技術者試験に頻出するCS基礎トピックを交えて解説します。

文法解説

1. Node.jsとは(V8エンジン・シングルスレッド)

Node.jsはChromeのV8 JavaScriptエンジンを組み込み、ブラウザの外でJavaScriptを実行できるランタイムです。JavaScript自体はシングルスレッド(1本の実行スレッド)で動作しますが、ファイルI/Oやネットワーク通信などの重い処理は裏側のlibuvというライブラリがOSのスレッドプールや非同期APIに委譲し、完了したらコールバックとしてメインスレッドに結果を返します。これにより「1スレッドなのに大量の同時接続をさばける」という特性が生まれます。

SAMPLE
console.log("Node.jsのバージョン:", process.version);
console.log("実行プラットフォーム:", process.platform);
console.log("現在の作業ディレクトリ:", process.cwd());

2. モジュールシステム:CommonJSとESM

Node.jsには2種類のモジュール形式があります。従来からのCommonJSrequire / module.exports)は同期的に読み込まれ、拡張子.js.cjsで使われます。現在のJavaScript標準であるESM(ECMAScript Modules)import / exportを使い、非同期に読み込まれます。ESMを使うにはpackage.json"type": "module"を指定するか、拡張子を.mjsにします。両者はトップレベルのthisの扱いや、ファイル内で使える暗黙の変数(__dirnameなど)も異なります。

SAMPLE (CommonJS)
// math.js
function add(a, b) {
  return a + b;
}
module.exports = { add };

// index.js
const { add } = require("./math");
console.log(add(1, 2)); // 3
SAMPLE (ESM)
// math.mjs
export function add(a, b) {
  return a + b;
}

// index.mjs
import { add } from "./math.mjs";
console.log(add(1, 2)); // 3

3. npmとpackage.json・依存関係管理

npm(Node Package Manager)はNode.js標準のパッケージ管理ツールです。プロジェクトの情報や依存パッケージはpackage.jsonに記述し、実体はnode_modulesフォルダにインストールされます。バージョンは^(マイナー・パッチ更新を許容)や~(パッチ更新のみ許容)といった記法(セマンティックバージョニング)で管理し、実際にインストールされたバージョンはpackage-lock.jsonに固定されます。dependenciesは本番でも必要な依存、devDependenciesは開発時のみ必要な依存(テストツールなど)です。

SAMPLE
{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node index.js",
    "test": "node --test"
  },
  "dependencies": {
    "express": "^4.18.0"
  },
  "devDependencies": {
    "eslint": "^9.0.0"
  }
}

主なコマンド:npm install(依存関係のインストール)、npm install express(パッケージ追加)、npm run start(scriptsの実行)。

4. 非同期処理(1):コールバックとエラーファーストコールバック

Node.jsの標準APIの多くは「完了時に呼ばれる関数(コールバック)」を渡すスタイルで非同期処理を表現します。伝統的な作法として、コールバックの第1引数は必ずエラー(なければnull)、第2引数以降が結果という「エラーファーストコールバック」があります。複数の非同期処理をコールバックで連鎖させると、ネストが深くなる「コールバック地獄」が起きやすいという欠点があります。

SAMPLE
const fs = require("fs");

fs.readFile("config.json", "utf-8", (err, data) => {
  if (err) {
    console.error("読み込み失敗:", err.message);
    return;
  }
  console.log("読み込み成功:", data);
});

5. 非同期処理(2):PromiseとAsync/Await

Promiseは非同期処理の結果(成功=resolve / 失敗=reject)を表すオブジェクトで、.then() / .catch()でつなげてコールバック地獄を回避できます。さらにasync/awaitを使うと、非同期処理を同期処理のような見た目で書けます。async関数は常にPromiseを返し、内部でawaitを使うとPromiseの解決を待ってから次の行に進みます。複数の非同期処理を並行して実行したい場合はPromise.all()を使います。

SAMPLE
const fs = require("fs/promises");

async function loadConfigs() {
  try {
    const [a, b] = await Promise.all([
      fs.readFile("a.json", "utf-8"),
      fs.readFile("b.json", "utf-8"),
    ]);
    console.log(JSON.parse(a), JSON.parse(b));
  } catch (err) {
    console.error("読み込みエラー:", err.message);
  }
}

loadConfigs();

6. イベントループの仕組み

Node.jsのイベントループは複数の「フェーズ」を順番に巡回する仕組みです。主なフェーズは、期限切れのsetTimeout / setIntervalを実行するtimers、I/Oコールバックを実行するpollsetImmediateを実行するcheckなどです。各フェーズの間には「マイクロタスクキュー」の処理が挟まります。マイクロタスク(process.nextTickのキューと、Promiseの.thenなどのキュー)は、次のフェーズに進む前に必ず空になるまで実行され、process.nextTickはPromiseのマイクロタスクよりも優先度が高いです。一方setTimeout(fn, 0)setImmediateは「マクロタスク」でフェーズをまたいで実行されるため、実行順が異なります。

SAMPLE
console.log("1: 同期処理");

setTimeout(() => console.log("5: setTimeout(マクロタスク)"), 0);
setImmediate(() => console.log("6: setImmediate(マクロタスク)"));

Promise.resolve().then(() => console.log("3: Promise.then(マイクロタスク)"));
process.nextTick(() => console.log("2: process.nextTick(最優先)"));

console.log("4: 同期処理の続き");

// 実行順の目安:
// 1: 同期処理 → 4: 同期処理の続き → 2: nextTick → 3: Promise.then
// → 5: setTimeout / 6: setImmediate(順序は環境依存の場合あり)

7. EventEmitterによるイベント駆動プログラミング

Node.jsの多くの組み込みクラス(http.ServerやStreamなど)はeventsモジュールのEventEmitterを継承しています。on()でイベントを購読し、emit()でイベントを発行します。これにより「〇〇が起きたら△△する」という疎結合な設計がしやすくなります。

SAMPLE
const { EventEmitter } = require("events");

class OrderSystem extends EventEmitter {
  placeOrder(item) {
    console.log(`${item}を注文しました`);
    this.emit("ordered", item);
  }
}

const system = new OrderSystem();
system.on("ordered", (item) => {
  console.log(`[通知] ${item}の注文を受け付けました`);
});

system.placeOrder("キーボード");

8. ストリーム(Stream)の基礎

ストリームは、データをすべて一度にメモリへ読み込むのではなく「チャンク」と呼ばれる小さな単位で少しずつ処理する仕組みです。Readable(読み込み用)、Writable(書き込み用)、Duplex(両方向)、Transform(変換しながら中継)の4種類があり、大きなファイルの処理やネットワーク通信でメモリ効率と応答性を向上させます。

SAMPLE
const fs = require("fs");

const readStream = fs.createReadStream("bigfile.txt", { encoding: "utf-8" });
const writeStream = fs.createWriteStream("copy.txt");

readStream.on("data", (chunk) => {
  console.log("受信したチャンク:", chunk.length, "文字");
});

readStream.on("end", () => console.log("読み込み完了"));

// pipe()でReadableからWritableへ自動的に流し込む
readStream.pipe(writeStream);

9. ファイルシステムAPI(fsモジュール)

fsモジュールにはファイル操作用の関数が3系統あります。①fs.readFileSyncのような同期版(完了までスレッドを止める)、②fs.readFileのようなコールバック版の非同期、③fs/promisesPromise版の非同期です。サーバー用途では同期版はイベントループ全体を止めてしまうため、通常はPromise版+async/awaitを使います。

SAMPLE
const fsSync = require("fs");
const fsPromises = require("fs/promises");

// ① 同期版(スクリプトの起動時設定読み込みなど、限定的な用途向け)
const data1 = fsSync.readFileSync("config.json", "utf-8");

// ② コールバック版
fsSync.readFile("config.json", "utf-8", (err, data2) => {
  if (err) throw err;
  console.log(data2);
});

// ③ Promise版(推奨)
async function main() {
  const data3 = await fsPromises.readFile("config.json", "utf-8");
  console.log(data3);
}
main();

10. HTTPサーバーの構築

組み込みのhttpモジュールだけでも外部ライブラリなしにWebサーバーを立てられます。req.urlreq.methodでルーティングを自作し、res.writeHead()でステータスコードとヘッダーを設定、res.end()でレスポンスを返します。

SAMPLE
const http = require("http");

const server = http.createServer((req, res) => {
  if (req.method === "GET" && req.url === "/") {
    res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
    res.end("Hello, Node.js!");
  } else {
    res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
    res.end("Not Found");
  }
});

server.listen(3000, () => {
  console.log("http://localhost:3000 で待ち受け中");
});

11. データ構造の実装:連結リスト(Linked List)

連結リストは、各要素(ノード)が「値」と「次のノードへの参照」を持つデータ構造です。配列と違い要素の挿入・削除がノードの参照の付け替えだけで済むため先頭・中間への挿入・削除がO(1)〜O(n)で行えます(先頭挿入はO(1)、任意位置探索はO(n))。一方でインデックスによるランダムアクセスは先頭から辿る必要があるためO(n)かかります。

SAMPLE
class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
  }

  // 先頭に追加: O(1)
  addFirst(value) {
    const node = new Node(value);
    node.next = this.head;
    this.head = node;
  }

  // 末尾に追加: O(n)(末尾まで辿る必要がある)
  addLast(value) {
    const node = new Node(value);
    if (!this.head) {
      this.head = node;
      return;
    }
    let cur = this.head;
    while (cur.next) cur = cur.next;
    cur.next = node;
  }

  toArray() {
    const result = [];
    let cur = this.head;
    while (cur) {
      result.push(cur.value);
      cur = cur.next;
    }
    return result;
  }
}

const list = new LinkedList();
list.addLast(1);
list.addLast(2);
list.addFirst(0);
console.log(list.toArray()); // [0, 1, 2]

12. アルゴリズムと計算量(1):探索(線形探索・二分探索)

線形探索(Linear Search)は先頭から順に1つずつ比較する方法で、計算量はO(n)です。二分探索(Binary Search)は「ソート済み」の配列に対して、中央の値と比較し探索範囲を半分に絞り込んでいく方法で、計算量はO(log n)と非常に高速です。ただし二分探索は事前に配列がソートされている必要があります。

SAMPLE
// 線形探索: O(n)
function linearSearch(arr, target) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === target) return i;
  }
  return -1;
}

// 二分探索: O(log n)(配列はソート済みであること)
function binarySearch(arr, target) {
  let low = 0;
  let high = arr.length - 1;
  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) {
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }
  return -1;
}

const sorted = [1, 3, 5, 7, 9, 11, 13];
console.log(linearSearch(sorted, 7));  // 3
console.log(binarySearch(sorted, 7));  // 3

13. アルゴリズムと計算量(2):クイックソート

クイックソート(Quick Sort)は基準値(ピボット)より小さい要素と大きい要素に分割し、それぞれを再帰的にソートする分割統治法です。平均計算量はO(n log n)ですが、ピボットの選び方が悪い(常に最小・最大を選んでしまう等)と最悪計算量O(n²)になります。比較として、単純なバブルソート選択ソートは常にO(n²)です。

SAMPLE
// クイックソート: 平均O(n log n)、最悪O(n^2)
function quickSort(arr) {
  if (arr.length <= 1) return arr;

  const pivot = arr[Math.floor(arr.length / 2)];
  const left = arr.filter((v) => v < pivot);
  const mid = arr.filter((v) => v === pivot);
  const right = arr.filter((v) => v > pivot);

  return [...quickSort(left), ...mid, ...quickSort(right)];
}

console.log(quickSort([5, 3, 8, 1, 9, 2])); // [1, 2, 3, 5, 8, 9]

14. エラーハンドリング

Node.jsでは処理の種類によってエラーの扱い方が異なります。同期処理はtry/catch、Promiseベースの非同期処理は.catch()またはtry/catchawait、コールバックベースのAPIは前述の「エラーファーストコールバック」で処理します。また、独自のエラークラスをErrorを継承して作ることで、エラーの種類を判別しやすくできます。キャッチされなかった例外はprocess.on("uncaughtException", ...)で最終的に捕捉できますが、これはあくまで最終防衛ラインであり、通常は個別にエラー処理を書くべきです。

SAMPLE
class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

function checkAge(age) {
  if (age < 0) {
    throw new ValidationError("年齢は0以上である必要があります");
  }
  return age;
}

try {
  checkAge(-5);
} catch (err) {
  if (err instanceof ValidationError) {
    console.error("入力エラー:", err.message);
  } else {
    throw err; // 想定外のエラーは再スロー
  }
}

15. 環境変数とプロセス管理

processオブジェクトはNode.jsの実行プロセスに関する情報・操作を提供するグローバルオブジェクトです。process.envで環境変数(APIキーやDB接続情報などの設定値)にアクセスでき、本番/開発環境で設定を切り替える際によく使われます。process.argvでコマンドライン引数、process.exit(code)でプロセスの終了、process.on("SIGINT", ...)でCtrl+Cなどのシグナル処理ができます。

SAMPLE
const port = process.env.PORT || 3000;
console.log(`ポート ${port} で起動します`);

console.log("コマンドライン引数:", process.argv.slice(2));

process.on("SIGINT", () => {
  console.log("終了シグナルを受信。後片付けをして終了します。");
  process.exit(0);
});

16. V8のメモリ管理とガベージコレクション

Node.js(V8)はメモリを「ヒープ」上に確保し、不要になったオブジェクトをガベージコレクション(GC)が自動的に解放します。V8のGCは世代別方式を採用しており、生成されたばかりの短命なオブジェクトを扱うNew Space(Scavenge)と、生き残った長命のオブジェクトを扱うOld Space(Mark-Sweep-Compact)に分けて効率よく回収します。開発者が明示的にメモリを解放するコードは通常書きませんが、不要になったオブジェクトへの参照(グローバル変数や解除し忘れたイベントリスナーなど)を残したままにすると「メモリリーク」が発生し、GCが回収できずメモリ使用量が増え続けます。

SAMPLE
// メモリ使用量を確認する
const usage = process.memoryUsage();
console.log(`ヒープ使用量: ${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`);

// メモリリークの典型例: 使い終わったリスナーを解除し忘れる
const emitter = new (require("events").EventEmitter)();
function onData(data) { /* ... */ }
emitter.on("data", onData);
// 不要になったら明示的に解除する
emitter.off("data", onData);

処理速度改善のポイント

Node.jsはシングルスレッドで動くため、書き方次第でパフォーマンスに大きな差が出ます。

サンプルページで実践コードを見る →