JavaScript Objectメソッド完全ガイド〜Object.keys/values/entries・分割代入・Immutable更新【2026年版】〜

「Object.keysとObject.entriesってどう違うの?」「分割代入を使いこなしたい」「Immutableな更新ってどうやるの?」――JavaScriptで開発をしていると、オブジェクト操作の壁に何度もぶつかります。配列に比べてObjectのAPIは地味ですが、業務コードの大半はオブジェクトの操作で構成されていると言っても過言ではありません。

本記事では、ES2025までに追加されたObject関連APIを総ざらいし、30個以上のコピペで動くサンプルコードとともに、現役エンジニアが本当に使う実践パターンを徹底解説します。Object.keys/values/entriesから、Object.groupBy、structuredClone、Immerによる不変更新まで、明日から使える内容に絞ってお届けします。

この記事で身につくこと

  • Object.keys / values / entries / fromEntriesの使い分け
  • 浅いコピーと深いコピー(structuredClone)の正しい選択
  • 分割代入・デフォルト値・リネーム・ネスト展開の全パターン
  • ImmutableUpdate(spread / Immer / lodash)による安全な状態管理
  • Object.groupBy(ES2024)による集計の現代的な書き方
  • Map vs Object の判断基準とパフォーマンス比較
  1. 1. Object.keys / values / entries — 3兄弟の使い分け
    1. 1-1. Object.keys でキー一覧を取得
    2. 1-2. Object.values で値一覧を取得
    3. 1-3. Object.entries でキー・値のペアを取得
    4. 1-4. オブジェクトの map / filter を自前で実装
    5. 1-5. ユーティリティ関数として切り出す
  2. 2. Object.fromEntries — 配列やMapからオブジェクトへ
    1. 2-1. 二次元配列からオブジェクト生成
    2. 2-2. URLSearchParams のパース
    3. 2-3. FormData をオブジェクトに変換
    4. 2-4. Map → Object 変換
  3. 3. オブジェクトのコピー — 浅い・深いの違いを完全理解
    1. 3-1. Object.assign による浅いコピー
    2. 3-2. spread構文による浅いコピー(モダンな書き方)
    3. 3-3. structuredClone による深いコピー(ES2022)
    4. 3-4. structuredClone がコピーできる型
    5. 3-5. JSON.parse(JSON.stringify()) の罠
  4. 4. オブジェクトを固める — freeze / seal / preventExtensions
    1. 4-1. Object.freeze で完全に凍結
    2. 4-2. Object.isFrozen で確認
    3. 4-3. freeze は浅い凍結
    4. 4-4. 深い凍結(deepFreeze)の実装
    5. 4-5. Object.seal — プロパティ追加・削除は禁止、値変更はOK
    6. 4-6. Object.preventExtensions — 追加だけ禁止
  5. 5. プロトタイプ操作 — Object.create / getPrototypeOf
    1. 5-1. Object.create で継承
    2. 5-2. Object.create(null) でプレーンなオブジェクト
    3. 5-3. Object.setPrototypeOf(非推奨パターン)
  6. 6. プロパティディスクリプタの世界
    1. 6-1. Object.defineProperty で読み取り専用
    2. 6-2. getter / setter を定義
    3. 6-3. Object.defineProperties で複数同時定義
    4. 6-4. Object.getOwnPropertyDescriptors で全ディスクリプタ取得
  7. 7. プロパティ存在チェックの正解 — Object.hasOwn
    1. 7-1. Object.hasOwn(ES2022)
    2. 7-2. in 演算子との違い
    3. 7-3. hasOwnProperty の古典的問題
  8. 8. 分割代入を極める
    1. 8-1. 基本の分割代入
    2. 8-2. リネーム
    3. 8-3. デフォルト値
    4. 8-4. リネーム + デフォルト値
    5. 8-5. ネストの分割代入
    6. 8-6. レスト構文で残りをまとめる
    7. 8-7. 関数引数での分割代入
  9. 9. オプショナルチェイン と Nullish coalescing
    1. 9-1. オプショナルチェイン(?.)
    2. 9-2. Nullish coalescing(??)
    3. 9-3. 組み合わせの実践
  10. 10. Object.groupBy — ES2024の新機能
    1. 10-1. 基本の使い方
    2. 10-2. 数値の範囲でグルーピング
    3. 10-3. groupBy が無い環境のpolyfill
  11. 11. JSON との行き来
    1. 11-1. JSON.stringify の基本と整形
    2. 11-2. replacer で出力をフィルタ
    3. 11-3. reviver でパース時に変換
    4. 11-4. toJSON カスタマイズ
  12. 12. Map vs Object — どちらを使うべきか
    1. 12-1. 使い分けの早見表
    2. 12-2. Mapの基本
    3. 12-3. Object → Map 変換
    4. 12-4. Map → Object 変換
  13. 13. Immutable更新パターン — Reactの必須テクニック
    1. 13-1. 1階層のプロパティ更新
    2. 13-2. ネストされたオブジェクトの更新
    3. 13-3. 配列を含むオブジェクトの更新
    4. 13-4. プロパティ削除のImmutable版
  14. 14. Immer — Immutable更新を直感的に書く
    1. 14-1. Immerの基本
    2. 14-2. Immerでカレント関数を作る
    3. 14-3. ReduxToolkitでの内部利用
  15. 15. lodash の get / set — 動的なパス指定
    1. 15-1. _.get で安全に値を取得
    2. 15-2. _.set で深いプロパティをセット
    3. 15-3. _.cloneDeep の代替
  16. 16. TypeScriptでのオブジェクト型推論の限界
    1. 16-1. Object.keysの戻り値はstring[]
    2. 16-2. Object.entriesも同様
    3. 16-3. satisfies 演算子(TS 4.9+)
  17. 17. パフォーマンス考察 — 何が速くて何が遅いか
    1. 17-1. 大量プロパティのコピーは遅い
    2. 17-2. キーが事前に分かっているなら直接アクセス
    3. 17-3. 頻繁な追加・削除はMapが速い
  18. 18. 現役エンジニアが本気で学ぶなら
    1. 18-1. 学習ロードマップの一例
  19. 19. まとめ — 明日から使えるObject操作

