|
| 1 | +# 第6章: オブジェクトとプロトタイプ |
| 2 | + |
| 3 | +他の言語(Java, C\#, Pythonなど)の経験がある方にとって、JavaScriptの「オブジェクト」と「継承」のモデルは最も混乱しやすい部分の一つです。JavaScriptはクラスベースではなく、**プロトタイプベース**のオブジェクト指向言語です。 |
| 4 | + |
| 5 | +本章では、ES6(ECMAScript 2015)以降の`class`構文(第7章で扱います)の裏側で実際に何が起きているのか、その仕組みの根幹である「プロトタイプチェーン」について解説します。 |
| 6 | + |
| 7 | +## オブジェクトリテラルとプロパティ |
| 8 | + |
| 9 | +JavaScriptにおけるオブジェクトは、基本的にはキー(プロパティ名)と値のコレクション(連想配列やハッシュマップに近いもの)です。最も一般的な生成方法は**オブジェクトリテラル** `{...}` を使うことです。 |
| 10 | + |
| 11 | +```js-repl:1 |
| 12 | +> const book = { |
| 13 | +... title: "JavaScript Primer", |
| 14 | +... "page-count": 350, // ハイフンを含むキーは引用符が必要 |
| 15 | +... author: { |
| 16 | +... name: "John Doe", |
| 17 | +... age: 30 |
| 18 | +... } |
| 19 | +... }; |
| 20 | +undefined |
| 21 | +> book.title |
| 22 | +'JavaScript Primer' |
| 23 | +> book["page-count"] // 識別子として無効な文字を含む場合はブラケット記法 |
| 24 | +350 |
| 25 | +> book.author.name |
| 26 | +'John Doe' |
| 27 | +``` |
| 28 | + |
| 29 | +### プロパティの追加・削除 |
| 30 | + |
| 31 | +動的な言語であるJavaScriptでは、オブジェクト作成後にプロパティを追加・削除できます。 |
| 32 | + |
| 33 | +```js-repl:2 |
| 34 | +> const config = { env: "production" }; |
| 35 | +undefined |
| 36 | +> config.port = 8080; // 追加 |
| 37 | +8080 |
| 38 | +> delete config.env; // 削除 |
| 39 | +true |
| 40 | +> config |
| 41 | +{ port: 8080 } |
| 42 | +``` |
| 43 | + |
| 44 | +## メソッドと this(復習) |
| 45 | + |
| 46 | +オブジェクトのプロパティには関数も設定できます。これを**メソッド**と呼びます。 |
| 47 | +第5章で学んだ通り、メソッド呼び出しにおける `this` は、「ドットの左側にあるオブジェクト(レシーバ)」を指します。 |
| 48 | + |
| 49 | +```js-repl:3 |
| 50 | +> const counter = { |
| 51 | +... count: 0, |
| 52 | +... increment: function() { |
| 53 | +... this.count++; |
| 54 | +... return this.count; |
| 55 | +... }, |
| 56 | +... // ES6からの短縮記法(推奨) |
| 57 | +... reset() { |
| 58 | +... this.count = 0; |
| 59 | +... } |
| 60 | +... }; |
| 61 | +undefined |
| 62 | +> counter.increment(); |
| 63 | +1 |
| 64 | +> counter.increment(); |
| 65 | +2 |
| 66 | +> counter.reset(); |
| 67 | +undefined |
| 68 | +> counter.count |
| 69 | +0 |
| 70 | +``` |
| 71 | + |
| 72 | +## プロトタイプとは何か? |
| 73 | + |
| 74 | +ここからが本章の核心です。JavaScriptのすべてのオブジェクトは、自身の親となる別のオブジェクトへの隠されたリンクを持っています。このリンク先のオブジェクトを**プロトタイプ**と呼びます。 |
| 75 | + |
| 76 | +オブジェクトからプロパティを読み取ろうとしたとき、そのオブジェクト自身がプロパティを持っていなければ、JavaScriptエンジンは自動的にプロトタイプを探しに行きます。 |
| 77 | + |
| 78 | +### `__proto__` と `Object.getPrototypeOf` |
| 79 | + |
| 80 | +歴史的経緯により、多くのブラウザで `obj.__proto__` というプロパティを通じてプロトタイプにアクセスできますが、現在の標準的な方法は `Object.getPrototypeOf(obj)` です。 |
| 81 | + |
| 82 | +```js-repl:4 |
| 83 | +> const arr = [1, 2, 3]; |
| 84 | +undefined |
| 85 | +> // 配列の実体はオブジェクトであり、Array.prototypeを継承している |
| 86 | +> Object.getPrototypeOf(arr) === Array.prototype |
| 87 | +true |
| 88 | +> // Array.prototypeの親はObject.prototype |
| 89 | +> Object.getPrototypeOf(Array.prototype) === Object.prototype |
| 90 | +true |
| 91 | +> // Object.prototypeの親はnull(チェーンの終端) |
| 92 | +> Object.getPrototypeOf(Object.prototype) |
| 93 | +null |
| 94 | +``` |
| 95 | + |
| 96 | +## プロトタイプチェーンによる継承の仕組み |
| 97 | + |
| 98 | +あるオブジェクトのプロパティにアクセスした際、JavaScriptは以下の順序で探索を行います。 |
| 99 | + |
| 100 | +1. そのオブジェクト自身(Own Property)が持っているか? |
| 101 | +2. 持っていなければ、そのオブジェクトのプロトタイプが持っているか? |
| 102 | +3. それでもなければ、プロトタイプのプロトタイプが持っているか? |
| 103 | +4. `null` に到達するまで繰り返し、見つからなければ `undefined` を返す。 |
| 104 | + |
| 105 | +この連鎖を**プロトタイプチェーン**と呼びます。クラス継承のように型定義をコピーするのではなく、**リンクを辿って委譲(Delegation)する**仕組みです。 |
| 106 | + |
| 107 | +以下のコードで、具体的な動作を確認してみましょう。 |
| 108 | + |
| 109 | +```js:prototype_chain.js |
| 110 | +const animal = { |
| 111 | + eats: true, |
| 112 | + walk() { |
| 113 | + console.log("Animal walks"); |
| 114 | + } |
| 115 | +}; |
| 116 | + |
| 117 | +const rabbit = { |
| 118 | + jumps: true, |
| 119 | + __proto__: animal // 注意: __proto__への代入は学習目的以外では非推奨 |
| 120 | +}; |
| 121 | + |
| 122 | +const longEar = { |
| 123 | + earLength: 10, |
| 124 | + __proto__: rabbit |
| 125 | +}; |
| 126 | + |
| 127 | +// 1. longEar自身は walk を持っていない -> rabbitを見る |
| 128 | +// 2. rabbitも walk を持っていない -> animalを見る |
| 129 | +// 3. animal が walk を持っている -> 実行 |
| 130 | +longEar.walk(); |
| 131 | + |
| 132 | +// 自身のプロパティ |
| 133 | +console.log(`Jumps? ${longEar.jumps}`); // rabbitから取得 |
| 134 | +console.log(`Eats? ${longEar.eats}`); // animalから取得 |
| 135 | + |
| 136 | +// プロパティの追加(シャドーイング) |
| 137 | +// longEar自身に walk を追加すると、animalの walk は隠蔽される |
| 138 | +longEar.walk = function() { |
| 139 | + console.log("LongEar walks simply"); |
| 140 | +}; |
| 141 | + |
| 142 | +longEar.walk(); |
| 143 | +``` |
| 144 | + |
| 145 | +```js-exec:prototype_chain.js |
| 146 | +Animal walks |
| 147 | +Jumps? true |
| 148 | +Eats? true |
| 149 | +LongEar walks simply |
| 150 | +``` |
| 151 | +
|
| 152 | +## Object.create() とコンストラクタ関数 |
| 153 | +
|
| 154 | +`__proto__` を直接操作するのはパフォーマンスや標準化の観点から推奨されません。プロトタイプを指定してオブジェクトを生成する正しい方法は2つあります。 |
| 155 | +
|
| 156 | +### 1\. Object.create() |
| 157 | +
|
| 158 | +指定したオブジェクトをプロトタイプとする新しい空のオブジェクトを生成します。 |
| 159 | +
|
| 160 | +```js-repl:5 |
| 161 | +> const proto = { greet: function() { return "Hello"; } }; |
| 162 | +undefined |
| 163 | +> const obj = Object.create(proto); |
| 164 | +undefined |
| 165 | +> obj.greet(); |
| 166 | +'Hello' |
| 167 | +> Object.getPrototypeOf(obj) === proto |
| 168 | +true |
| 169 | +``` |
| 170 | +
|
| 171 | +### 2\. コンストラクタ関数(new演算子) |
| 172 | +
|
| 173 | +ES6の `class` が登場する前、JavaScriptでは関数をコンストラクタとして使用し、`new` 演算子を使ってインスタンスを生成していました。これは現在でも多くのライブラリの内部で使用されている重要なパターンです。 |
| 174 | +
|
| 175 | + * 関数オブジェクトは `prototype` という特別なプロパティを持っています(`__proto__`とは別物です)。 |
| 176 | + * `new Func()` すると、作られたインスタンスの `__proto__` に `Func.prototype` がセットされます。 |
| 177 | +
|
| 178 | +```js:constructor_pattern.js |
| 179 | +// コンストラクタ関数(慣習として大文字で始める) |
| 180 | +function User(name) { |
| 181 | + // this = {} (新しい空のオブジェクトが暗黙的に生成される) |
| 182 | + this.name = name; |
| 183 | + // return this (暗黙的にこのオブジェクトが返される) |
| 184 | +} |
| 185 | + |
| 186 | +// すべてのUserインスタンスで共有したいメソッドは |
| 187 | +// User.prototype に定義する(メモリ節約のため) |
| 188 | +User.prototype.sayHi = function() { |
| 189 | + console.log(`Hi, I am ${this.name}`); |
| 190 | +}; |
| 191 | + |
| 192 | +const user1 = new User("Alice"); |
| 193 | +const user2 = new User("Bob"); |
| 194 | + |
| 195 | +user1.sayHi(); |
| 196 | +user2.sayHi(); |
| 197 | + |
| 198 | +// 仕組みの確認 |
| 199 | +console.log(user1.__proto__ === User.prototype); // true |
| 200 | +console.log(user1.sayHi === user2.sayHi); // true (同じ関数を共有している) |
| 201 | +``` |
| 202 | + |
| 203 | +```js-exec:constructor_pattern.js |
| 204 | +Hi, I am Alice |
| 205 | +Hi, I am Bob |
| 206 | +true |
| 207 | +true |
| 208 | +``` |
| 209 | + |
| 210 | +> **重要な区別:** |
| 211 | +> |
| 212 | +> * `obj.__proto__`: オブジェクトの実の親(リンク先)。 |
| 213 | +> * `Func.prototype`: その関数を `new` したときに、生成されるインスタンスの `__proto__` に代入される**テンプレート**。 |
| 214 | +
|
| 215 | +## この章のまとめ |
| 216 | + |
| 217 | +1. JavaScriptはクラスベースではなく、**プロトタイプベース**の継承を行う。 |
| 218 | +2. オブジェクトは隠しプロパティ(`[[Prototype]]`)を持ち、プロパティが見つからない場合にそこを探索する(プロトタイプチェーン)。 |
| 219 | +3. `Object.create(proto)` は、特定のプロトタイプを持つオブジェクトを直接生成する。 |
| 220 | +4. コンストラクタ関数と `new` 演算子を使うと、`Func.prototype` を親に持つインスタンスを生成できる。これがJavaなどの「クラス」に近い振る舞いを模倣する仕組みである。 |
| 221 | + |
| 222 | +## 練習問題1: 基本的なプロトタイプ継承 |
| 223 | + |
| 224 | +`Object.create()` を使用して、以下の要件を満たすコードを書いてください。 |
| 225 | + |
| 226 | +1. `robot` オブジェクトを作成し、`battery: 100` というプロパティと、バッテリーを10減らして残量を表示する `work` メソッドを持たせる。 |
| 227 | +2. `robot` をプロトタイプとする `cleaningRobot` オブジェクトを作成する。 |
| 228 | +3. `cleaningRobot` 自身に `type: "cleaner"` というプロパティを追加する。 |
| 229 | +4. `cleaningRobot.work()` を呼び出し、正しく動作(プロトタイプチェーンの利用)を確認する。 |
| 230 | + |
| 231 | +```js:practice6_1.js |
| 232 | +``` |
| 233 | + |
| 234 | +```js-exec:practice6_1.js |
| 235 | +``` |
| 236 | + |
| 237 | +### 練習問題2: コンストラクタ関数 |
| 238 | + |
| 239 | +コンストラクタ関数 `Item` を作成してください。 |
| 240 | + |
| 241 | +1. `Item` は引数 `name` と `price` を受け取り、プロパティとして保持する。 |
| 242 | +2. `Item.prototype` に `getTaxIncludedPrice` メソッドを追加する。これは税率10%を加えた価格を返す。 |
| 243 | +3. `new Item("Apple", 100)` でインスタンスを作成し、税込価格が110になることを確認する。 |
| 244 | + |
| 245 | +```js:practice6_2.js |
| 246 | +``` |
| 247 | + |
| 248 | +```js-exec:practice6_2.js |
| 249 | +``` |
| 250 | + |
0 commit comments