このチャプターでは、プロトタイプを操作する追加のメソッドを説明します。

私たちがすでに知っている方法以外にも、プロトタイプを get/set する方法があります。

例:

let animal = {
  eats: true
};

// animal をプロトタイプとして新しいオブジェクトを作成する
let rabbit = Object.create(animal);

alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // rabbit のプロトタイプを取得

Object.setPrototypeOf(rabbit, {}); // rabbit のプロトタイプを {} に変更

Object.create は任意の2つ目の引数を持っています: プロパティディスクリプタです。私たちはこのように、新しいオブジェクトに追加のプロパティを提供することができます。:

let animal = {
  eats: true
};

let rabbit = Object.create(animal, {
  jumps: {
    value: true
  }
});

alert(rabbit.jumps); // true

ディスクリプタはチャプター プロパティフラグとディスクリプタ で説明したのと同じフォーマットです。

for..in でプロパティをコピーするよりも、よりパワフルにオブジェクトをクローンするために Object.create を使うことができます。:

// 完全に同一の obj の浅いクローン
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

この呼び出しはすべてのプロパティを含む obj の本当の正確なコピーを作ります。: 列挙型、非列挙型、データプロパティ、setter/getter – すべてと、正確な [[Prototype]] です。

略史

もし [[Prototype]] を管理するすべての方法を数えると、たくさんあります! 同じことをするたくさんの方法があります!

なぜでしょう?

それは歴史的な理由のためです。

  • コンストラクタ関数の "prototype" プロパティは非常に古代から機能しています。
  • 2012年後半: Object.create が標準に登場しました。それは与えられたプロトタイプでオブジェクトを作りますが、それを取得/設定することはできませんでした。なのでブラウザはいつでもプロトタイプの取得/設定ができる非標準の __proto__ アクセサを実装しました。
  • 2015年後半: Object.setPrototypeOfObject.getPrototypeOf が標準に追加されました。__proto__ はどこでも実行されているデファクトだったので、標準の付録Bに追加されています。これはブラウザ以外の環境ではオプションです。

今のところ、私たちはすべての方法を自由に使い分けています。

技術的には、いつでも [[Prototype]] の取得/設定が可能です。しかし、通常はオブジェクト作成時に一度だけ設定を行い、変更はしません。: rabbitanimal から継承しており、それは変更しません。また、JavaScriptエンジンは高度に最適化されています。Object.setPrototypeOf または obj.__proto__= で “その場で” プロトタイプを変更することは、可能ですがとても遅い操作になります。

“非常にシンプルな” オブジェクト

ご存知の通り、オブジェクトはキー/値ペアを格納するための連想配列として使うことができます。

…しかし、もしその中で ユーザから提供された キーを格納しようとした場合(例えばユーザが入力した辞書)、興味深い問題が起こります。: すべてのキーは "__proto__" を除いてうまく動作します。

例を確認してみましょう:

let obj = {};

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // [object Object], "some value" ではありません!

もしユーザが __proto__ を入力した場合、その代入は無視されます!

それは驚くことではありません。__proto__ プロパティは特別です: それはオブジェクトまたは null であり、文字列はプロトタイプにはなれません。

しかし、このような振る舞いを実装するつもりはありませんでした。私たちはキー/値ペアを格納したいですが、キー名が "__proto__" の場合は正しく保存されませんでした。なので、これはバグです。ここでの結果はひどくはありませんが、他のケースでは、プロトタイプは実際に変更される可能性があるため、処理が予期しない方向で間違ってしまう可能性があります。

最悪なのは – 通常開発者はこのような可能性について全く考えません。これにより、このようなバグに気付きにくくなり、特にJavaScriptがサーバー側で使用されている場合には、それらを脆弱性に変えることさえあります。

このようなことは __proto__ の場合にのみ起こります。すべての他のプロパティは通常 “代入可能” です。

どうやってこの問題を回避しましょう?

最初に、Map を使うよう切り替えることができます。それですべて問題ありません。

しかし、 言語の作成者がその問題をずっと前から考えていたため、Object もまたここでは上手くいきます。

__proto__ はオブジェクトのプロパティではなく、Object.prototype のアクセサプロパティです。:

なので、もし obj.__proto__ が読み込まれたり代入された場合、該当の getter/setter がそのプロトタイプから呼ばれ、それは [[Prototype]] の取得/設定をします。

最初に言ったとおり、__proto__[[Prototype]] にアクセスする方法であり、[[Prototype]] 自身ではありません。

今、もし連想配列としてオブジェクトを使いたい場合、リテラルのトリックを使ってそれを行う事ができます。:

let obj = Object.create(null);

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // "some value"

Object.create(null) はプロトタイプなし([[Prototype]]null)の空オブジェクトを作ります。:

したがって、__proto__ のための継承された getter/setter はありません。今や通常のデータプロパティとして処理されますので、上の例は正しく動作します。

このようなオブジェクトを “非常にシンプルな” または “純粋な辞書オブジェクト” と呼びます。なぜなら、それらは通常のオブジェクト {...} よりもシンプルなためです。

欠点は、そのようなオブジェクトには組み込みのオブジェクトメソッドがないことです。 toString:

let obj = Object.create(null);

alert(obj); // Error (no toString)

…しかし、連想配列ではそれは通常問題ありません。

ほとんどのオブジェクトに関連したメソッドは Object.keys(obj) のように Object.something(...) であることに注意してください。 – それらはプロトタイプにはないので、このようなオブジェクトで機能し続けます。:

let chineseDictionary = Object.create(null);
chineseDictionary.hello = "ni hao";
chineseDictionary.bye = "zai jian";