1. Object.keys / values / entries — 3兄弟の使い分け

オブジェクト操作の基本にして最重要のAPIが、Object.keys / Object.values / Object.entriesの3兄弟です。配列メソッド(map / filter / reduce)と組み合わせることで、ほぼすべてのオブジェクト変換が表現できます。

1-1. Object.keys でキー一覧を取得

const user = { id: 1, name: "Taro", age: 28, email: "taro@example.com" };

const keys = Object.keys(user);
console.log(keys);
// ["id", "name", "age", "email"]

// プロパティ数を数える
console.log(Object.keys(user).length); // 4

// for...inと異なりプロトタイプチェーンは辿らない
const proto = { inherited: "ignored" };
const child = Object.create(proto);
child.own = "visible";
console.log(Object.keys(child)); // ["own"] のみ

1-2. Object.values で値一覧を取得

const scores = { math: 80, english: 92, science: 75 };

const values = Object.values(scores);
console.log(values); // [80, 92, 75]

// 合計と平均
const total = Object.values(scores).reduce((a, b) => a + b, 0);
const avg = total / Object.values(scores).length;
console.log(total, avg); // 247 82.33...

// 最高点
console.log(Math.max(...Object.values(scores))); // 92

1-3. Object.entries でキー・値のペアを取得

const config = { host: "localhost", port: 5432, ssl: true };

for (const [key, value] of Object.entries(config)) {
  console.log(`${key} = ${value}`);
}
// host = localhost
// port = 5432
// ssl = true

// クエリ文字列に変換
const params = { page: 1, limit: 20, sort: "asc" };
const qs = Object.entries(params)
  .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
  .join("&");
console.log(qs); // page=1&limit=20&sort=asc

1-4. オブジェクトの map / filter を自前で実装

配列にはmap / filterがありますが、オブジェクトには存在しません。Object.entries + Object.fromEntriesの組み合わせで実装できます。

// オブジェクトのvalueをすべて2倍にする
const prices = { apple: 100, banana: 50, cherry: 300 };

const doubled = Object.fromEntries(
  Object.entries(prices).map(([k, v]) => [k, v * 2])
);
console.log(doubled);
// { apple: 200, banana: 100, cherry: 600 }

// 200円以上だけ残す
const expensive = Object.fromEntries(
  Object.entries(prices).filter(([_, v]) => v >= 200)
);
console.log(expensive); // { cherry: 300 }

1-5. ユーティリティ関数として切り出す

// TypeScriptで型安全に
function mapValues<T, U>(
  obj: Record<string, T>,
  fn: (value: T, key: string) => U
): Record<string, U> {
  return Object.fromEntries(
    Object.entries(obj).map(([k, v]) => [k, fn(v, k)])
  );
}

const result = mapValues({ a: 1, b: 2, c: 3 }, (v) => v * 10);
console.log(result); // { a: 10, b: 20, c: 30 }

2. Object.fromEntries — 配列やMapからオブジェクトへ

Object.fromEntriesはES2019で追加された、Object.entriesの逆変換を行うメソッドです。配列やMapからオブジェクトを生成する際の決定版です。

2-1. 二次元配列からオブジェクト生成

const pairs = [
  ["name", "Hanako"],
  ["age", 30],
  ["role", "engineer"],
];

const obj = Object.fromEntries(pairs);
console.log(obj);
// { name: "Hanako", age: 30, role: "engineer" }

2-2. URLSearchParams のパース

const url = new URL("https://example.com/?page=2&limit=10&sort=desc");
const queryObject = Object.fromEntries(url.searchParams);
console.log(queryObject);
// { page: "2", limit: "10", sort: "desc" }

2-3. FormData をオブジェクトに変換

