初めに
JavaScriptを勉強し始めてすぐ遭遇する概念ですが、なかなか全般的に理解できないのでそのまま放置してました。(読めるけどうまく使えなかった)最近もう一度JavaScriptを勉強することを機に、自分の言葉でわかりやすくまとめていきたいと思います。
まずはJavaScriptのオブジェクトは何?から始めたいと思います。
前に書いたものを振り返るとやっぱりあの頃の自分はまだ全般を理解していなかったと思った。例えばtypeof
運算子でArray
がobject
だと疑問があったけど、自分の中は実践が大事だから使いこなせるまでそのまま疑問を放置してました。
でもこうして振り返りの大切さ、そしてやりがいも感じました。わからないことをわかるようになる楽しみを味わいながら頑張っていきたいと思います!
継承方法についてほかの文章:
JavaScriptのinheritanceについて part1
JavaScriptのinheritanceについて part2
もう一度プロトタイプチェーンを考察した(相関図入り):
JavaScriptの.__proto__とprototypeとinstance
Object in JavaScript
JavaScriptのデータ型は大きく分けると、プリミティブとオブジェクトがあります。
Primitives
String
Number
BigInt
Boolean
undefined
Symbol
(ES6)
null
七つのプリミティブはimmutable(変わらない)、つまりstate(状態)が変更できないということです。変数に新しい値を代入することができるが、値自体を変更したわけではありません。(この部分は、自分は「利用してない値がメモリーリサイクルされる」と理解している)
Object
Object
Array
Function
null
(the end of prototype chain)
そしてオブジェクトはJavaScriptの核心概念として、すべてのデータ型を格納するデータ型で、さらに細かく分けるとArray
、Function
、null
これらがオブジェクトのサブタイピングです。
そのなかnull
はプリミティブでありながら、すべてのObject.prototype
の原点(何も継承してないオブジェクト)でもあります。
const x = {
a: 1
}
console.log(x.__proto__ === Object.prototype) // true
console.log(x.__proto__.__proto__ === null) //true
console.log(Object.prototype.__proto__ === null) //true
console.log(Object.getPrototypeOf(Object.prototype)) // null
Function
関数はオブジェクト?
少し変な言い方かもしれませんが関数はオブジェクトです。確かにtypeof
を使うとfunction
という結果が出てきたんですが、JavaScriptの関数のなかには[[Call]]
という隠しプロパティを持って、callable object
(呼び出せるオブジェクト)として使えます。
function callable() {
const a = 1
}
callable.b = 2
console.log(callable.a) // undefined
console.log(callable.b) // 2
console.log(callable) // [Function: callable] { b: 2 }
console.log(callable.__proto__) // {}
console.log(callable.__proto__.__proto__ === Object.prototype) // true
console.log(callable.__proto__.__proto__.__proto__ === null) // true
Function
は外部からアクセス不能という特性を持っていますが、オブジェクトのようにb
とその値を格納することができる。
実際ログしてみるとFunction
の後ろにはオブジェクトがついていてcallable.b
によって値にアクセスできる。
また、Object.prototype
のアクセサプロパティ__proto__
を使って、関数のプロトタイプ.__proto__
(参照先)は{}
、順次に上にさかのぼるとObject.prototype
、null
になります。
Prototype in JavaScript
ではPrototype
、Prototype Chain
は何でしょう?
prototype
:原型。(複製元) 継承とプロトタイプチェーン
まずはprototype
は原型という意味を持つ単語として、JavaScriptではプロパティやメソッドなどの元となるオブジェクトをprototype
と言い、すべてのオブジェクトには必ずprototype
が存在する。(null
にはありません。)
Prototypal Inheritance & Prototype Chain
Inheritanceというのは継承です。C++
、C#
、Java
ではClassical inheritance、JavaScriptではPrototypal inheritanceを使用します。
classical inheritance vs prototypal inheritance in javascript - stackoverflow
Master the JavaScript Interview: What’s the Difference Between Class & Prototypal Inheritance? - Eric Elliott
Prototypal inheritanceを簡単に言うと、prototype
はオブジェクトがほかのオブジェクトからプロパティやメソッドを直接継承する機能として、もともと持つものの上に、ほかのprototype
と連携し拡張することができます。
自分のオブジェクトからプロパティを読み込んでも見つからなかった場合、自分のプロトタイプ.__proto__
に読み込んでみる。それもなかった場合はさらに上、最後は必ずnull
にたどり着くので、すべてのプロトタイプ.__proto__
にはなかった場合はundefined
を返します。これがプロトタイプチェーンです。
let animal = {
eats: true
}
let rabbit = {
jumps: true
}
rabbit.__proto__ = animal
console.log(rabbit.jumps) // true
console.log(rabbit.eats) // true // get property from 'animal'
console.log(rabbit.__proto__) // { eats: true } // inherit from 'animal'
console.log(rabbit.__proto__.__proto__) // [Object: null prototype] {}
console.log(rabbit.__proto__.__proto__.__proto__) // null
console.log(rabbit.a) // undefined
(.__proto__
の変更や使用は混乱を生じやすいので未推奨です。ここはただ説明の例として。)
Prototypal inheritance - javascript.info
JavaScriptのメソッドもこの形式で利用されています。
MDNでメソッドを調べるときよく見てるけど気づかなかったところがあります。
メソッドを探すとき文字列ならString.prototype.
、配列ならArray.prototype.
から始まり、以前はただの分類だと思っていたがprototype
を理解したら全部プロトタイプチェーンからメソッドを探し使っていることが分かりました。
ChromeのDevToolsで[[Property]]
一覧表示できます。これで隠しプロパティ[[Property]]
も見やすくなりますね。
↓は別の例です。
// Base object
const point2D = {
x: 10,
y: 20,
}
// Inherit from 'point3D' object
const point3D = {
z: 30,
__proto__: point2D,
}
console.log(
point3D.x, // 10, inherited from point2D
point3D.y, // 20, inherited from point2D
point3D.z // 30, from its own property
)
Prototype - JavaScript. The Core: 2nd Edition
.__ proto __
Object.prototype.__ proto __ - MDN
.__proto__
はオブジェクト内部の[[Property]]
プロパティとは同じではなく、Object.prototype
のアクセサープロパティです。
今は非推奨になっているので、Object.getPrototypeOf
と Object.setPrototypeOf
関数の利用がおすすめです。
( [[Property]] の直接変更はパフォーマンスに影響を及ぼすので避けた方がいいです。)
Constructor Function & new
vs. Prototype
constructor
:コンストラクタ、構築子。 Object.prototype.constructor - MDN
Constructor function(コンストラクタ関数) というのは、普通の関数の内部にthis
でExecution Context(実行コンテキスト)を変えさせて、そしてnew
で新しいインスタンスを創り出していく関数です。
7/13補足:もう一度調べたらすべての関数にはconstructor
プロパティを持ち、new
でインスタンスを創るときにconstructor
プロパティで継承対象(親)を参照し、その親がコンストラクタ関数と呼びます。
この文章のコンストラクタ関数は特にthis
オブジェクトを所有するコンストラクタ関数を指しています。
new
で創られたインスタンスのコンテキストは、自分のスコープにバインドするようになります。
function Cat(name, color) {
this.name = name
this.color = color
this.species = 'cat'
this.favorite = () => {
console.log('eat and sleep')
}
}
const cat1 = new Cat('Mio', 'white')
const cat2 = new Cat('Tora', 'orange')
console.log(cat1.name, cat1.color) // Mio white
console.log(cat2.name, cat2.color) // Tora orange
console.log(cat1.species) // cat
cat1.favorite() // eat and sleep
console.log(cat2.species) // cat
cat2.favorite() // eat and sleep
(コンストラクタ関数は頭文字が大文字という暗黙のルールがあります。)
初めてconsole.log(cat1.name, cat1.color)
を読んだとき、多分私と同じ、なぜこれだけで値をアクセスできるの?って思っちゃいますね。
まずfunction
はcallable objectとして、別のところからの値を保存しアクセルすることもできます。this
はここでの作用は指定された実行コンテキストで新しいオブジェクトを生成し値を保存する、ということで、オブジェクトのように値のアクセスができるわけです。なのでコンストラクタ関数にはthis
で共有のオブジェクトでプロパティ(property name: property value)とメソッドを保存し、new
ですべてのイスタンスが同じプロパティやメソッドが利用できるようになります。
しかしここには一つの問題があります。new
で毎回新しいインスタンスを生成するたびに、中身が同じプロパティとメソッドを何度も複製していくということで、メモリーにとってデメリットになります。例えばthis.species
とthis.favorite()
には固定された内容には、何かメモリーにやさしい方法がないでしょうか。
実はJavaScriptではすべてのコンストラクタ関数にprototype
プロパテ(オブジェクト)を持たされて、コンストラクタ関数で定義したプロパティやメソッドが、自分のprototype
プロパティに所有し、そしてnew
で創られたインスタンスたちは親であるコンストラクタ関数からprototype
プロパティを継承する。
なので変わらない、あるいは変わる必要のないプロパティやメソッドをコンストラクタ関数のprototype
オブジェクトに直接定義していいです。
function Cat(name, color) {
this.name = name
this.color = color
}
Cat.prototype.species = 'cat'
Cat.prototype.favorite = () => {
console.log('eat and sleep')
}
// console.log(Cat.prototype)
// { species: 'cat', favorite: [Function (anonymous)] }
ここから
コンストラクタ関数とインスタンスの関連、
コンストラクタ関数がインスタンスのprototype
であるかどうかの確認、
インスタンスのプロパティはコンストラクタ関数からどう継承してきたかの確認、
...いくつのメソッド紹介したいと思います。
instanceof
instanceOf
を用いてコンストラクタ関数とインスタンスの関連を確認する。
console.log(cat1 instanceof Cat) // true
console.log(cat2 instanceof Cat) // true
isPrototypeOf()
isPrototypeOf()
メソッドを用いて、親コンストラクタ関数のprototype
オブジェクトとイスタンスの関連を確認できます。
console.log(Cat.prototype.isPrototypeOf(cat1)) // true
console.log(Cat.prototype.isPrototypeOf(cat2)) // true
hasOwnProperty()
すべてのイスタンスにはhasOwnProperty()
メソッドがあります。hasOwnProperty()
メソッドはプロトタイプチェーンではなく自分のプロパティだけ検定するので、これであるプロパティが親コンストラクタ関数(ローカル)から継承したか、それとも親コンストラクタ関数のprototype
オブジェクトから継承したかを確認できます。
console.log(cat1.hasOwnProperty('name')) // true
console.log(cat1.hasOwnProperty('species')) // false
console.log(cat2.hasOwnProperty('favorite')) // false
('species'
、'favorite'
いずれもコンストラクタ関数からではなく、prototype
を経由して継承しているのでfalse
。)
in
hasOwnProperty()
とは逆にin
は自身のプロパティだけでなくプロトタイプチェーンを全部チェックする。
console.log('name' in cat1) // true
console.log('species' in cat1) // true
constructor in instance?
よく間違われるところだと思いますが、実はインスタンスにはconstructor
プロパティを所有しません。
console.log(cat1.hasOwnProperty('constructor')) // false
console.log(cat1.__proto__.hasOwnProperty('constructor')) // true
console.log(cat1.constructor === Cat) // true
console.log(cat2.constructor === Cat) // true
console.log(cat1.constructor === Cat)
がtrue
なのは、インスタンスのconstructor
でなく、上のプロトタイプチェーンにコンストラクタ関数Cat
がいるから、Cat
のconstructor
と照らし合わせて自然とtrue
が返されました。
インスタンスのconstructor
プロパティは可変です。コンストラクタ関数のprototype
が変えてしまえば、新しいイスタンスのconstructor
も変えてしまいます。(古いのはpass by sharingの影響でそのままです。)
Cat.prototype = {}
const cat3 = new Cat()
console.log(cat3.constructor === Cat) // false
console.log(cat3.constructor === Object) // true
console.log(cat1.constructor === Cat) // true
console.log(cat1.constructor === Object) // false
console.log(cat2.constructor === Cat) // true
console.log(cat2.constructor === Object) // false
// note: pass by sharing
参考資料まとめ
Primitive (プリミティブ) - MDN
継承とプロトタイプチェーン - MDN
Object.prototype.__ proto __ - MDN
Object.prototype.constructor - MDN
Object.getPrototypeOf() - MDN
Object.prototype.hasOwnProperty() - MDN
Prototype - JavaScript. The Core: 2nd Edition
classical inheritance vs prototypal inheritance in javascript - stackoverflow
Master the JavaScript Interview: What’s the Difference Between Class & Prototypal Inheritance? - Eric Elliott
Prototypal inheritance - javascript.info
感想
勉強メモとはいえ、公の場でいろんな方に見せるものでもありますので、正直外国人である自分の考えがうまく伝わったかどうかとても不安です。まだまだ初心者(学習歴8ヶ月)なのに、こういう角度でこうやって解釈していいのかなって文章を書くたびに思ってしまいます。でもやはりこれからもう一度振り返るときのために一度文章にする必要があると思います。
長文ですが、最後まで読んでいただきありがとうございます。お気づきの点がございましたら、ご指摘いただけると幸いです。