JavaScriptにおける`Object`クラス
この記事ではJavaScriptにおけるObjectクラスについて説明します。
JavaScriptにおけるObjectクラスについて実際的なサンプルを含めて解説します。
YouTube Video
JavaScriptにおけるObjectクラス
Object はあらゆる JavaScript オブジェクトの基底となる組み込みオブジェクトです。プロパティ管理、継承(プロトタイプチェーン)、列挙、複製、凍結など、言語の主要な機能の多くは Object の振る舞いを通して提供されます。
Object.create
オブジェクトの生成方法は複数あり、用途に応じて使い分けます。
オブジェクトリテラル(最も一般的)
次のコードは最も単純で可読性が高いオブジェクト作成方法を示します。
1// Create an object using object literal
2const user = {
3 name: "Alice",
4 age: 30,
5 greet() {
6 return `Hello, I'm ${this.name}`;
7 }
8};
9
10console.log(user.greet()); // "Hello, I'm Alice"
- この例ではリテラルでプロパティとメソッドを定義しています。シンプルでパフォーマンス面でも一般的に優れています。
new Object() コンストラクタ
Object コンストラクタはあまり使われませんが、挙動を理解しておくと便利です。
1// Create an object using the Object constructor
2const objFromCtor = new Object();
3objFromCtor.x = 10;
4objFromCtor.y = 20;
5
6console.log(objFromCtor); // { x: 10, y: 20 }
new Object()は空オブジェクトを返しますが、リテラル{}のほうが短く一般的です。
Object.create によるプロトタイプ指定
Object.create はプロトタイプを直接指定したオブジェクトを作るために使います。
1// Create an object with a specified prototype
2const proto = { hello() { return "hi"; } };
3const obj = Object.create(proto);
4obj.name = "Bob";
5
6console.log(obj.hello()); // "hi"
7console.log(Object.getPrototypeOf(obj) === proto); // true
Object.createは継承ベースのオブジェクト設計に最適で、プロトタイプチェーンを明確に制御できます。
プロパティ属性と記述子
プロパティには「値」「書き込み可能」「列挙可能」「設定可能」などの属性があり、Object.defineProperty で細かく制御できます。
基本的な defineProperty の使用例
次はdefinePropertyを用いて、非列挙プロパティと読み取り専用プロパティを定義する例です。
1// Define a non-enumerable read-only property
2const person = { name: "Carol" };
3
4Object.defineProperty(person, "id", {
5 value: 12345,
6 writable: false,
7 enumerable: false,
8 configurable: false
9});
10
11console.log(person.id); // 12345
12console.log(Object.keys(person)); // ["name"] — "id" is non-enumerable
13person.id = 999; // silently fails or throws in strict mode
14console.log(person.id); // still 12345
definePropertyを用いると、列挙・書き換え・削除など、プロパティの振る舞いを厳密に管理できます。
アクセサプロパティ(getter / setter)
アクセサを使うとプロパティの読み書きにロジックを組み込めます。
1// Use getter and setter to manage internal state
2const data = {
3 _value: 1,
4 get value() {
5 return this._value;
6 },
7 set value(v) {
8 if (typeof v === "number" && v > 0) {
9 this._value = v;
10 } else {
11 throw new Error("value must be a positive number");
12 }
13 }
14};
15
16console.log(data.value); // 1
17data.value = 5;
18console.log(data.value); // 5
19// data.value = -1; // would throw
getterとsetterを使うとプロパティアクセスを外部 API のように扱い、バリデーションや副作用を追加できます。
プロトタイプと継承(prototype / __proto__ / Object.getPrototypeOf)
JavaScript の継承はクラスベースではなくプロトタイプチェーンに基づきます。オブジェクトは他のオブジェクトをプロトタイプとして参照できます。
Object.getPrototypeOf と Object.setPrototypeOf
次の例はプロトタイプを調べたり設定したりする方法です。
1// Inspect and change prototype
2const base = { speak() { return "base"; } };
3const derived = Object.create(base);
4console.log(Object.getPrototypeOf(derived) === base); // true
5
6const other = { speak() { return "other"; } };
7Object.setPrototypeOf(derived, other);
8console.log(derived.speak()); // "other"
Object.getPrototypeOfは、オブジェクトのプロトタイプを取得します。Object.setPrototypeOfは既存オブジェクトのプロトタイプを変更しますが、パフォーマンスに悪影響を与えることがあるため注意して使います。
重要な組み込みメソッド
Object.prototype が提供するインスタンスメソッドと、Object が持つ静的メソッドの中から、よく使われる重要なものを厳選してわかりやすく解説します。
hasOwnProperty, isPrototypeOf, toString, valueOf
hasOwnPropertyやisPrototypeOf、toString、valueOfはオブジェクトの基本的な振る舞いを定義します。
1// Demonstrate prototype methods
2const base = { greet() { return "hello"; } };
3const child = Object.create(base);
4const a = { x: 1 };
5
6console.log(a.hasOwnProperty("x")); // true
7console.log(a.hasOwnProperty("toString")); // false — toString is inherited
8
9console.log(a.toString()); // "[object Object]" by default
10
11console.log(base.isPrototypeOf(child)); // true
12console.log(Object.prototype.isPrototypeOf(child)); // true
hasOwnPropertyは自前のプロパティかどうかを確認する際に必須のメソッドです。isPrototypeOfは、対象オブジェクトが自分をプロトタイプとして持つかを確認します。
Object.keys, Object.values, Object.entries
Object.keysやObject.values、Object.entriesは、列挙可能な自身のプロパティをリストで取得します。反復処理や変換に使えます。
1// Keys, values and entries
2const item = { id: 1, name: "Widget", price: 9.99 };
3
4// ["id", "name", "price"]
5console.log(Object.keys(item));
6
7// [1, "Widget", 9.99]
8console.log(Object.values(item));
9
10// [["id",1], ["name","Widget"], ["price",9.99]]
11console.log(Object.entries(item));- これらはイテレーションやオブジェクトの変換で頻繁に使います。
Object.assign
Object.assignは、浅いコピーやマージに利用されます。プロトタイプやアクセサプロパティはコピーされないことに注意してください。
1// Shallow copy / merge using Object.assign
2const target = { a: 1 };
3const source = { b: 2 };
4const result = Object.assign(target, source);
5
6console.log(result); // { a: 1, b: 2 }
7console.log(target === result); // true (merged into target)
- ネストしたオブジェクトは「参照」がコピーされるだけなので、深い複製が必要な場合は別実装が要ります。
Object.freeze, Object.seal, Object.preventExtensions
Object.freeze、Object.seal、Object.preventExtensionsは、オブジェクトのミュータビリティ(変更可能性)を制御します。
1// Freeze vs seal vs preventExtensions
2const obj = { a: 1 };
3Object.freeze(obj);
4obj.a = 2; // fails silently or throws in strict mode
5delete obj.a; // fails
6
7const obj2 = { b: 2 };
8Object.seal(obj2);
9obj2.b = 3; // allowed
10// delete obj2.b; // fails
11
12const obj3 = { c: 3 };
13Object.preventExtensions(obj3);
14obj3.d = 4; // fails
freezeは最も厳しく、内部プロパティの変更も阻止します。sealはプロパティの追加・削除を禁止しますが、既存プロパティの値は変更できます。preventExtensionsは新規プロパティの追加のみを禁止し、既存プロパティの変更や削除は許可されます。
オブジェクトの列挙性・順序と for...in / for...of
for...in は列挙可能なプロパティ名を列挙しますが、プロトタイプチェーン上のプロパティも含まれるため hasOwnProperty と組み合わせることが多いです。Object.keys() と for...ofを組み合わせて使うほうが、安全で意図が明確になります。
1// Safe enumeration
2const obj = Object.create({ inherited: true });
3obj.own = 1;
4
5for (const key in obj) {
6 if (obj.hasOwnProperty(key)) {
7 console.log("own prop:", key);
8 } else {
9 console.log("inherited prop:", key);
10 }
11}
12
13for (const key of Object.keys(obj)) {
14 console.log("key via Object.keys:", key);
15}- プロパティ列挙のルールは ES の仕様で定義されており、同じ順序を期待できる場面とできない場面があります。一般的には数値として解釈されるキーは昇順、それ以外は挿入順です。
複製(クローン)と深いコピー
オブジェクトのコピーにはObject.assignやスプレッド構文を利用する浅いコピーと、再帰的コピーなどによる深いコピーがあり、使い分けが重要です。
浅いコピー(スプレッド / Object.assign)
1// Shallow copy with spread operator
2const original = { a: 1, nested: { x: 10 } };
3const shallow = { ...original };
4shallow.nested.x = 99;
5console.log(original.nested.x); // 99 — nested object is shared
- 浅いコピーではネストしたオブジェクトは参照が共有されるため、元のオブジェクトが変更される可能性があります。
簡易な深いコピー(注意点あり)
深いコピーには JSON トリックが手軽ですが、関数や Date、循環参照、undefined などを失う欠点があります。完全な深いクローンには専用ライブラリを利用する必要があります。
1// Deep clone using JSON methods — limited use-cases only
2const source = { a: 1, d: new Date(), nested: { x: 2 } };
3const cloned = JSON.parse(JSON.stringify(source));
4console.log(cloned); // ok for plain data, but Date becomes string, functions lost
- JSON ベースは単純データでの素早い使い方には便利ですが、一般的なケースでは動作が壊れることがあります。
ミックスイン(Mixin)とオブジェクト合成
多重継承の代わりにミックスインで振る舞いを合成するパターンがよく使われます。
1// Simple mixin function
2const canEat = {
3 eat() { return `${this.name} eats`; }
4};
5const canWalk = {
6 walk() { return `${this.name} walks`; }
7};
8
9function createPerson(name) {
10 const person = { name };
11 return Object.assign(person, canEat, canWalk);
12}
13
14const p = createPerson("Dana");
15console.log(p.eat()); // "Dana eats"
16console.log(p.walk()); // "Dana walks"
- ミックスインは柔軟ですが、プロパティ名の衝突やテスト可能性に注意が必要です。
よくある落とし穴とベストプラクティス
以下は、よくある落とし穴とベストプラクティスです。
-
可変性(mutability) オブジェクトはデフォルトでミュータブルです。状態管理のあるアプリでは
Object.freeze、イミュータブルライブラリを用いた、不変データ構造を検討します。 -
プロトタイプ汚染 外部データをそのまま
Object.assignやループでオブジェクトにマージすると、__proto__やconstructorといった特別なプロパティで期待しない副作用が起きる可能性があり、セキュリティリスクになります。ユーザー入力を直接マージする際はフィルタリングするようにします。 -
for...inの落とし穴for...inはプロトタイプも列挙するため、hasOwnPropertyでチェックします。Object.keysを使うほうが明確になります。 -
浅いコピーの誤用 ネストしたオブジェクトの変更が元のオブジェクトへ影響しないようにするために、深いコピーが必要かどうか検討します。
実践例:不変(immutable)オブジェクト更新パターン
状態を直接変更せずに新しいオブジェクトを返すパターンは React 等でよく使われます。
1// Immutable update example
2const state = { todos: [{ id: 1, text: "Buy milk", done: false }] };
3
4// Toggle todo done immutably
5function toggleTodo(state, todoId) {
6 return {
7 ...state,
8 todos: state.todos.map(t => t.id === todoId ? { ...t, done: !t.done } : t)
9 };
10}
11
12const newState = toggleTodo(state, 1);
13console.log(state.todos[0].done); // false
14console.log(newState.todos[0].done); // true
- このコードは、元の
stateオブジェクトを直接変更せずに新しい状態オブジェクトを作る例です。toggleTodo関数はtodos配列をコピーし、対象の要素だけ変更した新オブジェクトを返すため、元のstateはそのまま残ります。 - イミュータブル更新は副作用を減らし、状態管理を容易にします。
実践例:安全なマージ(プロトタイプ汚染に注意)
外部 JSON をマージする場合は __proto__ を無視するようにすることでプロトタイプ汚染を防げます。
1// Safe merge ignoring __proto__ keys
2function safeMerge(target, source) {
3 for (const key of Object.keys(source)) {
4 if (key === "__proto__" || key === "constructor") continue;
5 target[key] = source[key];
6 }
7 return target;
8}
9
10const target = {};
11const source = JSON.parse('{"a":1,"__proto__":{"polluted":true}}');
12safeMerge(target, source);
13console.log(target.polluted); // undefined — safe
14console.log({}.polluted); // undefined — prototype not polluted
- ライブラリやフレームワークでもこの種の保護は重要です。
パフォーマンス上の注意
パフォーマンスに関連して、以下の点を考慮できます。
- プロトタイプチェンジ(
Object.setPrototypeOf)や、動的なプロパティの追加や削除はエンジン最適化を阻害するため頻繁に行わないようにします。 - 小さなオブジェクトを大量に生成する場合は、構成を揃えた(同じプロパティセットの)オブジェクトを使うことで最適化が効きやすくなります。
- 深いコピーはコストが高いです。必要最小限にするか差分更新を検討します。
まとめ
Object は JavaScript の中心的な仕組みであり、オブジェクトの生成、プロパティの振る舞い、継承、コピー、ミュータビリティ制御など多岐にわたる機能を提供します。Object.defineProperty や Object.assign、Object.freeze といった API を理解し、プロトタイプ汚染や浅いコピーの落とし穴に注意しながら設計することが重要です。
YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。