// HTMLフォーム送信時の典型パターン
const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
  e.preventDefault();
  const formData = new FormData(form);
  const data = Object.fromEntries(formData);
  console.log(data); // { username: "...", email: "...", ... }

  fetch("/api/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });
});

2-4. Map → Object 変換

const userMap = new Map([
  ["id", 1],
  ["name", "Yamada"],
]);

const userObj = Object.fromEntries(userMap);
console.log(userObj); // { id: 1, name: "Yamada" }

3. オブジェクトのコピー — 浅い・深いの違いを完全理解

「コピーしたつもりが元のオブジェクトまで書き換わっていた」というバグは、JavaScript初学者が必ず通る道です。ここを正確に理解しないと、Reactのstate更新でハマります。

3-1. Object.assign による浅いコピー

const original = { name: "Sato", age: 25, address: { city: "Tokyo" } };

const copy = Object.assign({}, original);
copy.name = "Suzuki";
console.log(original.name); // "Sato" ← 影響なし

// しかし、ネストされたオブジェクトは参照共有
copy.address.city = "Osaka";
console.log(original.address.city); // "Osaka" ← 巻き込まれる!

3-2. spread構文による浅いコピー(モダンな書き方)

const original = { a: 1, b: 2, c: { nested: true } };

const copy = { ...original };
copy.a = 999;
console.log(original.a); // 1 ← OK

// ネストは依然として共有
copy.c.nested = false;
console.log(original.c.nested); // false ← 同じ問題

3-3. structuredClone による深いコピー(ES2022)

structuredCloneはWeb標準APIで、ライブラリ不要で深いコピーが可能です。2026年現在、深いコピーの第一選択肢です。

const original = {
  name: "Tanaka",
  address: { city: "Tokyo", zip: "100-0001" },
  tags: ["admin", "developer"],
};

const deep = structuredClone(original);
deep.address.city = "Osaka";
deep.tags.push("new");

console.log(original.address.city); // "Tokyo" ← 影響なし
console.log(original.tags); // ["admin", "developer"] ← 影響なし

3-4. structuredClone がコピーできる型

// オブジェクト・配列はもちろん、以下も対応
const complex = {
  date: new Date(),
  regex: /foo/g,
  map: new Map([["key", "value"]]),
  set: new Set([1, 2, 3]),
  arrayBuffer: new ArrayBuffer(8),
};

const cloned = structuredClone(complex);
console.log(cloned.date instanceof Date); // true
console.log(cloned.map.get("key")); // "value"

// ただし関数やDOMノードはコピー不可
try {
  structuredClone({ fn: () => 1 });
} catch (e) {
  console.error(e.message); // "could not be cloned"
}

3-5. JSON.parse(JSON.stringify()) の罠

// 古典的な深いコピー手法だが、欠点が多い
const original = {
  date: new Date("2026-01-01"),
  fn: () => "hello",
  undef: undefined,
  num: NaN,
  inf: Infinity,
};

const bad = JSON.parse(JSON.stringify(original));
console.log(bad.date); // 文字列に化ける!
console.log(bad.fn); // undefined ← 消える
console.log(bad.undef); // キーごと消失
console.log(bad.num); // null ← NaNはnullに
console.log(bad.inf); // null ← Infinityもnullに

// → structuredClone か Immer を使うべき

4. オブジェクトを固める — freeze / seal / preventExtensions

オブジェクトの変更を制限する3段階の方法があります。意図せぬ書き換えを防ぐdefensive programmingの基本です。

4-1. Object.freeze で完全に凍結

const config = Object.freeze({
  API_URL: "https://api.example.com",
  TIMEOUT: 5000,
});

config.TIMEOUT = 10000; // strict modeでなければ silent fail
console.log(config.TIMEOUT); // 5000

"use strict";
config.NEW = "added"; // TypeError: Cannot add property

4-2. Object.isFrozen で確認

const frozen = Object.freeze({ a: 1 });
const normal = { a: 1 };

console.log(Object.isFrozen(frozen)); // true
console.log(Object.isFrozen(normal)); // false

4-3. freeze は浅い凍結

const nested = Object.freeze({
  level1: { level2: { value: 1 } },
});

// トップレベルは凍結されるが…
nested.level1 = "changed"; // 無視される

// ネストは生きている
nested.level1.level2.value = 999;
console.log(nested.level1.level2.value); // 999 ← 変わる

4-4. 深い凍結(deepFreeze)の実装

function deepFreeze(obj) {
  Object.keys(obj).forEach((key) => {
    const value = obj[key];
    if (value && typeof value === "object") {
      deepFreeze(value);
    }
  });
  return Object.freeze(obj);
}

const truly = deepFreeze({
  a: { b: { c: 1 } },
});
truly.a.b.c = 999; // strict modeでTypeError
console.log(truly.a.b.c); // 1

4-5. Object.seal — プロパティ追加・削除は禁止、値変更はOK

const sealed = Object.seal({ name: "Aoki", age: 40 });

sealed.age = 41; // OK
sealed.email = "x"; // 追加は無視
delete sealed.name; // 削除も無視

