0
0

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のinheritanceについて part1

Last updated at Posted at 2022-07-12

初めに

前回は 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コンテキストを子へ変更する、つまりAnimalthisオブジェクトのプロパティを、Catthisオブジェクトに入れるという継承方法です。
ここで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)] }

プロトタイプチェーンというのは自分のオブジェクトに持ってないものなら、自分のプロトタイプ、つまり上に探していきますね。

CatAnimalからのspeciesが強制的にバインドされCatthisオブジェクトの一部として書き込まれたで、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.constructorAnimalに指しています。

なのでこのCat.prototype = new Animal()という、継承対象のインスタンスで自分のprototypeへの上書きが、継承前から創り出した自分のインスタンスの継承関係に混乱が生じてしまいます。

cat1cat2Catのインスタンスだけど、cat1pass 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 valuepass 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が作り出された当初、親のCatCat.prototype{}constructorも自分自身に指しているので、cat1はそれを参照しています。

Cat.prototype = new Animal()で新しい値(Animal)が付与され、そこから生まれたcat2が新しい値を参照するので、Catのインスタンスとしてinstanceofconstructorも新しい値に従ってうまくいきましたが、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の変化によって変わったように見えましたが。(遠回りのような言い方してすみません、要するにcat2constructorがないから親の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.prototypeAnimal.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)] }

お互い影響されずに無事に継承しました。

感想

今回は前回の続き、コンストラクタ関数の継承をまとめてみました。決してどれの方法がいいか悪いか、を強調するのではなく、なぜこうなる?次の方法でどう改善されていくか、そしてどのような課題が残されていたかを語りたかったです。自分にふさわしいプロトタイプチェーンの書き方についてまだまだ探索中です。引き続き頑張ります。次回はコンストラクタ関数を使わず継承する方法についてまとめていきたいと思います。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?