初めに
前回は JavaScriptのPrototypeについて でプロトタイプ、プロトタイプチェーンをまとめてみました。
今回はConstructor FunctionのInheritance(継承) 、四つの方法をまとめていきたいと思います。
Inheritance
ここから継承の仕方を紹介したいと思います。
デモのためにまずは2つのコンストラクタ関数から行きます。
function Animal() {
this.species = 'animal'
}
function Cat(name, color) {
this.name = name
this.color = color
}
コンストラクタ関数Animal
を、コンストラクタ関数Cat
に継承させるのがデモの目的です。
by binding methods (call, apply...)
function Animal() {
this.species = 'animal'
}
function Cat(name, color) {
Animal.apply(this)
this.name = name
this.color = color
}
const cat1 = new Cat('Mio', 'white')
console.log(cat1.name, cat1.color) // Mio white
console.log(cat1.species) // animal
console.log(cat1) // Cat { species: 'animal', name: 'Mio', color: 'white' }
一番目はバインドメソッドで強制的に親のthis
コンテキストを子へ変更する、つまりAnimal
のthis
オブジェクトのプロパティを、Cat
のthis
オブジェクトに入れるという継承方法です。
ここでapply()
でバインドする前の状態を見てみたいと思います。
まずはCat.prototype
オブジェクトにspecies
プロパティとfavorite
メソッドを書き込んでみて、
function Cat(name, color) {
// Animal.apply(this)
this.name = name
this.color = color
}
Cat.prototype.species = 'cat'
Cat.prototype.favorite = () => {
console.log('eat and sleep')
}
const cat1 = new Cat('Mio', 'white')
console.log(cat1.name, cat1.color) // Mio white
console.log(cat1.species) // cat
cat1.favorite() // eat and sleep
console.log(cat1) // Cat { name: 'Mio', color: 'white' }
console.log(Cat.prototype) // { species: 'cat', favorite: [Function (anonymous)] }
そしたらAnimal.apply(this)
でバインドすると何か変わるでしょうか。
console.log(cat1.name, cat1.color) // Mio white
console.log(cat1.species) // animal
cat1.favorite() // eat and sleep
console.log(cat1) // Cat { species: 'animal', name: 'Mio', color: 'white' }
console.log(Cat.prototype) // { species: 'cat', favorite: [Function (anonymous)] }
プロトタイプチェーンというのは自分のオブジェクトに持ってないものなら、自分のプロトタイプ、つまり上に探していきますね。
Cat
はAnimal
からのspecies
が強制的にバインドされCat
のthis
オブジェクトの一部として書き込まれたで、this
オブジェクトですでにspecies
が見つかった場合、上にはいきません。それで自分のプロトタイプCat.prototype.species = 'cat'
が使えなくなりました。
もちろんCat.prototype
のプロパティたちは書き換えられたわけではないので、プロパティが重複しない以上Cat.prototype
にたどり着いてプロパティを利用することができます。(例えばfavorite
メソッド)
(実際四つの方法を分かったうえ、バインドメソッドがプロトタイプチェーンへの変動しないところが、一番混乱生じにくい方法だと思います。)
↓は検定メソッドの結果です。
console.log(cat1.hasOwnProperty('species')) // true
console.log(Cat.prototype.isPrototypeOf(cat1)) // true
console.log(cat1.constructor === Cat) // true
console.log(cat1.constructor === Animal) // false
console.log(cat1.hasOwnProperty('species')) // true
から、species
がコンストラクタ関数(ローカル)から継承したことが分かりました。
by another new instance
二番目はprototype
プロパティの操作ですが、
function Animal() {
this.species = 'animal'
}
function Cat(name, color) {
this.name = name
this.color = color
}
const cat1 = new Cat('Mio', 'white')
console.log(Cat.prototype) // {}
console.log(cat1.constructor) // [Function: Cat]
Cat.prototype = new Animal()
console.log(Cat.prototype) // Animal { species: 'animal' }
console.log(cat1.constructor) // [Function: Cat]
一番目のようにCat.prototype
のプロパティ、つまりprototype
オブジェクトのプロパティへの書き込みではなく、継承したい親関数のインスタンスで上書きするんです。このやり方は、元に持っているprototype
オブジェクトを削除するように見えますが、実際はちょっと違います。
まずはCat
のプロトタイプがAnimal
のインスタンスで上書きされた後に、Cat
の新しいインスタンスcat2
を作りましょう。
const cat2 = new Cat('Tora', 'orange')
console.log(cat2.__proto__) // Animal { species: 'animal' }
console.log(cat2.__proto__ === Cat.prototype) // true
console.log(cat2.constructor) // [Function: Animal]
インスタンスにはconstructor
がないため、自分のコンストラクタ関数のプロトタイプへたどり着き、cat2.constructor
がAnimal
に指しています。
なのでこのCat.prototype = new Animal()
という、継承対象のインスタンスで自分のprototype
への上書きが、継承前から創り出した自分のインスタンスの継承関係に混乱が生じてしまいます。
cat1
もcat2
もCat
のインスタンスだけど、cat1
はpass by sharingの影響で前の歴史(値)に取り残されたままです。
console.log(cat1 instanceof Cat) // false
console.log(cat2 instanceof Cat) // true
console.log(Cat.prototype.isPrototypeOf(cat1)) // false
console.log(Cat.prototype.isPrototypeOf(cat2)) // true
ちょっと別の話になりますが、まずはpass by sharingってどういうことから説明したいと思います。
一般の認識ではJavaScriptはpass by valueとpass by reference二つがあります。実際JavaScriptのpass by referenceではなくpass by sharingっていうほうが適切です。(実際JavaScriptはpass by valueしかないんですが、区別のためにpass by sharingという技術上の言い方が出てきました。)
Is JavaScript a pass-by-reference or pass-by-value language? - Stackoverflow
pass by referenceは値が連動することに対して、pass by sharingは値の記憶方法として、一つの変数に一つの値のように、一つの参照をコピーして記憶することで連動はできません。
7/16補足:この文章の後、JavaScriptのObjectのDeep copyメソッドについてでpass by valueの説明図を作成しました。
cat1
が作り出された当初、親のCat
、Cat.prototype
は{}
でconstructor
も自分自身に指しているので、cat1
はそれを参照しています。
Cat.prototype = new Animal()
で新しい値(Animal
)が付与され、そこから生まれたcat2
が新しい値を参照するので、Cat
のインスタンスとしてinstanceof
もconstructor
も新しい値に従ってうまくいきましたが、cat1
古い値を参照したせいで、Cat
のインスタンスすら認めてもらえなくなりました。
きっと、どこかでこれ↓で解決できるんじゃない?って
Cat.prototype.constructor = Cat
console.log(cat1.constructor) // [Function: Cat]
console.log(cat2.constructor) // [Function: Cat]
console.log(cat1 instanceof Cat) // false
console.log(cat2 instanceof Cat) // true
console.log(Cat.prototype.isPrototypeOf(cat1)) // false
console.log(Cat.prototype.isPrototypeOf(cat2)) // true
console.log(cat1.species) // undefined
console.log(cat2.species) // animal
console.log(cat2) // Cat { name: 'Tora', color: 'orange' }
確かに、constructor
は可変だから新しい参照を記憶しているcat2
が自分にないconstructor
プロパティがCat.prototype.constructor
の変化によって変わったように見えましたが。(遠回りのような言い方してすみません、要するにcat2
のconstructor
がないから親のconstructor
にたどり着いてその新しい値をログしただけです。pass by referenceで連動したわけではありません。)
cat1
にはinstanceof
はだめでした。isPrototypeOf()
もだめでした。古い値を参照しているcat1
には救いはありません...。
この方法でprototype
オブジェクトを削除ではなく、新しい値を付与することで新しい参照が記憶されますが、古い値は消えたのではなくちゃんと生きていて、利用されている限りどこかで保存されています。
cat2
のように創られた時点ですでに新しい参照に従うので問題はありません。
これはけっして悪い方法ではありませんが、
ただ 古いインスタンスには、救いようのない継承方法 なので、プロトタイプチェーンが長ければ要注意です。
by prototype property
三番目は、二番目の改善版です。ここは継承対象のインスタンスではなく、prototype
を継承するのです。これで継承するたびにnew
でインスタンスを創ることなく、メモリーに優しいのです。
function Animal() { }
Animal.prototype.species = 'animal'
function Cat(name, color) {
this.name = name
this.color = color
}
const cat1 = new Cat('Mio', 'white')
console.log(Cat.prototype) // {}
console.log(cat1.constructor) // [Function: Cat]
console.log(cat1 instanceof Cat) // true
console.log(Cat.prototype.isPrototypeOf(cat1)) // true
Cat.prototype = Animal.prototype
console.log(cat1 instanceof Cat) // false
console.log(Cat.prototype.isPrototypeOf(cat1)) // false
Cat.prototype.constructor = Cat
console.log(cat1 instanceof Cat) // false
console.log(Cat.prototype.isPrototypeOf(cat1)) // false
でもやはり、古いインスタンスには救いがないです。
const cat2 = new Cat('Tora', 'orange')
console.log(cat2 instanceof Cat) // true
console.log(Cat.prototype.isPrototypeOf(cat2)) // true
console.log(cat2) // Cat { name: 'Tora', color: 'orange' }
console.log(cat2.species) // animal
console.log(Cat.prototype.constructor) // [Function: Cat]
console.log(Animal.prototype.constructor) // [Function: Cat]
console.log(Cat.prototype) // { species: 'animal' }
console.log(Animal.prototype) // { species: 'animal' }
でもこのやり方では、Cat.prototype
とAnimal.prototype
は同じオブジェクトをシェアしているので、片方を変更したらもう片方も変更されてしまいます。
Cat.prototype.favorite1 = () => {
console.log('fruit is yummy!')
}
cat2.favorite1() // fruit is yummy!
console.log(Cat.prototype)
// { species: 'animal', favorite1: [Function (anonymous)] }
console.log(Animal.prototype)
// { species: 'animal', favorite1: [Function (anonymous)] }
Animal.prototype.favorite2 = () => {
console.log('sleep and play')
}
cat2.favorite2() // sleep and play
console.log(Cat.prototype)
// {
// species: 'animal',
// favorite1: [Function (anonymous)],
// favorite2: [Function (anonymous)]
// }
console.log(Animal.prototype)
// {
// species: 'animal',
// favorite1: [Function (anonymous)],
// favorite2: [Function (anonymous)]
// }
by function() {}
そして四番目は三番目の改善版です。
片方のprototype
を変更しても、もう片方が影響されないように、両方の間に空の関数(オブジェクト)を利用して継承対象のprototype
プロパティを保存する。
function Animal() { }
Animal.prototype.species = 'animal'
function Cat(name, color) {
this.name = name
this.color = color
}
console.log(Cat) // [Function: Cat]
console.log(Cat.prototype) // {}
const F = function () { }
F.prototype = Animal.prototype
Cat.prototype = new F()
console.log(Cat) // [Function: Cat]
console.log(Cat.prototype) // Animal {}
Cat.prototype.constructor = Cat
console.log(Cat) // [Function: Cat]
console.log(Cat.prototype) // Animal { constructor: [Function: Cat] }
// console.log(Animal) // [Function: Animal]
// console.log(Animal.prototype) // { species: 'animal' }
ここまで来たらCat.prototype
の動きも分かりやすくなると思います。
Cat.prototype
:
{}
(関数のprototype
)
↓
Animal {}
(FのインスタンスによってAnimal.prototype
を継承)
↓
Animal { constructor: [Function: Cat] }
(Cat.prototype.constructorでconstructor
プロパティに新しい値を付与)
そして空関数F
のインスタンスのおかげで、
const cat2 = new Cat('Tora', 'orange')
Cat.prototype.favorite1 = () => {
console.log('fruit is yummy!')
}
cat2.favorite1() // fruit is yummy!
console.log(Cat.prototype)
// Animal {
// constructor: [Function: Cat],
// favorite1: [Function(anonymous)]
// }
console.log(Animal.prototype) // { species: 'animal' }
Animal.prototype.favorite2 = () => {
console.log('sleep and play')
}
cat2.favorite2() // sleep and play
console.log(Cat.prototype)
// Animal {
// constructor: [Function: Cat],
// favorite1: [Function (anonymous)]
// }
console.log(Animal.prototype)
// { species: 'animal', favorite2: [Function (anonymous)] }
お互い影響されずに無事に継承しました。
感想
今回は前回の続き、コンストラクタ関数の継承をまとめてみました。決してどれの方法がいいか悪いか、を強調するのではなく、なぜこうなる?次の方法でどう改善されていくか、そしてどのような課題が残されていたかを語りたかったです。自分にふさわしいプロトタイプチェーンの書き方についてまだまだ探索中です。引き続き頑張ります。次回はコンストラクタ関数を使わず継承する方法についてまとめていきたいと思います。