メインコンテンツへ移動

prototype, __proto__, constructor, instanceofのルーツ

無料2022-01-04#JS#__proto__和prototype#原型和原型链#prototype和constructor#prototype和instanceof#Javascript Object Layout

prototype, __proto__, constructor, instanceofのルーツを徹底的に理解する

JavaScriptのプロトタイプチェーンといえば、有名な「古の神図」があります。

(上の図はJavaScript Object Layoutより)

図の中の関係から見ていきましょう

関数の2つのグループ、オブジェクトの3つのグループ、そして派生した2つのグループ、図には合計7つの関係があります。

まずは関数から:

/**
 * 関数の2つの関係
 */
function Cat() {}
// 第0グループの関係
// インスタンスのconstructorはコンストラクタ
new Cat().constructor === Cat
// コンストラクタのconstructorは、コンストラクタを作成したものです
Cat.constructor === Function

// 第1グループの関係:インスタンスの__proto__はコンストラクタのprototypeを指す
// このグループは覚えやすいです。プロトタイプチェーン継承の重要な操作を思い出してください。
new Cat().__proto__ === Cat.prototype
// 第2グループの関係:コンストラクタのprototypeのconstructorは自身を指す
Cat.prototype.constructor === Cat

次にオブジェクトを見てみましょう:

/**
 * オブジェクトの3つの関係
 */
// 第1グループの関係は依然として成立します
new Object().__proto__ === Object.prototype
// 第2グループの関係も依然として成立します
Object.prototype.constructor === Object
// 第3グループの関係:Objectのprototypeの__proto__はnullである
Object.prototype.__proto__ === null

最後は派生したものです:

/**
 * 派生した2つの関係
 */
// 第4グループの関係:コンストラクタはnew Functionによって得られる
// したがって、コンストラクタの__proto__はFunctionのprototypeを指す
Cat.__proto__ === Function.prototype
// 同上
Object.__proto__ === Function.prototype
// 第5グループの関係:本来は同上であるはずですが、特殊なので別に扱います
// Functionもnew Functionによって得られます!!
// Function.constructor === Function、つまりFunctionのコンストラクタは自分自身です。したがって
// Functionの__proto__はFunctionのprototypeを指す
Function.__proto__ === Function.prototype
// 第6グループの関係:Functionのprototypeの__proto__はObjectのprototypeを指す
Function.prototype.__proto__ === Object.prototype

prototype、__proto__、constructorの三角関係をどう理解するか?

このように理解してください:

  • コンストラクタはインスタンスを作成する機械である
  • インスタンスが作成過程でどのような属性を得られるかは、機械がどのような属性を隠し持っているかによって決まる
  • それらの属性はコンストラクタのprototype属性の中に隠されている(これをプロトタイプオブジェクトと呼ぶ)
  • インスタンスをnewする際、インスタンスに__proto__を付けることで、インスタンスがそれらの属性を辿って見つけられるようにする

特殊なケース:

  • コンストラクタとそのprototypeは双方向の関係にあり、逆に指し示す属性をconstructorと呼ぶ

prototypeと__proto__をどう区別するか?

本質的に、決定的な違いは一点だけです:誰が使うか

  • prototypeはインスタンスが使うためのプロトタイプオブジェクトであり、コンストラクタだけがprototypeを持つ
  • __proto__は自身のプロトタイプオブジェクトを指し示す属性であり、すべてのオブジェクトが__proto__を持つ
  • x.prototypex.__proto__ の違いは、前者はインスタンスが使うためのもの(new xしない限り役に立たない)であり、後者は自分自身が使うためのものであること

どちらもプロトタイプに関わるものですが、名前をどう区別すればよいでしょうか?

  • prototypeは(サブクラスの)インスタンスが使う属性をひとまとめにしたもので、プロトタイプオブジェクトと呼ぶ
  • __proto__はプロトタイプチェーンを繋ぎ合わせるもので、便宜上プロトタイプと呼ぶ

