初めに
今回はクラスの継承や拡張についてまとめていきたいと思います。
basic syntax
part1後の書き方の練習です。
class Person {
constructor(name, birthYear) {
this.name = name
this.birthYear = Number(birthYear)
}
calculateAge() {
const today = new Date()
return today.getFullYear() - this.birthYear
}
}
let Taro = new Person('Taro', '1980')
console.log(Taro) // Person { name: 'Taro', birthYear: 1980 }
console.log(Taro.calculateAge())
インスタンスを創るのに、constructor()
への引数に任してthis
オブジェクトを創り内部メソッドがthis
オブジェクトをアクセスして値を利用する書き方です。
class Person {
constructor(name) {
this.name = name
}
calculateAge(birthYear) {
this.birthYear = Number(birthYear)
const today = new Date()
return today.getFullYear() - this.birthYear
}
}
let Taro = new Person('Taro')
console.log(Taro) // Person { name: 'Taro' }
console.log(Taro.calculateAge('1980')) // 42
console.log(Taro) // Person { name: 'Taro', birthYear: 1980 }
メソッド内部にthis
オブジェクトを作り、呼び出されるときに付与する引数が指定の値となり、それを利用する書き方です。
calculateAge()
の中にthis.
でオブジェクトプロパティを生成させるから別のthis
オブジェクトが生成されると思ったけれど、実際はどこでthis
を使ってもclass Person
内部フィールドで一つのコンテキストにまとめられているような感じです。
(まとめてconstructor()
に書く、必要に応じて各関数に書く、どちらがいいかわからないんですが、場合によって使い分けが必要だと思います。)
以下はclass
のthis
についての検証コード。
class Test {
constructor(a) {
// console.log(this) // Test {}
this.a = a
}
test1(a) {
// console.log(this) // Test {}
this.a = a
console.log(this.a)
}
}
let test = new Test(5)
console.log(test) // Test { a: 5 }
test.test1(10) // 10
console.log(test) // Test { a: 10 }
class
フィールドにあるthis
は全部一つのオブジェクトにまとめられ、プロパティ名が同じであれば上書きもできます。
extends
extends
はclass
を拡張させる機能を持ち、今のクラス宣言にほかのクラスの子クラス(派生クラス)を生成させるキーワードです。
class Person {
constructor(name) {
this.name = name
}
calculateAge(birthYear) {
this.birthYear = Number(birthYear)
const today = new Date()
return today.getFullYear() - this.birthYear
}
}
//
class Taro extends Person {
place(birthPlace) {
this.birthPlace = birthPlace
return `I come from ${this.birthPlace}`
}
}
let taro = new Taro('Taro Yamada')
console.log(taro.calculateAge('1995')) // 27
console.log(taro.place('Japan')) // I come from Japan
console.log(taro)
// Taro { name: 'Taro Yamada', birthYear: 1995, birthPlace: 'Japan' }
part1の検証で、constructor()
内部で定義されてない限り、メソッドたちはprototype
へ移行されるという不思議な動きがあると分かりました。
自分の持っていないメソッドはプロトタイプチェーンで親class
のprototype
へたどり着き、メソッドをアクセスする。
例えば、place()
はTaro.prototype
、calculateAge()
はPerson.prototype
を。
もう一度Object.getPrototypeOf()
で検証してみると、
console.log(Person instanceof Function) // true
console.log(Object.getPrototypeOf(taro) === Taro.prototype) // true
console.log(Object.getPrototypeOf(Taro.prototype) === Person.prototype) // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype) // true
function personFn() { }
let newPersonFn = new personFn()
console.log(Object.getPrototypeOf(newPersonFn) === personFn.prototype) // true
console.log(Object.getPrototypeOf(personFn.prototype) === Object.prototype) // true
class
のprototype
の継承方法はコンストラクタ関数とは変わらないのですが、
console.log(Taro instanceof Function) // true
console.log(Object.getPrototypeOf(taro) === Taro.prototype) //true
console.log(Object.getPrototypeOf(Taro) === Person) // true
console.log(Object.getPrototypeOf(newPersonFn) === personFn.prototype) // true
console.log(Object.getPrototypeOf(personFn) === Function.prototype) // true
インスタンスは自分のprototype
持ってないため本来なら親のprototype
からプロパティやメソッドをアクセスするのです。
class Taro
もFunction
インスタンスだから、プロトタイプチェーンのルールからすればclass Taro
の.__proto__
、つまり一個上のプロトタイプチェーンは親のPerson.prototype
のはずだったけれど。
しかしconsole.log(Object.getPrototypeOf(Taro) === Person) // true
っていうのは、やっぱりコンストラクタ関数と違います。これでclass
のextends
継承は親のprototype
ではなく、親のフィールドへの延長ってことが分かりました。
class Taro extends Person
というのは、class Taro
がclass Person
へのアクセスを許可する。
ではconsole.log(Object.getPrototypeOf(taro) === Taro.prototype) // true
はどういうことでしょうか。part1で検証した結果、constructor()
内で定義されてないメソッドはprototype
に移行される。
console.log(Object.getOwnPropertyNames(Taro.prototype))
// [ 'constructor', 'place' ]
これがインスタンスtaro
がTaro.prototype
へアクセスして、place()
を利用できる理由だと思います。そしてclass Person
がclass Taro
の延長フィールドとして、自分のインスタンスtaro
もclass Person
フィールドをアクセスできます。
overriding class field
class Taro extends Person {
calculateAge(birthYear) {
this.birthYear = Number(birthYear)
return this.birthYear
}
}
let taro = new Taro('Taro Yamada')
console.log(taro.calculateAge('1995')) // 1995
(ここからの部分は自分の解釈が多いです。)
子クラスclass Taro
内部のメソッドが、親クラスのclass Person
と同じ名前のメソッドが存在すれば上書きします(自フィールド)。という言い方は適切かどうかは分かりませんが。
もともと自分のフィールドにないものだから延長線上の親フィールドへたどり着きメソッドを利用するようになる。しかし今は自分のフィールドで見つけるようになったら、親フィールドへアクセスせず自フィールドのメソッドを利用するのではないかと。
実際、子クラスフィールド内で定義されたメソッドは親フィールドのメソッドを上書きするのではありません。
class Taro extends Person {
calculateAge(birthYear) {
this.birthYear = Number(birthYear)
return this.birthYear
}
}
let taro = new Taro('Taro Yamada')
console.log(taro.calculateAge('1995')) // 1995
class Jiro extends Person { }
let jiro = new Jiro('Jiro Yamada')
console.log(jiro.calculateAge('1999')) // 23
別の子クラスフィールド内部は同じ名前のメソッドを定義しなければまた親フィールドにたどり着きます。
this
の作用が大事だと感じて、class
フィールドとconstructor()
での違いがとても気になるので、まずは
// class field
class Person {
name = 'Taro'
}
class Taro extends Person { }
let taro = new Taro()
console.log(taro.name)
console.log(Object.getOwnPropertyNames(taro)) // [ 'name' ]
console.log(taro.hasOwnProperty('name')) // true
// constructor
class Person {
constructor(name) {
this.name = name
}
}
class Taro extends Person { }
let taro = new Taro('Taro')
console.log(taro.name)
console.log(Object.getOwnPropertyNames(taro)) // [ 'name' ]
console.log(taro.hasOwnProperty('name')) // true
どちらでもインスタンスtaro
が所有していて、しかし
class Person {
name = 'Taro'
constructor() {
this.name = 'Jiro'
}
}
class Jiro extends Person { }
let jiro = new Jiro()
console.log(jiro) // Jiro { name: 'Jiro' }
constructor()
にthis
で同じプロパティを再定義したら、class
フィールドのプロパティをオーバーライドします。
ほかの例も一緒に見ていきたいと思います。
class Person {
test = function fnInClassField() {
console.log('class field test')
}
constructor() {
console.log(this)
// Jiro { test: [Function: fnInClassField] }
this.test = function fnInConstructor() {
console.log('function in constructor')
}
}
test() {
console.log('function in parent class')
}
}
class Jiro extends Person { }
let jiro = new Jiro()
console.log(jiro)
// Jiro { test: [Function: fnInConstructor] }
同じ名前test
プロパティを三つのところに用意して、constructor()
内部でthis
オブジェクトをログしてみたら、最初の状態ではJiro { test: [Function: fnInClassField] }
だったが、下にあるthis.test
に上書きされ、最終的にJiro { test: [Function: fnInConstructor] }
になりました。
ではtest()
はどこにいってました?
class Person {
test() {
console.log('function in parent class')
}
}
class Jiro extends Person { }
let jiro = new Jiro()
console.log(jiro)
// Jiro {}
console.log(jiro.hasOwnProperty('test'))
// false
console.log(Object.getOwnPropertyNames(Person.prototype))
// [ 'constructor', 'test' ]
Person.prototype
ですね。
そして子クラスでは同じ名前のメソッドが書かれても、
class Jiro extends Person {
test() {
console.log('function in child class')
}
}
let jiro = new Jiro()
jiro.test()
// function in constructor
console.log(jiro.test)
// [Function: fnInConstructor]
console.log(Object.getOwnPropertyNames(Jiro.prototype))
// [ 'constructor', 'test' ]
class Person
のconstructor()
を上書きしません。
class Jiro
内部のtest()
もさきほどのルールのように自分のprototype
へ移行しました。
console.log(jiro.test)
呼び出しは、class Person
のconstructor()
に一番さきにたどり着きました。
さらに、もしconstructor()
のプロパティtest
を消して、どちらがさきにたどり着くのかな?
jiro.test() // class field test
console.log(jiro.test) // [Function: fnInClassField]
class Person
のプロパティtest
もなかったら、
jiro.test() // function in child class
console.log(jiro.test) // [Function: test]
Jiro.prototype
ですね。
となると、アクセスの優先順位は、
親constructor()
→ 親フィールド → 自分のprototype
→ 親のprototype
ここまで少しまとめてみたいと思います。なぜかclass
はtypeof
ではfunction
であったが、内部の構造としてはオブジェクトに近いと感じています。
-
class
フィールドで書いたプロパティがconstructor()
によって最終的に上書きされたり、上書きされずにthis
オブジェクトのプロパティに残ります。 - メソッドは変数で保存されてなかったら、
this
オブジェクトではなく.prototype
へ。 - 上の二点によってアクセス順位が付けられています。
-
constructor()
のプロパティにはthis
が必須で、インスタンスのthis
オブジェクトを生成するためにも、外部から引数の導入先を指定するためにもthis
に頼っています。
super
super
キーワードの使い方としてですが、
- 子クラスの
contructor()
でthis
を使う前にsuper()
呼び出さねばなりません。 - 子クラスのないメソッドなら、
super.
を通して親クラスのメソッドを使えるようになります。
下は子クラスにsuper()
を経由して親フィールドへ
class Person {
constructor(name) {
this.name = name
console.log(this)
}
}
class Taro extends Person {
constructor(birthYear, birthPlace) {
super()
this.birthYear = birthYear
this.birthPlace = birthPlace
console.log(this)
}
}
let taro = new Taro('1995', 'Tokyo')
console.log(taro.name) // undefined
console.log(Object.getOwnPropertyNames(taro)) // [ 'name', 'birthYear', 'birthPlace' ]
console.log(taro.hasOwnProperty('birthYear')) // true
console.log(taro.hasOwnProperty('birthPlace')) // true
class Person {
constructor(name) {
this.name = name
}
calculateAge(birthYear) {
this.birthYear = birthYear
const today = new Date()
return today.getFullYear() - this.birthYear
}
place(birthPlace) {
this.birthPlace = birthPlace
return `${this.name} comes from ${this.birthPlace}`
}
}
// super
class Taro extends Person {
personalInfo(birthYear, birthPlace) {
setTimeout(() => console.log(`${this.name} is ${super.calculateAge(birthYear)} years old, and ${super.place(birthPlace)}.`), 1000)
}
}
let taro = new Taro('Taro Yamada')
console.log(taro.name) // Taro Yamada
console.log(taro.calculateAge('1995')) // 27
console.log(taro.place('Japan')) // Taro Yamada comes from Japan
taro.personalInfo('1995', 'Japan')
// Taro Yamada is 27 years old, and Taro Yamada comes from Japan.
正直に言って、ここまでの検証から見るとsuper
キーワードはthis
の参照先の示しみたいな機能を持つものだと感じています...自分のconstructor
を再定義する前にsuper()
でthis
の参照を一致させる仕事をしているみたいな感じです。(じゃないと親constructor
で定義されたプロパティとメソッドが使えなくなる。)
use Arrow functions
参考文章のClass inheritanceではちょっと気になるところがありますので、
class Taro extends Person {
personalInfo(birthYear, birthPlace) {
setTimeout(() => {
console.log(super.calculateAge(birthYear))
console.log(super.place(birthPlace))
}, 1000)
}
}
let taro = new Taro('Taro Yamada')
taro.personalInfo('1995', 'Japan')
// 27
// Taro Yamada comes from Japan
Web APIs
などの外部関数なら、そのなかのcallback
はアロー関数のように書いても構いません、ちゃんと動けます。むしろアロー関数じゃなきゃ動けなくなる。
class Taro extends Person {
personalInfo() {
setTimeout(() => console.log(this), 1000)
}
}
let taro = new Taro('Taro Yamada')
taro.personalInfo()
// Taro { name: 'Taro Yamada' }
class Taro extends Person {
personalInfo() {
setTimeout(function () {
console.log(this)
}, 1000)
}
}
let taro = new Taro('Taro Yamada')
taro.personalInfo()
// Timeout {
// ...
// }
JavaScriptのthisについて -- this in Arrow function
JavaScriptのthisについて -- API asynchronous callbacks
やはり問題なのはthis
です。アロー関数ならthis
は親のコンテキストを継承するので、ここの親フィールドはTaro {}
だからthis
コンテキストもそれを継承し、ちゃんとプロパティとメソッドをアクセスできる状態です。
Web APIs
のcallback
がアロー関数じゃなければ、this
コンテキストは普通はwindow
に参照していますが、ここはTimeout {}
自体を参照する、なんか興味深いことですが、ここまでの知識がないためまず課題として残しておきます。
下はクロージャの検証です。
class Saburo extends Person {
add(a) {
return (b) => {
console.log(this)
return a + b
}
}
}
let saburo = new Saburo()
console.log(saburo.add(1)(2))
// Saburo {}
// 3
class Saburo extends Person {
add(a) {
return function (b) {
console.log(this)
return a + b
}
}
}
let saburo = new Saburo()
console.log(saburo.add(1)(2))
// undefined
// 3
当たり前かもしれないけど、クロージャでもアロー関数じゃないとthis
の参照が変わってしまう。
(使えるは使えるけれど、this
にかかわるプロパティは使えませんが)
class Saburo extends Person {
constructor(a, b) {
super()
this.a = a
this.b = b
}
add() {
return () => {
console.log(this)
return this.a + this.b
}
}
}
let saburo = new Saburo(1, 2)
console.log(saburo.add()())
// Saburo { a: 1, b: 2 }
// 3
(でも多分、たいていのプロはこういう書き方はしないと思います...。)
Web APIs
ではなくても、なるべくthis
コンテキストを一致するため、メソッド内部の関数はアロー関数の書き方を使った方がいいと思います。
super & [[HomeObject]]
あまり使ったことのないsuper
に出会って、最初super
がthis
を子クラスである自フィールドへ変えたのでしょうか?という素朴な疑問持ってたけれど、いろいろ調べてわかったこと、以下は自分で解釈しました。
[[Super: internals, [[HomeObject]]]]
[[HomeObject]]
自分のオブジェクトで定義されたthis
コンテキストを、ほかのオブジェクトを経由して参照先を再指定することができません。
前this
について色々と調べたから、class
をオブジェクトとして見ると、内部のメソッド(constructor()
もメソッドも)がImplicit bindingthis
は一個上のコンテキストつまりclass
を指しています。そうなると親クラスも子クラスも自分のthis
を持ち、子クラスが延長線のままではthis
を使うという自フィールドに新しく定義しようとする行為はエラーになってしまいます。(this
は自フィールドなのに、メソッドは親フィールド。じゃどっちを参照すればいい?って混乱しちゃうよね)
なのでsuper
を通してthis
を定義する(その前this
はuninitialized)、子クラスthis
が自フィールドに指してもらうというのは、super
がthis
の参照を親フィールドでも自フィールドでもなく、[[HomeObject]] プロパティの値を参照せよって指示を出したわけだと思います。そしてconstructor()
内でsuper()
を使えばそれ以降のthis
の参照が確定して、もし親フィールドのメソッドをアクセスしようとするならsuper.
を通してメソッドをそのまま使わせてもらう。
(super.
でthis
コンテキストを上書きするのではなく、super.
でメソッドをコピーして自フィールドに持ち込むという意味です。super
はthis
に [[HomeObject]] へ参照させ、次第にコピーされたメソッドは自フィールドのプロパティを使うようになる)
How to extend a class without having to use super in ES6? - stackoverflow
class Person {
constructor(birthPlace = 'America') {
this.name = 'Saburo'
this.birthYear = 2000
this.birthPlace = birthPlace
}
test() {
console.log(`I am ${this.name}, I was born on ${this.birthYear} in ${this.birthPlace}`)
}
}
class Saburo extends Person {
constructor(birthPlace) {
// name = 'Saburo'
// ReferenceError: name is not defined
// this.name = 'Saburo'
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
super()
this.birthPlace = birthPlace
}
}
let saburo = new Saburo('Hokkaidou')
saburo.test()
// I am Saburo, I was born on 2000 in Hokkaidou
//
class Shiro extends Person { }
let shiro = new Shiro()
shiro.test()
// I am Saburo, I was born on 2000 in America
this
はuninitialized状態だと、ReferenceError
参照先エラーで、自フィールドにないプロパティがnot defined
になったり、自フィールドのthis
をアクセスする前にsuper
の呼び出しが必須になったりする。
saburo.test()
でname
を出力できるのは決して自フィールドにname
を持ってるわけではなく、親フィールドにアクセスしてプロパティの値を参照し、birthPlace
を変えたのも、決して親フィールドのプロパティを変えたのでなく、super()でthis
が確定し、自フィールドのthis
オブジェクトにbirthPlace
を見つかったからさらに親フィールドへアクセスする必要もなくなった。
自分の解釈や推測が多いけれど何だか前の検証からも納得できる結論に導いている。次回は静的プロパティとメソッドをまとめていきたいと思います。