console.log(sealed); // { name: "Aoki", age: 41 }
console.log(Object.isSealed(sealed)); // true

4-6. Object.preventExtensions — 追加だけ禁止

const obj = Object.preventExtensions({ x: 1 });
obj.x = 2; // OK
delete obj.x; // OK
obj.y = 3; // 無視

console.log(Object.isExtensible(obj)); // false
console.log(obj); // {}

5. プロトタイプ操作 — Object.create / getPrototypeOf

「JavaScriptはプロトタイプベースの言語」と言われますが、実務でObject.createを意識して使う場面は限定的です。それでもライブラリ実装などで重要な知識です。

5-1. Object.create で継承

const animal = {
  greet() {
    return `I'm ${this.name}`;
  },
};

const dog = Object.create(animal);
dog.name = "Pochi";
console.log(dog.greet()); // "I'm Pochi"

console.log(Object.getPrototypeOf(dog) === animal); // true

5-2. Object.create(null) でプレーンなオブジェクト

// 通常のオブジェクトはObject.prototypeを継承するため、
// toStringやhasOwnPropertyなどのキーを持つ
const normal = {};
console.log(normal.toString); // [Function: toString]

// 完全にクリーンなオブジェクトが欲しい場合
const clean = Object.create(null);
console.log(clean.toString); // undefined

// ハッシュマップとして使う場合に有用
clean["toString"] = "user-defined";
console.log(clean.toString); // "user-defined"

5-3. Object.setPrototypeOf(非推奨パターン)

// パフォーマンスが大幅に悪化するため避けるべき
const a = { foo: 1 };
const b = { bar: 2 };

Object.setPrototypeOf(a, b);
console.log(a.bar); // 2 ← bから継承
// ↑ 実務では Object.create か class を使うべき

6. プロパティディスクリプタの世界

普段書くobj.x = 1は、内部的にはディスクリプタを設定しています。これを直接操作することで、読み取り専用プロパティやgetter/setterを定義できます。

6-1. Object.defineProperty で読み取り専用

const user = { name: "Inoue" };

Object.defineProperty(user, "id", {
  value: 100,
  writable: false,    // 書き換え不可
  enumerable: false,  // for...in / Object.keysで出ない
  configurable: false // 削除・再定義不可
});

user.id = 999; // 無視
console.log(user.id); // 100
console.log(Object.keys(user)); // ["name"] のみ

6-2. getter / setter を定義

const account = {
  _balance: 1000,
};

Object.defineProperty(account, "balance", {
  get() {
    return `¥${this._balance.toLocaleString()}`;
  },
  set(value) {
    if (typeof value !== "number") {
      throw new Error("balance must be number");
    }
    this._balance = value;
  },
  enumerable: true,
});

console.log(account.balance); // "¥1,000"
account.balance = 5000;
console.log(account.balance); // "¥5,000"

6-3. Object.defineProperties で複数同時定義

const product = {};

Object.defineProperties(product, {
  name: { value: "MacBook", writable: true, enumerable: true },
  price: { value: 250000, writable: false, enumerable: true },
  category: { value: "PC", enumerable: false },
});

console.log(product); // { name: "MacBook", price: 250000 }
console.log(Object.keys(product)); // ["name", "price"]

6-4. Object.getOwnPropertyDescriptors で全ディスクリプタ取得

const obj = { a: 1 };
Object.defineProperty(obj, "b", {
  value: 2,
  writable: false,
});

const descriptors = Object.getOwnPropertyDescriptors(obj);
console.log(descriptors);
/*
{
  a: { value: 1, writable: true, enumerable: true, configurable: true },
  b: { value: 2, writable: false, enumerable: false, configurable: false }
}
*/

// 完全コピー(ディスクリプタごと)
const clone = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);

7. プロパティ存在チェックの正解 — Object.hasOwn

ES2022で追加されたObject.hasOwnは、長年のhasOwnProperty地獄を解決した決定版です。

7-1. Object.hasOwn(ES2022)

const obj = { name: "Hashimoto", age: undefined };

console.log(Object.hasOwn(obj, "name")); // true
console.log(Object.hasOwn(obj, "age"));  // true(値がundefinedでも true)
console.log(Object.hasOwn(obj, "foo")); // false

// プロトタイプチェーンは見ない
console.log(Object.hasOwn(obj, "toString")); // false

7-2. in 演算子との違い

const obj = { name: "Kimura" };

console.log("name" in obj); // true
console.log("toString" in obj); // true ← プロトタイプチェーンを辿る!

console.log(Object.hasOwn(obj, "toString")); // false ← 自身のプロパティのみ

7-3. hasOwnProperty の古典的問題

// hasOwnPropertyは継承メソッドなので上書きされうる
const dangerous = {
  hasOwnProperty: "I am a property",
  realKey: 1,
};

// dangerous.hasOwnProperty("realKey"); // TypeError!

// Object.hasOwnなら安全
console.log(Object.hasOwn(dangerous, "realKey")); // true

