初めに
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.prototype
、Object.prototype
、null
を除いて)コンストラクタ関数の創り出されると同時にprototype
が形成され、インスタンスの.__proto__
になる。
今回デモのコードの相関図
初めからこの図読むとこれ何なのか分からないと思います。
一番最後にまた読みましょう。
ここから、なぜ先ほど名詞の解釈がそうなったのか、図と検証で説明したいと思います。
.__ 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
一体何でしょうか?
ここは検証は長い~長い~ことになるから後に付けるから図を見てみましょう。
Function.prototype
は、Function
のprototype
です。(説明になってないじゃんー!)
ちょっとお待ちを!ここで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__
(参照先)になりますが、言葉の意味としてイコールではないことが分かりやすくなると思います。
プリミティブもオブジェクトもすべての.__proto__
がFunction.prototype
なので組み込みメソッド(built-in function)を継承して使えるようになったのです。
ではPerson
のインスタンスnewPerson
はどうなるでしょう。
図から見るとnewPerson
はFunction.prototype
をスキップしたんじゃないんですか?
いい質問ですね。これが「継承関係」一番のかなめだと思います。
次の .__ proto __ & instanceof で説明したいと思います。
補足:↓はnewPerson
の.__proto__
の検証です。
console.log(newPerson.__proto__ === Person.prototype) // true
console.log(Object.getPrototypeOf(newPerson) === Person.prototype) // true
.__ proto __ & instanceof
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
だとしても、そもそもnewPerson
はPerson
がFunction
から継承したメソッドを複製したので、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]
newPerson
もstr
もインスタンス同士だからほぼ同じ反応が出てきて
⇒ インスタンスにはprototype
がundefined
とのこと、
⇒ .__proto__
が存在し、それが複製元のprototypeとのこと
確かにstr
はString
のインスタンスではないけれど、str
もnew
のように完全複製をしたわけでもなかったため、この結果もおかしくないと思います。
ただ一つお互いの継承関係が分かる決め手がありますが、それが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
ここはプリミティブとオブジェクトの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
で創られたインスタンスはこの機能がありません。.prototype
はundefined
です。
function & its prototype & new
instance
ここに二つ疑問があります。
プリミティブなら元々存在して、複製元とのインスタンスの継承関係も分かったけど、
Person
は私が勝手に創り出した関数ですよね?str
と同じようにinstanceof
がfalse
を返すはずだが、なぜかtrue
でした。
console.log(Person instanceof Function) // true
また、Person.prototype
が存在する時点が明らかにPerson
より前なんて不可能だと思います。ほかのプリミティブやオブジェクトはこの問題がないだが、Function
と関わるとなんだか複雑になります。
自分の考察では、
一つ目は普通の関数はFunction
のインスタンスだから、インスタンスが複製元のprototype、つまりFunction.prototype
のメソッドを継承する、ここまではstr
と同じように見えるが全然違うと思います。instanceof
がtrue
になったのは、関数Person
がFunction
を完全複製したわけです。そのおかげで、関数には何でも入れられる、何でも利用できるオブジェクトになるのは、ほかのプリミティブやオブジェクト(object
を除いて)ができないことです。
なぜまたそんなこと言いきれる?
だってObject
も同じことやってるじゃん...。
console.log(Object instanceof Function) // true
console.log(Function instanceof Object) // true
(あくまでも考察からの自己解釈。)
二つ目は述べたようにPerson.prototype
がPerson
関数が創り出されると同時に生まれるが、Function.prototype
と、その上のObject.prototype
、null
はその前の時点ですでに存在していると思います。(そうでないと継承関係ごじゃごじゃになるから)
おまけの検証コード
// 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]