2024年1月24日

プロトタイプメソッド、__proto__ を持たないオブジェクト

このセクションの最初の章で、プロトタイプを設定する最新の方法があることを述べました。

obj.__proto__ を使用してプロトタイプを設定または読み取ることは、時代遅れであり、やや非推奨(JavaScript標準のいわゆる「Annex B」に移動され、ブラウザ専用)とみなされています。

プロトタイプを取得/設定する最新の方法は次のとおりです

非推奨とされていない __proto__ の唯一の使い方は、新しいオブジェクトを作成するときのプロパティとして使用する { __proto__: ... } です。

ただし、これにも特別なメソッドがあります

  • Object.create(proto[, descriptors]) – 指定された proto[[Prototype]] として持つ空のオブジェクトと、オプションのプロパティ記述子を作成します。

例:

let animal = {
  eats: true
};

// create a new object with animal as a prototype
let rabbit = Object.create(animal); // same as {__proto__: animal}

alert(rabbit.eats); // true

alert(Object.getPrototypeOf(rabbit) === animal); // true

Object.setPrototypeOf(rabbit, {}); // change the prototype of rabbit to {}

Object.create メソッドは、オプションの第2引数としてプロパティ記述子を持つため、少し強力です。

次のように、新しいオブジェクトに追加のプロパティを提供できます

let animal = {
  eats: true
};

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

alert(rabbit.jumps); // true

記述子は、プロパティフラグと記述子の章で説明されているものと同じ形式です。

Object.create を使用して、for..in でプロパティをコピーするよりも強力なオブジェクトクローンを作成できます。

let clone = Object.create(
  Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)
);

この呼び出しは、列挙可能および列挙不可能なすべてのプロパティ、データプロパティとセッター/ゲッターを含む、すべてを、正しい [[Prototype]] で、obj の完全に正確なコピーを作成します。

簡単な歴史

[[Prototype]] を管理する方法はたくさんあります。これはどうして起きたのでしょうか?なぜ?

これは歴史的な理由によるものです。

プロトタイプ継承は言語の初期から存在していましたが、それを管理する方法は時間の経過とともに進化しました。

  • コンストラクタ関数の prototype プロパティは、非常に古い時代から機能しています。これは、指定されたプロトタイプを持つオブジェクトを作成する最も古い方法です。
  • その後、2012年に Object.create が標準に登場しました。これにより、指定されたプロトタイプを持つオブジェクトを作成する機能が提供されましたが、それを取得/設定する機能は提供されませんでした。一部のブラウザは、ユーザーがいつでもプロトタイプを取得/設定できるように、開発者に柔軟性を提供するために、非標準の __proto__ アクセサーを実装しました。
  • その後、2015年に、__proto__ と同じ機能を実行するために、Object.setPrototypeOfObject.getPrototypeOf が標準に追加されました。__proto__ は事実上どこにでも実装されていたため、非推奨となり、標準の Annex B に組み込まれました。つまり、ブラウザ以外の環境ではオプションです。
  • その後、2022年に、オブジェクトリテラル {...}__proto__ を使用することが正式に許可されました(Annex B から移動)。ただし、getter/setter obj.__proto__ としては使用できません(まだ Annex B にあります)。

なぜ __proto__ は関数 getPrototypeOf/setPrototypeOf に置き換えられたのでしょうか?

なぜ __proto__ は部分的に回復し、{...} での使用が許可されたのでしょうか。ただし、getter/setter としては許可されないのでしょうか?

それは興味深い質問であり、__proto__ がなぜ悪いのかを理解する必要があります。

すぐに答えを得るでしょう。

スピードが重要な場合は、既存のオブジェクトの [[Prototype]] を変更しないでください

技術的には、いつでも [[Prototype]] を取得/設定できます。ただし、通常はオブジェクト作成時に一度だけ設定し、それ以上変更しません。rabbitanimal から継承し、それは変更されません。

そして、JavaScriptエンジンはこれに対して高度に最適化されています。Object.setPrototypeOf または obj.__proto__= を使用して「オンザフライ」でプロトタイプを変更すると、オブジェクトプロパティアクセス操作の内部最適化が壊れるため、非常に遅い操作になります。そのため、何をしているのかを理解しているか、JavaScriptの速度がまったく問題にならない場合を除いて、避けてください。

「非常にシンプルな」オブジェクト

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

…ただし、ユーザーが提供する キーを格納しようとすると(たとえば、ユーザーが入力した辞書)、興味深い不具合が発生する可能性があります。"__proto__" を除くすべてのキーは正常に機能します。

例をご覧ください

let obj = {};

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

alert(obj[key]); // [object Object], not "some value"!

ここで、ユーザーが __proto__ を入力すると、4行目の割り当ては無視されます!