alert(Object.keys(chineseDictionary)); // hello,bye

すべてのプロパティを取得する

オブジェクトから キー/値 を取得する多くの方法があります。

それらの1つはすでに知っています。:

  • Object.keys(obj) / Object.values(obj) / Object.entries(obj) – 列挙可能な自身の文字列プロパティ名/値/キー値ペアの配列を返します。それらのメソッドは 列挙可能な プロパティで、キーとして文字列を持つ ものだけをリストします。

もしもシンボリックプロパティがほしい場合は:

非列挙型のプロパティが欲しい場合は:

すべて のプロパティが欲しい場合は:

これらのメソッドは、どのプロパティが返されるかについて少し異なりますが、すべてがそのオブジェクト自身で動作します。プロトタイプからのプロパティはリストされません。

for..in ループは異なります。: それは継承されたプロパティもループします。

例:

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// 自身のキーのみ
alert(Object.keys(rabbit)); // jumps

// 継承されたキーも
for(let prop in rabbit) alert(prop); // jumps, eats

もし継承されたプロパティを区別したい場合、組み込みのメソッド obj.hasOwnProperty(key) があります。: それは obj 自身が key という名前のプロパティをもつ(継承でない) 場合に true を返します。

だから、継承されたプロパティをフィルタリングすることができます。:

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

for(let prop in rabbit) {
  let isOwn = rabbit.hasOwnProperty(prop);
  alert(`${prop}: ${isOwn}`); // jumps:true, then eats:false
}

ここでは、私たちは次の継承のチェーンを持っています。: rabbit, 次に animal そして Object.prototype (animal はリテラルオブジェクト {...} なので、これはデフォルトです)、その上に null があります。:

面白いことが1つあります。メソッド rabbit.hasOwnProperty はどこからきたでしょう?チェーンを見ると、Object.prototype.hasOwnProperty によってメソッドが提供されていることがわかります。言い換えると、それは継承されています。

…しかしなぜ hasOwnPropertyfor..in ループで現れないのでしょうか?答えはシンプルです。それは列挙可能ではありません。 Object.prototypeの他のすべてのプロパティと同様です。だからこそそれらはリストに載っていません。

サマリ

このチャプターで説明したメソッドの簡単なリストを要約として示します。:

私たちは __proto__[[Prototype]] の getter/setterであり、他のメソッドと同様に Object.prototype に存在することも明らかにしました。

私たちは、Object.create(null) によってプロトタイプなしのオブジェクトを作ることができます。このようなオブジェクトは “純粋な辞書” として使われ、キーとして "__proto__" の問題はありません。

オブジェクトプロパティ(Object.keys など)を返すすべてのメソッドは – “自身の” プロパティを返します。もし継承されたものが欲しい場合は、for..in を使います。

タスク

重要性: 5

任意の key/value ペアを格納するために Object.create(null) として生成されたオブジェクト dictonary があります。

その中にメソッド dictionary.toString() を追加してください。それはカンマ区切りのキーのリストを返します。あなたの toString はオブジェクト上の for..in で現れるべきではありません。

次のように動作します:

let dictionary = Object.create(null);

// dictionary.toString メソッドを追加するあなたのコード

// データの追加
dictionary.apple = "Apple";
dictionary.__proto__ = "test"; // __proto__ はここでは通常のプロパティキー

// ループでは apple と __proto__ だけです
for(let key in dictionary) {
  alert(key); // "apple", then "__proto__"
}

// 実行時のあなたの toString です
alert(dictionary); // "apple,__proto__"

このメソッドは Object.keys を使ってすべての列挙可能なキーを取り、そのリストを出力します。

toString を非列挙型にするために、プロパティディスクリプタを使って定義しましょう。Object.create の構文は、2番目の引数としてプロパティディスクリプタのオブジェクトを指定することができます。

let dictionary = Object.create(null, {
  toString: { // toString プロパティの定義
    value() { // 値は関数です
      return Object.keys(this).join();
    }
  }
});

dictionary.apple = "Apple";
dictionary.__proto__ = "test";

// ループでは apple と __proto__ だけです
for(let key in dictionary) {
  alert(key); // "apple", then "__proto__"
}

// toString によるカンマ区切りのプロパティのリスト
alert(dictionary); // "apple,__proto__"

ディスクリプタを使ってプロパティを作成するとき、そのフラグはデフォルトでは false です。なので、上のコードで。dictionary.toString は非列挙型です。

重要性: 5

新しい rabbit オブジェクトを作りましょう:

function Rabbit(name) {
  this.name = name;
}
Rabbit.prototype.sayHi = function() {
  alert(this.name);
};

let rabbit = new Rabbit("Rabbit");

これらの呼び出しは同じことをしますか?それとも違う?

rabbit.sayHi();
Rabbit.prototype.sayHi();
Object.getPrototypeOf(rabbit).sayHi();
rabbit.__proto__.sayHi();

最初の呼び出しは、this == rabbit を持っており、他のものは thisRabbit.prototype と等しいものを持っています。なぜなら、それは実際にドットの前のオブジェクトだからです。

従って、最初の呼び出しのみ Rabbit を表示し、それ以外は undefined を表示します。:

function Rabbit(name) {
  this.name = name;
}
Rabbit.prototype.sayHi = function() {
  alert( this.name );
}

let rabbit = new Rabbit("Rabbit");

rabbit.sayHi();                        // Rabbit
Rabbit.prototype.sayHi();              // undefined
Object.getPrototypeOf(rabbit).sayHi(); // undefined
rabbit.__proto__.sayHi();              // undefined
チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。