// Object.create(null) のオブジェクトでも安全
const nullProto = Object.create(null);
nullProto.x = 1;
// nullProto.hasOwnProperty("x"); // TypeError(メソッドが存在しない)
console.log(Object.hasOwn(nullProto, "x")); // true ← OK

8. 分割代入を極める

分割代入(Destructuring)は、ES2015で追加されてからJavaScriptの書き味を一変させました。React開発では1日に何百回も書くパターンです。

8-1. 基本の分割代入

const user = { id: 1, name: "Kobayashi", email: "k@example.com" };

const { id, name, email } = user;
console.log(id, name, email); // 1 "Kobayashi" "k@example.com"

// 一部だけ取り出す
const { name: userName } = user;
console.log(userName); // "Kobayashi"

8-2. リネーム

const apiResponse = {
  user_id: 1,
  user_name: "Saito",
  user_email: "s@example.com",
};

// snake_case → camelCase
const { user_id: userId, user_name: userName, user_email: email } = apiResponse;
console.log(userId, userName, email); // 1 "Saito" "s@example.com"

8-3. デフォルト値

function fetchUser({ id, page = 1, limit = 20 } = {}) {
  return { id, page, limit };
}

console.log(fetchUser({ id: 100 }));
// { id: 100, page: 1, limit: 20 }

console.log(fetchUser({ id: 100, limit: 50 }));
// { id: 100, page: 1, limit: 50 }

console.log(fetchUser());
// { id: undefined, page: 1, limit: 20 }

8-4. リネーム + デフォルト値

const config = { host: "localhost" };

const { host: hostname = "0.0.0.0", port: portNumber = 3000 } = config;
console.log(hostname, portNumber); // "localhost" 3000

8-5. ネストの分割代入

const response = {
  status: 200,
  data: {
    user: {
      name: "Maeda",
      address: { city: "Yokohama", zip: "220-0001" },
    },
  },
};

const {
  data: {
    user: {
      name,
      address: { city },
    },
  },
} = response;

console.log(name, city); // "Maeda" "Yokohama"

8-6. レスト構文で残りをまとめる

const user = { id: 1, name: "Murata", email: "m@example.com", role: "admin" };

const { id, ...rest } = user;
console.log(id); // 1
console.log(rest); // { name: "Murata", email: "m@example.com", role: "admin" }

// 特定プロパティを除外したコピーを作るイディオム
const { password, ...publicUser } = { id: 1, name: "X", password: "secret" };
console.log(publicUser); // { id: 1, name: "X" }

8-7. 関数引数での分割代入

type CreateUserOptions = {
  name: string;
  age?: number;
  role?: "admin" | "user";
};

function createUser({ name, age = 20, role = "user" }: CreateUserOptions) {
  return { name, age, role, createdAt: new Date() };
}

createUser({ name: "Hayashi" });
// { name: "Hayashi", age: 20, role: "user", createdAt: ... }

9. オプショナルチェイン と Nullish coalescing

ES2020で追加された?.??は、深いネストを安全に辿るためのモダンJSの必須記法です。

9-1. オプショナルチェイン(?.)

const user = {
  profile: {
    address: null,
  },
};

// 古い書き方
const city1 = user && user.profile && user.profile.address && user.profile.address.city;

// モダンな書き方
const city2 = user?.profile?.address?.city;
console.log(city2); // undefined ← エラーにならない

// メソッド呼び出しも対応
const result = obj?.maybeMethod?.();

// 配列インデックスも
const first = list?.[0];

9-2. Nullish coalescing(??)

// || はfalsy値全てに反応
const a = 0 || "default";   // "default" ← 0が消えてしまう
const b = "" || "default";  // "default" ← 空文字が消える

// ?? はnull / undefinedのみ
const c = 0 ?? "default";   // 0 ← 維持される
const d = "" ?? "default";  // ""
const e = null ?? "default"; // "default"
const f = undefined ?? "default"; // "default"

// 設定値のフォールバック
function init(options = {}) {
  const timeout = options.timeout ?? 5000;
  const retry = options.retry ?? 3;
  return { timeout, retry };
}
console.log(init({ timeout: 0 })); // { timeout: 0, retry: 3 }

9-3. 組み合わせの実践

function getUserCity(user) {
  return user?.profile?.address?.city ?? "未設定";
}

console.log(getUserCity({})); // "未設定"
console.log(getUserCity({ profile: { address: { city: "Sapporo" } } })); // "Sapporo"
console.log(getUserCity(null)); // "未設定"

10. Object.groupBy — ES2024の新機能

2024年にECMAScriptに追加されたObject.groupByは、配列要素をキーごとにグルーピングする待望のメソッドです。Node.js 21以降、ブラウザもChrome117+、Safari17.4+で利用可能です。

10-1. 基本の使い方

const products = [
  { name: "Apple", category: "fruit", price: 100 },
  { name: "Carrot", category: "vegetable", price: 80 },
  { name: "Banana", category: "fruit", price: 50 },
  { name: "Onion", category: "vegetable", price: 60 },
];

