5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScriptの.__proto__とprototypeとinstance

Last updated at Posted at 2022-07-14

初めに

inheritanceについて書くとき、自分がプロトタイプチェーンへの認識が足りないと認識したから、もう一度調べてまとめてみました。読みやすくなるため相関図も作ってみました。
主な参考資料はこちらです↓
How does JavaScript .prototype work? - stackoverflow
__ proto __ VS. prototype in JavaScript

自分の考えと考察も入れてこの文章を書きましたので、自分勝手の解釈も含みます。

まずは名詞の解釈について:

  • .__proto__:プロトタイプチェーンの参照(reference)、プロトタイプチェーンの本体です。(隠しプロパティ[[prototype]]を暴くアクセサープロパティ)
  • prototype:原型であり継承(親)オブジェクトの参照(reference)でもあります。組み込みメソッド(built-in function)もコンストラクタ関数(constructor function)にも各自の参照元があります。
  • instance:インスタンス、複製先。
  • constructor:コンストラクタ、構築子、複製元を記録するオブジェクト。

TL;DR

  • instance(インスタンス) ⇒ コンストラクタ関数のなかみを完全に複製したもの。
  • .__proto__ ⇒ プロトタイプチェーンの参照先として、インスタンス自体持ってないプロパティやメソッドを、見つけるまで上へを辿っていく。(なかった場合はundefined
  • prototype ⇒ (もとからあるFunction.prototypeObject.prototypenullを除いて)コンストラクタ関数の創り出されると同時にprototypeが形成され、インスタンスの.__proto__になる。

今回デモのコードの相関図

relationship1.png
初めからこの図読むとこれ何なのか分からないと思います。
一番最後にまた読みましょう。

ここから、なぜ先ほど名詞の解釈がそうなったのか、図と検証で説明したいと思います。

.__ proto __ & prototype

.__proto__はプロトタイプチェーンの参照先、プロトタイプチェーンの本体です。
.__proto__は定義上ではprototypeとまったく違うものです。でもよく間違われるのは、両者が判定がtrueになるためではないかと。

最初prototypeについて書くとき、この二つの違いが何となく分かるけど、決定的な何かが分かりません。(笑)少し検証したけれど、何かいけそうで文章を書いたが、実際この二つを混同して語ってたことが今になって気づきましたが。

なので、ここから両者の違いを検証から入りたいと思います。

function Person() { }
const newPerson = new Person()

console.log(Person.__proto__) // {}
console.log(Person.prototype) // {}

console.log(Person.__proto__ === Person.prototype) // false
console.log(Person.__proto__ === Function.prototype) // true

console.log(Object.getPrototypeOf(Person) === Person.prototype) // false
console.log(Object.getPrototypeOf(Person) === Function.prototype) // true

コンストラクタ関数Person.__ proto __prototypeが同じく{}をログしたが、見た目は同じだが、実際は別物です。(隠しプロパティ[[property]]の中身が違うから)

.__proto__が非推奨になったのでObject.getPrototypeOf()を使い[[prototype]]の.__proto__プロパティを見つけ出します。もちろん結果は同じですが。
(正直この関数の名前からするとprototypeを探すことだと誤解しがちですね。)

Person.__ proto __、実はFunction.prototypeです。
ではこのFunction.prototype一体何でしょうか?
ここは検証は長い~長い~ことになるから後に付けるから図を見てみましょう。
relationship2.png
Function.prototypeは、Functionprototypeです。(説明になってないじゃんー!)
ちょっとお待ちを!ここでprototypeの解釈をもう一度見てください!
prototype:原型であり継承(親)オブジェクトの参照(reference)でもあります。組み込みメソッド(built-in function)もコンストラクタ関数(constructor function)にも各自の参照元があります。
prototypeは参照元だから、Function.prototypeはつまりJavaScriptで使えるすべてFunction(built-in function)の参照元ということです。

どうしてそんなに言い切れるんでしょうか?
実際に検証してみたらその関連性を発覚しました。

console.log(String.__proto__ === Function.prototype) // true
console.log(Number.__proto__ === Function.prototype) // true
console.log(BigInt.__proto__ === Function.prototype) // true
console.log(Boolean.__proto__ === Function.prototype) // true
// console.log(undefined.__proto__ === Function.prototype)
// TypeError: Cannot read property '__proto__' of undefined
console.log(Symbol.__proto__ === Function.prototype) // true
// console.log(null.__proto__ === Function.prototype)
// TypeError: Cannot read property '__proto__' of undefined

console.log(Object.__proto__ === Function.prototype) // true
console.log(Array.__proto__ === Function.prototype) // true
console.log(Function.__proto__ === Function.prototype) // true

console.log(Person.__proto__ === Person.prototype) // false
console.log(Person.__proto__ === Function.prototype) // true
console.log(Person.prototype) // {}
console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true

(undefinedとnullにはそもそも.__proto__プロパティがないため利用できるメソッドはないということも分かりました。)

console.log(Person.__proto__ === Person.prototype) // false以外、すべての.__proto__Function.prototypeでした。

ここで気を付けることが二つあります。
.__proto__prototypeと意味上イコールではないこと。
.__proto__はあくまでも参照先を示すこと。

コンストラクタ関数Personの原型は間違いなくPerson.prototypeですが、このprototypeのなかに何も入っていません。空のオブジェクト{}です。もしPerson.prototypeが関数Personのプロトタイプチェーンであれば、JavaScriptの組み込みメソッド(built-in function)が使えません。なぜなら、Person.prototypeの上はObject.prototypeになるから、つまりメソッドを持つFunction.prototypeがスキップされることになります。

なのでconsole.log(Person.__proto__ === Function.prototype) // trueで分かったように、.__proto__チェーンの参照として、JavaScriptの元からある組み込みメソッドを使わせるためにプロトタイプチェーンの参照元を示しました。

これで〇〇.prototype.__proto__(参照先)になりますが、言葉の意味としてイコールではないことが分かりやすくなると思います。
relationship3.png
プリミティブもオブジェクトもすべての.__proto__Function.prototypeなので組み込みメソッド(built-in function)を継承して使えるようになったのです。

ではPersonのインスタンスnewPersonはどうなるでしょう。
図から見るとnewPersonFunction.prototypeをスキップしたんじゃないんですか?

いい質問ですね。これが「継承関係」一番のかなめだと思います。
次の .__ proto __ & instanceof で説明したいと思います。

補足:↓はnewPerson.__proto__の検証です。

console.log(newPerson.__proto__ === Person.prototype) // true
console.log(Object.getPrototypeOf(newPerson) === Person.prototype) // true

.__ proto __ & instanceof

relationship4.png
instance:インスタンス、複製先。
instanceof:インスタンスの複製元の検定演算子。

実はJavaScriptでは、(undefinedとnull除いて)すべてのプリミティブとオブジェクトがFunctionのインスタンスです。
以下はその検証コードです。

console.log(String instanceof Function) // true
console.log(Number instanceof Function) // true
console.log(BigInt instanceof Function) // true
console.log(Boolean instanceof Function) // true
console.log(Symbol instanceof Function) // true

console.log(undefined instanceof Function) // false
console.log(null instanceof Function) // false

console.log(Array instanceof Function) // true
console.log(Object instanceof Function) // true
console.log(Function instanceof Function) // true

console.log(Person instanceof Function) // true
console.log(newPerson instanceof Function) // false

この検証で分かったのは、プリミティブもオブジェクトもインスタンス経由でメソッドの継承関係が成り立つことです。
Person内部のメソッドが全てnewPersonが継承していくように、これらもFunctionから組み込みメソッドを継承している。

console.log(newPerson instanceof Function) // falseだとしても、そもそもnewPersonPersonFunctionから継承したメソッドを複製したので、Function.prototypeを経由しなくてもFunctionのメソッドが利用できます。

ここで分かったのは、prototypeによる継承と、instanceによる継承が違うやり方だけど同じ目的を達成したということです。

なぜそこまではっきり言えるのかな。
うーん、実はここまではただ自分が検証結果の整理です。
実際ほかのプリミティブで検証してみたら、

function Person() { }
const newPerson = new Person()
console.log(newPerson.prototype) // undefined
console.log(newPerson.__proto__) // {}
console.log(newPerson.__proto__ === Person.prototype) // true
console.log(newPerson instanceof Person) // true

console.log(newPerson.constructor) // [Function: Person]


const str = '123'
console.log(str.prototype) // undefined
console.log(str.__proto__) // {}
console.log(str.__proto__ === String.prototype) // true
console.log(str instanceof String) // false

console.log(str.constructor) // [Function: String]

newPersonstrもインスタンス同士だからほぼ同じ反応が出てきて
⇒ インスタンスにはprototypeundefinedとのこと、
⇒ .__proto__が存在し、それが複製元のprototypeとのこと
確かにstrStringのインスタンスではないけれど、strnewのように完全複製をしたわけでもなかったため、この結果もおかしくないと思います。

ただ一つお互いの継承関係が分かる決め手がありますが、それがconstructorです。
constructor:コンストラクタ、構築子、複製元を記録するオブジェクト。
↑自分よがりの解釈ですが、実際MDNの説明では、

constructor プロパティは、インスタンスオブジェクトを生成した Object のコンストラクター関数への参照を返します。なお、このプロパティの値は関数そのものへの参照であり、関数名を含んだ文字列ではありません。
Object.prototype.constructor

「インスタンスオブジェクトを生成した Object のコンストラクター関数への参照を返します。」
確かにconstructorは可変で自由に書き替えできますが、単にinitial valueから見るとおおむね両者の関係がわかるようになると思います。

↓はほかの検証です。

const arr = []
console.log(arr.prototype) // undefined
console.log(arr.__proto__) // Object(0) []
console.log(arr.__proto__ === Array.prototype) // true
console.log(arr instanceof String) // false
console.log(arr.constructor) // [Function: Array]

.__ proto __ & instanceof & prototype

relationship5.png
ここはプリミティブとオブジェクトのprototypeがどんな感じしているのと、さきほどinstanceofの参照と合わせた図です。

この図を理解すれば、もう.__proto__prototypeに振り回されることありません!(そう願います)

そして、もしFunctionに何か書き込んだらどうなるかな?詳しくはおまけの検証コードへ。
実際、Function.__proto__(参照先)、つまりFunction.prototypeを書き替えたらさらに大変なことになります。(低速の操作且つプロトタイプチェーンの破壊)
なので.__proto__の使用はお勧めしません。[[prototype]]を暴くにはObject.getPrototypeOf()(read-only)などより安全なメソッドがあります。

↓は上の相関図の検証コードです。

console.log(String.prototype) // {}
console.log(Number.prototype) // {}
console.log(BigInt.prototype) // Object [BigInt] {}
console.log(Boolean.prototype) // {}
// console.log(undefined.prototype)
// TypeError: Cannot read property 'prototype' of undefined
console.log(Symbol.prototype) // Object [Symbol] {}
// console.log(null.prototype)
// TypeError: Cannot read property 'prototype' of null
console.log(Array.prototype) // Object(0) []
console.log(Object.prototype) // [Object: null prototype] {}
console.log(Function.prototype) // {}

// is {} equal?
console.log(String.prototype === Number.prototype) // false
console.log(String.prototype === Boolean.prototype) // false
console.log(Number.prototype === Boolean.prototype) // false

console.log(String.prototype === Function.prototype) // false
console.log(Boolean.prototype === Function.prototype) // false
console.log(Number.prototype === Function.prototype) // false

// is anyone equal?
console.log(String.prototype === Function.prototype) // false
console.log(Number.prototype === Function.prototype) // false
console.log(BigInt.prototype === Function.prototype) // false
console.log(Boolean.prototype === Function.prototype) // false
console.log(Symbol.prototype === Function.prototype) // false

console.log(Array.prototype === Function.prototype) // false
console.log(Object.prototype === Function.prototype) // false

7/15補足:
Chrome DevToolの結果からみると、データ型それぞれのメソッドは各自のprototypeに保存していると考えられます。
つまりプリミティブもオブジェクトもFunctionのインスタンスとして自分のprototypeを形成し、自らのデータ型に使用できるメソッドをそこに保存するかもしれません。
しかしこれはプリミティブ、オブジェクトというデータ型に成り立つ話です。newで創られたインスタンスはこの機能がありません。.prototypeundefinedです。
dataprototype.png

function & its prototype & new instance

relationship6.png
ここに二つ疑問があります。
プリミティブなら元々存在して、複製元とのインスタンスの継承関係も分かったけど、
Personは私が勝手に創り出した関数ですよね?strと同じようにinstanceoffalseを返すはずだが、なぜかtrueでした。

console.log(Person instanceof Function) // true

また、Person.prototypeが存在する時点が明らかにPersonより前なんて不可能だと思います。ほかのプリミティブやオブジェクトはこの問題がないだが、Functionと関わるとなんだか複雑になります。

自分の考察では、
一つ目は普通の関数はFunctionのインスタンスだから、インスタンスが複製元のprototype、つまりFunction.prototypeのメソッドを継承する、ここまではstrと同じように見えるが全然違うと思います。instanceoftrueになったのは、関数PersonFunctionを完全複製したわけです。そのおかげで、関数には何でも入れられる、何でも利用できるオブジェクトになるのは、ほかのプリミティブやオブジェクト(objectを除いて)ができないことです。

なぜまたそんなこと言いきれる?
だってObjectも同じことやってるじゃん...。

console.log(Object instanceof Function) // true
console.log(Function instanceof Object) // true

あくまでも考察からの自己解釈。

二つ目は述べたようにPerson.prototypePerson関数が創り出されると同時に生まれるが、Function.prototypeと、その上のObject.prototypenullはその前の時点ですでに存在していると思います。(そうでないと継承関係ごじゃごじゃになるから)

おまけの検証コード

// constructor
console.log(String.constructor) // [Function: Function]
console.log(Number.constructor) // [Function: Function]
console.log(BigInt.constructor) // [Function: Function]
console.log(Boolean.constructor) // [Function: Function]
// console.log(undefined.constructor)
// TypeError: Cannot read property 'constructor' of undefined
console.log(Symbol.constructor) // [Function: Function]
// console.log(null.constructor)
//TypeError: Cannot read property 'constructor' of null

console.log(Array.constructor) // [Function: Function]
console.log(Object.constructor) // [Function: Function]
console.log(Function.constructor) // [Function: Function]

console.log(Person.constructor) // [Function: Function]
console.log(Person.prototype.constructor) // [Function: Person]
console.log(newPerson.constructor) // [Function: Person]

Function.constructor = Person

console.log(String.constructor)
// [Function: Function] { constructor: [Function: Person] }
console.log(Number.constructor)
// [Function: Function] { constructor: [Function: Person] }
console.log(BigInt.constructor)
// [Function: Function] { constructor: [Function: Person] }
console.log(Boolean.constructor)
// [Function: Function] { constructor: [Function: Person] }
console.log(Symbol.constructor)
// [Function: Function] { constructor: [Function: Person] }

console.log(Array.constructor)
// [Function: Function] { constructor: [Function: Person] }
console.log(Object.constructor)
// [Function: Function] { constructor: [Function: Person] }
console.log(Function.constructor)
// [Function: Person]

console.log(Person.constructor)
// [Function: Function] { constructor: [Function: Person] }
console.log(Person.prototype.constructor) // [Function: Person]
console.log(newPerson.constructor)
// [Function: Person]
5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?