instanceofはインスタンスとクラス(コンストラクタ)の関係をどう判別しているか?

MDNには、instanceofについて非常に的確に説明している一文があります。

The instanceof operator tests to see if the prototype property of a constructor appears anywhere in the prototype chain of an object.

instanceofより引用)

翻訳はしません。一文字変えるだけでも蛇足に感じられるからです。この一文に従えば、自分でinstanceofを実装することができます:

function insOf(obj, Ctor) {
  let proto = obj;

  // while (proto = obj.__proro__) {
  while (proto = Object.getPrototypeOf(proto)) {
    if (Ctor.prototype === proto) {
      return true;
    }
  }
  return false;
}

// Case 1
class A {}
insOf(new A(), A)
// Case 2
function Cat() {}
insOf(new Cat(), Cat)
insOf(new Cat(), Object)

constructorと継承について少し深く掘り下げる

第0グループの関係で、インスタンスのconstructorはコンストラクタであることを学びました:

// インスタンスのconstructorはコンストラクタ
new Cat().constructor === Cat

では、インスタンスのconstructor属性はどこから来るのでしょうか?

その通り、new Cat()でインスタンスを作成する際に、コンストラクタのプロトタイプオブジェクトからコピーされてきたものです:

new Cat().constructor === Cat.prototype.constructor

したがって、伝統的なES5の継承では:

function extend(Sub, Super) {
  // 1. Super.prototypeを匿名オブジェクトのプロトタイプに包み込み、その匿名オブジェクトを返す
  var proto = Object.create(Super.prototype);
  // 2. constructor属性を修正する
  proto.constructor = Sub;
  // 3. サブクラスのインスタンスに匿名オブジェクトのプロトタイプ属性へのアクセス権を与える
  Sub.prototype = proto;
}

constructorの向きを変更するかどうかは、instanceof演算子の判定結果には影響しません。修正するのはあくまでconstructor属性の値を正しくするためです。

// constructorを修正しない場合、サブクラスのインスタンスのconstructorはBではなく、依然としてAのままになります
function Sub() {}
function Super() {}
var proto = Object.create(Super.prototype);
Sub.prototype = proto;
// これでは非常に不自然です(インスタンスのconstructorがコンストラクタではなくなってしまいます)
new Sub().constructor !== Sub;
// そのため、修正します
proto.constructor = Sub;
// 正常になりました
new Sub().constructor === Sub;

いくつかの豆知識

1. アロー関数にはプロトタイプオブジェクトがない(そのため、アロー関数はコンストラクタとして使用できない)

(() => 1).prototype === undefined

2. ネイティブオブジェクトのプロトタイプについては、あまり深入りしないほうがいいでしょう。不確かな部分があります

Math.__proto__ === Object.prototype
Math.max.__proto__ === Function.prototype
Window.__proto__ === EventTarget
console.__proto__ === ?

3. 関数のprototypeを比較することにどのような意味があるか?

npmでダウンロード数が多いisモジュールの中に、古のコードの一行があります:

if (type === '[object Function]') {
  return value.prototype === other.prototype;
}

見間違いではありません。彼は2つの関数のprototypeを比較し、それを根拠に関数の同一性を判定しています。少し考えれば、あまり信頼できないことがわかります。例えば:

(x => x).prototype === (x => x + 1).prototype === undefined

そこで、ライブラリの作者に尋ねてみました。何度かやり取りした結果、prototypeの比較は、コンストラクタやクラスが同じかどうかを判定するのに使えることがわかりました(ただし、prototypeは改ざん可能であることに注意が必要です):

class Dog {}
class Cat {}
function Button() {}
function Panel() {}
Dog.prototype !== Cat.prototype
Button.prototype !== Panel.prototype

関数がコンストラクタやクラスでない場合、prototypeを比較することには全く意味がありません。OOPがあまり一般的でなかったJSにおいて、ライブラリ内での関数のprototype比較の役割は非常に限定的です。

コメント

コメントはまだありません

コメントを書く