const grouped = Object.groupBy(products, (item) => item.category);
console.log(grouped);
/*
{
  fruit: [
    { name: "Apple", category: "fruit", price: 100 },
    { name: "Banana", category: "fruit", price: 50 }
  ],
  vegetable: [
    { name: "Carrot", category: "vegetable", price: 80 },
    { name: "Onion", category: "vegetable", price: 60 }
  ]
}
*/

10-2. 数値の範囲でグルーピング

const ages = [12, 25, 33, 45, 67, 18, 50];

const byAgeGroup = Object.groupBy(ages, (age) => {
  if (age < 20) return "young";
  if (age < 60) return "middle";
  return "senior";
});

console.log(byAgeGroup);
// { young: [12, 18], middle: [25, 33, 45, 50], senior: [67] }

10-3. groupBy が無い環境のpolyfill

// 古い環境でも使えるシンプル実装
function groupBy(array, keyFn) {
  return array.reduce((acc, item) => {
    const key = keyFn(item);
    (acc[key] ||= []).push(item);
    return acc;
  }, {});
}

const grouped = groupBy(
  [{ type: "a" }, { type: "b" }, { type: "a" }],
  (x) => x.type
);
console.log(grouped); // { a: [{type:"a"},{type:"a"}], b: [{type:"b"}] }

11. JSON との行き来

11-1. JSON.stringify の基本と整形

const data = { name: "Sasaki", tags: ["js", "ts"] };

console.log(JSON.stringify(data));
// {"name":"Sasaki","tags":["js","ts"]}

// 第3引数で整形(インデント幅)
console.log(JSON.stringify(data, null, 2));
/*
{
  "name": "Sasaki",
  "tags": [
    "js",
    "ts"
  ]
}
*/

11-2. replacer で出力をフィルタ

const user = {
  id: 1,
  name: "Watanabe",
  password: "secret",
  email: "w@example.com",
};

// 配列で指定 → そのキーだけ出力
console.log(JSON.stringify(user, ["id", "name"]));
// {"id":1,"name":"Watanabe"}

// 関数で指定 → 動的に制御
const safe = JSON.stringify(user, (key, value) => {
  if (key === "password") return undefined;
  return value;
});
console.log(safe);
// {"id":1,"name":"Watanabe","email":"w@example.com"}

11-3. reviver でパース時に変換

const json = '{"name":"Ito","birthday":"2000-01-15T00:00:00.000Z"}';

const parsed = JSON.parse(json, (key, value) => {
  // ISO日付文字列をDateに復元
  if (typeof value === "string" && /^d{4}-d{2}-d{2}T/.test(value)) {
    return new Date(value);
  }
  return value;
});

console.log(parsed.birthday instanceof Date); // true
console.log(parsed.birthday.getFullYear()); // 2000

11-4. toJSON カスタマイズ

class User {
  constructor(name, password) {
    this.name = name;
    this.password = password;
  }
  toJSON() {
    // stringify時に呼ばれる
    return { name: this.name }; // パスワードは出さない
  }
}

const u = new User("Yoshida", "secret123");
console.log(JSON.stringify(u));
// {"name":"Yoshida"}

12. Map vs Object — どちらを使うべきか

「キー・値のペアを管理する」用途では、ObjectとMapの両方が選択肢になります。判断基準を整理します。

12-1. 使い分けの早見表

観点 Object Map
キーの型 文字列/Symbolのみ 任意の型(オブジェクトもOK)
順序保証 整数キーは昇順 挿入順を厳密に保持
サイズ取得 Object.keys().length map.size
イテレート for-in / entries for-of(高速)
頻繁な追加削除 遅め 最適化されている
JSON化 そのまま可能 変換が必要
プロトタイプ衝突 あり(toStringなど) なし

12-2. Mapの基本

const m = new Map();
m.set("name", "Goto");
m.set("age", 35);
m.set({ id: 1 }, "object as key!");

console.log(m.size); // 3
console.log(m.get("name")); // "Goto"
console.log(m.has("age")); // true

for (const [k, v] of m) {
  console.log(k, v);
}

12-3. Object → Map 変換

const obj = { a: 1, b: 2, c: 3 };

const map = new Map(Object.entries(obj));
console.log(map.get("a")); // 1
console.log(map.size); // 3

12-4. Map → Object 変換

const map = new Map([
  ["x", 10],
  ["y", 20],
]);

const obj = Object.fromEntries(map);
console.log(obj); // { x: 10, y: 20 }

13. Immutable更新パターン — Reactの必須テクニック

React / Redux / Zustandなど、現代のフロントエンドでは「stateは直接書き換えず、新しいオブジェクトを返す」のが鉄則です。具体的なパターンを押さえましょう。

13-1. 1階層のプロパティ更新

const state = { count: 0, user: "Hara" };

// NG: 直接変更
// state.count = 1;