それは開発者でない人にとっては確かに驚くべきことですが、私たちにとっては非常に理解できます。__proto__ プロパティは特別です。オブジェクトまたは null のいずれかである必要があります。文字列はプロトタイプになることはできません。そのため、文字列を __proto__ に割り当てると無視されます。

しかし、私たちはそのような動作を実装するつもりはなかったはずです。キー/値のペアを格納したいだけであり、"__proto__" という名前のキーは適切に保存されませんでした。つまり、これはバグです!

ここでは、結果はそれほどひどくありません。しかし、他のケースでは、obj に文字列の代わりにオブジェクトを格納することがあり、その場合、プロトタイプが実際に変更されます。その結果、実行がまったく予期しない方法で間違ってしまうことになります。

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

obj.toString に割り当てる際にも予期しないことが起こる可能性があります。これは組み込みのオブジェクトメソッドであるためです。

この問題を回避するにはどうすればよいでしょうか?

まず、プレーンなオブジェクトの代わりにストレージに Map を使用するように切り替えるだけで、すべて問題ありません

let map = new Map();

let key = prompt("What's the key?", "__proto__");
map.set(key, "some value");

alert(map.get(key)); // "some value" (as intended)

…しかし、Object 構文はより簡潔であるため、より魅力的であることがよくあります。

幸いなことに、言語作成者がずっと前にその問題について考えていたため、オブジェクトを使用することができます。

ご存知のように、__proto__ はオブジェクトのプロパティではなく、Object.prototype のアクセサープロパティです

そのため、obj.__proto__ が読み取られたり設定されたりすると、対応する getter/setter がそのプロトタイプから呼び出され、[[Prototype]] を取得/設定します。

このチュートリアルセクションの冒頭で述べたように、__proto__[[Prototype]] にアクセスする方法であり、[[Prototype]] 自体ではありません。

ここで、連想配列としてオブジェクトを使用し、そのような問題から解放されたい場合は、ちょっとしたトリックを使ってそれを行うことができます

let obj = Object.create(null);
// or: obj = { __proto__: 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 = "你好";
chineseDictionary.bye = "再见";

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

まとめ

  • 指定されたプロトタイプを持つオブジェクトを作成するには、次を使用します

    • リテラル構文: { __proto__: ... }、複数のプロパティを指定できます
    • または Object.create(proto[, descriptors])、プロパティ記述子を指定できます。

    Object.create は、すべての記述子を持つオブジェクトをシャローコピーする簡単な方法を提供します

    let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
  • プロトタイプを取得/設定する最新の方法は次のとおりです

  • 組み込みの __proto__ ゲッター/セッターを使用してプロトタイプを取得/設定することは推奨されていません。現在、仕様の Annex B にあります。

  • Object.create(null) または {__proto__: null} で作成されたプロトタイプのないオブジェクトについても説明しました。

    これらのオブジェクトは、任意の(おそらくユーザーが生成した)キーを格納するための辞書として使用されます。

    通常、オブジェクトは Object.prototype から組み込みメソッドと __proto__ ゲッター/セッターを継承し、対応するキーを「占有」し、潜在的に副作用を引き起こします。null プロトタイプを使用すると、オブジェクトは真に空になります。

タスク

重要度: 5

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

dictionary.toString() メソッドをそこに追加します。これは、キーのコンマ区切りのリストを返す必要があります。あなたの toString はオブジェクトに対する for..in に表示されるべきではありません。

これがその動作方法です

let dictionary = Object.create(null);

// your code to add dictionary.toString method

// add some data
dictionary.apple = "Apple";
dictionary.__proto__ = "test"; // __proto__ is a regular property key here

// only apple and __proto__ are in the loop
for(let key in dictionary) {
  alert(key); // "apple", then "__proto__"
}

// your toString in action
alert(dictionary); // "apple,__proto__"

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

toString を列挙不可能にするために、プロパティ記述子を使用して定義しましょう。Object.create の構文では、プロパティ記述子を持つオブジェクトを2番目の引数として提供できます。

let dictionary = Object.create(null, {
  toString: { // define toString property
    value() { // the value is a function
      return Object.keys(this).join();
    }
  }
});

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

// apple and __proto__ is in the loop
for(let key in dictionary) {
  alert(key); // "apple", then "__proto__"
}

// comma-separated list of properties by 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
チュートリアルマップ

コメント

コメントする前に以下をお読みください…
  • 改善点に関する提案がある場合は、コメントする代わりにGitHub issueを提出するか、プルリクエストを送信してください。
  • 記事の内容で理解できないことがある場合は、詳しく説明してください。
  • 数語のコードを挿入するには、<code>タグを使用してください。複数行の場合は、<pre>タグで囲んでください。10行を超える場合は、サンドボックス (plnkr, jsbin, codepen…) を使用してください。