// OK: 新しいオブジェクトを生成
const newState = { ...state, count: state.count + 1 };
console.log(newState); // { count: 1, user: "Hara" }
console.log(state); // { count: 0, user: "Hara" } ← 不変

13-2. ネストされたオブジェクトの更新

const state = {
  user: {
    profile: {
      name: "Hirano",
      age: 30,
    },
  },
};

// 各階層でspreadが必要
const newState = {
  ...state,
  user: {
    ...state.user,
    profile: {
      ...state.user.profile,
      age: 31,
    },
  },
};

console.log(newState.user.profile.age); // 31
console.log(state.user.profile.age); // 30 ← 影響なし

13-3. 配列を含むオブジェクトの更新

const state = {
  todos: [
    { id: 1, text: "buy milk", done: false },
    { id: 2, text: "write code", done: false },
  ],
};

// idで指定したtodoのdoneをtrueに
const updated = {
  ...state,
  todos: state.todos.map((t) =>
    t.id === 1 ? { ...t, done: true } : t
  ),
};

console.log(updated.todos[0].done); // true
console.log(state.todos[0].done); // false ← 不変

13-4. プロパティ削除のImmutable版

const state = { a: 1, b: 2, c: 3 };

// レスト構文で除外
const { b, ...rest } = state;
console.log(rest); // { a: 1, c: 3 }
console.log(state); // { a: 1, b: 2, c: 3 } ← 不変

14. Immer — Immutable更新を直感的に書く

ネストが深くなるとspreadは限界です。Immerを使えば、ミュータブルな書き方でImmutableな結果が得られます。Redux Toolkitの内部でも使われています。

14-1. Immerの基本

// npm install immer
import { produce } from "immer";

const state = {
  user: {
    profile: { name: "Nishi", age: 25 },
    addresses: [{ city: "Tokyo" }],
  },
};

const next = produce(state, (draft) => {
  draft.user.profile.age = 26;       // 普通の代入でOK
  draft.user.addresses.push({ city: "Osaka" }); // pushもOK
});

console.log(next.user.profile.age); // 26
console.log(state.user.profile.age); // 25 ← 不変
console.log(next.user.addresses.length); // 2

14-2. Immerでカレント関数を作る

import { produce } from "immer";

// 状態を受け取って状態を返す関数を作れる
const addTodo = produce((draft, text) => {
  draft.todos.push({ id: Date.now(), text, done: false });
});

const initial = { todos: [] };
const after = addTodo(initial, "buy bread");
console.log(after.todos); // [{ id: ..., text: "buy bread", done: false }]

14-3. ReduxToolkitでの内部利用

// Redux ToolkitのcreateSliceはinternalでImmerを使う
import { createSlice } from "@reduxjs/toolkit";

const userSlice = createSlice({
  name: "user",
  initialState: { name: "", age: 0 },
  reducers: {
    setName(state, action) {
      // 直接書き換えてOK!Immerが面倒を見る
      state.name = action.payload;
    },
    incrementAge(state) {
      state.age += 1;
    },
  },
});

15. lodash の get / set — 動的なパス指定

キーがランタイムで決まる場合(設定ファイル、フォームライブラリなど)、lodashのget / setが便利です。

15-1. _.get で安全に値を取得

import _ from "lodash";

const obj = {
  user: { profile: { address: { city: "Kobe" } } },
};

console.log(_.get(obj, "user.profile.address.city")); // "Kobe"
console.log(_.get(obj, "user.profile.foo.bar", "default")); // "default"
console.log(_.get(obj, ["user", "profile", "address", "city"])); // "Kobe"

15-2. _.set で深いプロパティをセット

import _ from "lodash";

const obj = {};
_.set(obj, "user.profile.name", "Mori");
console.log(obj); // { user: { profile: { name: "Mori" } } }

// 配列インデックスにも対応
_.set(obj, "tags[0]", "js");
_.set(obj, "tags[1]", "ts");
console.log(obj.tags); // ["js", "ts"]

15-3. _.cloneDeep の代替

import _ from "lodash";

const original = { date: new Date(), nested: { a: 1 } };

// structuredCloneでも対応できるが、lodashなら関数も保持できる
const clone = _.cloneDeep(original);
console.log(clone.date instanceof Date); // true
clone.nested.a = 999;
console.log(original.nested.a); // 1 ← 不変

16. TypeScriptでのオブジェクト型推論の限界

TypeScriptは強力ですが、Object操作では型推論が崩れる場面があります。回避策を押さえておきましょう。

16-1. Object.keysの戻り値はstring[]

type User = { id: number; name: string; age: number };
const user: User = { id: 1, name: "X", age: 20 };

// 期待: ("id" | "name" | "age")[]
// 実際: string[]
const keys = Object.keys(user);

// 型アサーションで対応
const typedKeys = Object.keys(user) as (keyof User)[];
typedKeys.forEach((k) => {
  console.log(user[k]); // OK: k is "id" | "name" | "age"
});

16-2. Object.entriesも同様

function typedEntries<T extends object>(
  obj: T
): [keyof T, T[keyof T]][] {
  return Object.entries(obj) as [keyof T, T[keyof T]][];
}

const user = { id: 1, name: "Y" };
typedEntries(user).forEach(([k, v]) => {
  // k: "id" | "name", v: number | string
  console.log(k, v);
});

16-3. satisfies 演算子(TS 4.9+)

// 型を強制しつつ、リテラル型を保持
const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
} satisfies Record<string, string | number>;

// config.apiUrl は string ではなく "https://api.example.com" 型
const url: "https://api.example.com" = config.apiUrl; // OK

17. パフォーマンス考察 — 何が速くて何が遅いか

「JavaScriptのオブジェクト操作で何が遅いのか?」は、本当のホットパスを書くまで意識する必要はありません。それでも知っておくと役立つベンチマーク事実があります。

17-1. 大量プロパティのコピーは遅い

// 10000プロパティを持つオブジェクトのコピー
function bench() {
  const big = {};
  for (let i = 0; i < 10000; i++) big[`k${i}`] = i;

  console.time("spread");
  for (let i = 0; i < 100; i++) ({ ...big });
  console.timeEnd("spread");

  console.time("Object.assign");
  for (let i = 0; i < 100; i++) Object.assign({}, big);
  console.timeEnd("Object.assign");

  console.time("structuredClone");
  for (let i = 0; i < 100; i++) structuredClone(big);
  console.timeEnd("structuredClone");
}
bench();
// 一般にspread &approx; Object.assign < structuredClone(深いコピー対応のため重い)

17-2. キーが事前に分かっているなら直接アクセス

// ❌ 毎回Object.keysを回すのは無駄
function slowSum(obj) {
  return Object.keys(obj).reduce((a, k) => a + obj[k], 0);
}

// ✅ 形が決まっているなら直接書く
function fastSum(obj) {
  return obj.math + obj.english + obj.science;
}

17-3. 頻繁な追加・削除はMapが速い

const obj = {};
const map = new Map();

console.time("object set");
for (let i = 0; i < 1_000_000; i++) obj[i] = i;
console.timeEnd("object set");

console.time("map set");
for (let i = 0; i < 1_000_000; i++) map.set(i, i);
console.timeEnd("map set");
// Mapの方が10〜30%程度速い傾向

18. 現役エンジニアが本気で学ぶなら

ここまでで30以上のサンプルを通じて、Object操作の全領域をカバーしました。しかし、独学で身につけるべき範囲はこれだけではありません。React、Redux、TypeScript、テスト、設計パターンと、現代のフロントエンドエンジニアに求められる知識は膨大です。

もし「体系立てて、実務で通用するスキルを最短で身につけたい」と考えているなら、プロのカリキュラムで一気に学ぶ選択肢があります。以下のスクールは、いずれも無料カウンセリングや無料体験が用意されており、自分に合うかを試してから判断できます。

JavaScript / TypeScript を本格的に学べるスクール

  • テックアカデミー — 現役エンジニアのマンツーマンメンタリングが強み。フロントエンドコース・JavaScriptコースが充実。
  • 侍エンジニア — 完全オーダーメイドカリキュラム。Reactや実務案件に直結する設計が可能。
  • DMM WEBCAMP — 転職保証つきの社会人コース。未経験からの転職実績が業界トップクラス。
  • レバテックカレッジ — 大学生・大学院生限定。短期集中で就活前にスキルを仕上げたい人向け。

18-1. 学習ロードマップの一例

1. JS基礎(変数・関数・スコープ・this)
2. ES2015+ のモダン構文(本記事の範囲)
3. 非同期(Promise / async・await)
4. DOM操作 + Fetch API
5. TypeScript(型システム・ジェネリクス)
6. React(関数コンポーネント・Hooks)
7. 状態管理(Zustand / Redux Toolkit)
8. テスト(Vitest / Testing Library)
9. ビルドツール(Vite / esbuild)
10. デプロイ(Vercel / Cloudflare Pages)

19. まとめ — 明日から使えるObject操作

長丁場おつかれさまでした。最後にこの記事で扱った要点を、6つの実務指針として整理します。

  1. Object.entries + fromEntries でオブジェクトのmap / filterを表現する
  2. structuredClone を深いコピーの第一選択肢にする(JSON経由は捨てる)
  3. Object.hasOwn をプロパティ存在チェックの標準とする
  4. ?. と ?? でネストとnullを安全に処理する
  5. spread or Immer でImmutable更新を徹底する
  6. 頻繁な追加削除や非文字列キーがあるならMapを選ぶ

本記事のコードはすべてES2025対応の最新版です。お気に入りのプレイグラウンド(PlayCodeStackBlitz)に貼って、ぜひ手を動かして確認してみてください。Object操作を制すれば、React / TypeScript / Node.jsすべての世界で書ける幅が広がります。

関連記事として、JavaScript配列メソッド完全ガイドasync/await完全実践ガイドもあわせてどうぞ。

コメント

タイトルとURLをコピーしました