初めに
今回は組み込みメソッドのクラスの拡張についてまとめていきたいと思います。
初投稿ではextends
とcall()
バインドメソッドの互換性に誤解しましたので、もう一度チェックしたうえ修正しました。
参考資料はこちらです。
Extending built-in classes - javascript.info
part3でまとめたように、私のなかではclass
のextends
もインスタンスもプロトタイプチェーンのルールで作動しているが、違うものとして捉えています。
今回は組み込みメソッドの拡張を紹介する前に、ここでは少し話を巻き戻して、class
が`普通の状態ではプロトタイプチェーンがどうなっているかを、図で表していきたいと思います。
(図を作るには面倒くさいけど、そうしないとめちゃくちゃ混乱してしまいます...。)
今回組み込みメソッドの代表 ⇒ Array
デモ用のclass
⇒ class PowerArray
とそのインスタンス ⇒ new PowerArray
22/08/09修正:
FunctionとObjectはお互いのインスタンスだったことが知っていますが、class
もインスタンスもこの特性を持つことが知りませんでした。
関連図を再作成しました。
class extends Built-in
class PowerArray extends Array {
isEmpty() {
return this.length === 0
}
}
let arr = new PowerArray(1, 2, 5, 10, 50)
console.log(arr) // PowerArray(5) [ 1, 2, 5, 10, 50 ]
console.log(arr.isEmpty()) // false
console.log(Object.getPrototypeOf(arr) === PowerArray.prototype) // true
console.log(arr.constructor === PowerArray) // true
今回の例はほぼ参考文章から引用しました。初めてみたとき、こんなこともできるか!って思わず。上のようにextends
を使ってArray
へ拡張、インスタンス名前付きの配列ができました。
プロトタイプチェーンのチェックでは、インスタンスarr
は通常通りにPowerArray.prototype
を参照しているけど、class PowerArray
のプロトタイプチェーンがすでに変わっています。
console.log(Object.getPrototypeOf(PowerArray) === Array) // true
console.log(Object.getPrototypeOf(PowerArray.prototype) === Array.prototype) // true
(今思えばこれがextends
の正体かもしれません。もともとclass
はFunction
のインスタンスでFunction.prototype
にたどり着くべきだったけれど、extends
で参照先が変えられ、それで別のclass
あるいは今回のようにArray
のメソッドを利用できるようになったのですね。)
22/08/09修正:
初投稿のときclass PowerArray
のプロトタイプチェーンの検定を着目してたけど、Class checking: "instanceof"ではclass
やオブジェクトが複数のインスタンスであることが可能だったと気づいて、慌てて修正しました...。
そしてfilter()
やmap()
などArray.prototypeに置かれたメソッドを利用したfilteredArr
に、もう一度プロトタイプチェーンの確認したら、
let filteredArr = arr.filter(item => item >= 10)
console.log(Object.getPrototypeOf(filteredArr) === PowerArray.prototype) // true
console.log(filteredArr.constructor === PowerArray) // true
console.log(arr instanceof PowerArray) // true
console.log(filteredArr instanceof Array) // true
new PowerArray
(変数arr)はPowerArray
からのインスタンスだったのでPowerArray.prototype
に参照してます。したがって、別の変数filteredArr
にアサインしてもこちらのプロトタイプチェーンは変わらない。ただ、Array
のメソッドを利用するならArray
からのインスタンスでなければ使えないんので、filteredArr
がArray
のインスタンスになったわけです。
22/08/09修正:
図2と図3では、要するにextends
拡張後、もともとインスタンスnew PowerArray
も親クラスの拡張フィールドArray
のインスタンスだったので、filter()
やmap()
メソッドの影響で変わったことではなかったです。
この方法のメリットは親クラスの.prototype
への参照しながら、指定した組み込みメソッドを利用できています。
let filteredArr = arr.filter(item => item >= 10)
console.log(filteredArr) // PowerArray(2) [ 10, 50 ]
console.log(filteredArr.isEmpty()) // false
なぜextends
でプロトタイプチェーンがこんな複雑な関係になったか、そして同じインスタンスなのに、filter()
実施するまえとその後、instanceof
の参照が変えられたことに頭がごちゃごちゃになって、図にしたらちょっとスッキリしました。(図の作成が面倒だけど!!)
自分なりにまとめた解釈は、extends
は図のようなほかのフィールド内部アクセスできる権限を与えるキーワードで、アクセスさせるためにclass PowerArray
のプロトタイプチェーンも、PowerArray.prototype
のプロトタイプチェーン参照を変えなきゃいけない。
new
でインスタンスオブジェクトを創ると、Array
へアクセスして創られ返された配列が、親とインスタンスの所属する関係が成立したと思います(22/08/10修正)。
しかしArray
のメソッドを利用するならextends
権限持っていないインスタンスには無理でした。なのでinstanceの参照をArray
に変えて、つまり普通の配列と同じArray
のインスタンスにしたらArray
のメソッドが利用できるではないかと。
instance参照の推測はただの誤解だったです。
Symbol.species
class SpeciesArray extends Array {
static get [Symbol.species]() {
return Array
}
isEmpty() {
return this.length === 0
}
}
Symbol.species - MDN
これも初めて出会った書き方です。MDNの説明によると、class
オブジェクトではなく、親のArray
オブジェクトを返すようになるということです。
では、前の例とはどこが違うのかな?
let a = new SpeciesArray(1, 2, 3)
console.log(a instanceof SpeciesArray) // true
console.log(Object.getPrototypeOf(a) === SpeciesArray.prototype) // true
ここまでは変わりなく、しかし↓は
let filteredArr = a.filter(item => item >= 10)
console.log(filteredArr instanceof Array) // true
console.log(Object.getPrototypeOf(filteredArr) === Array.prototype) // true
プロトタイプチェーンが変えられた...ぞ?
(完全に予想外だったので、実際のコードではFunction
、、Function.prototype
、Object
等々全部一通りやってました。)
うん、SpeciesArray
とSpeciesArray.prototype
完全に無視されました。
22/08/09修正:
インスタンスとしてはSymbol.species
使っても変わらなかったです。
class extends Built-inの例ではfilteredArr.isEmpty()
が使えるけど、
class PowerArray extends Array {
isEmpty() {
return this.length === 0
}
}
let arr = new PowerArray(1, 2, 5, 10, 50)
console.log(arr.isEmpty()) // false
let filteredArr = arr.filter(item => item >= 10)
console.log(filteredArr.isEmpty()) // false
ここにきたら、
class SpeciesArray extends Array {
static get [Symbol.species]() {
return Array
}
isEmpty() {
return this.length === 0
}
}
let a = new SpeciesArray(1, 2, 3)
console.log(a.isEmpty()) // false
let filteredArr = a.filter(item => item >= 10)
console.log(filteredArr.isEmpty())
// TypeError: filteredArr.isEmpty is not a function
filteredArr.isEmpty()
が使えなくなるのがその原因でした。
もう一度まとめてみると、もし親クラスのプロトタイプチェーン(プロパティやメソッド)まだ使いたい場合はclass extends Built-inのように書いていいと思います。
インスタンスオブジェクト(つまり親クラス内部で生んだ結果)だけ利用したいなら、Symbol.species
のほうがいいと思います。
(何となく.prototype
にあるpublic状態にしなければならないメソッドと相性がいいかも。)
compare with call()
call()
を使って背後にある動きを真似してみました。
// wrapper without extends
class WrapperArray {
constructor(arr) {
this.Array = [...arr]
}
isEmpty() {
return this.length === 0
}
}
let arr2 = new WrapperArray([10, 20, 30])
console.log(arr2)
// WrapperArray { Array: [ 10, 20, 30 ] }
let wrappedArr = Array.prototype.filter.call(arr2.Array, x => x <= 20)
console.log(wrappedArr)
// [ 10, 20 ]
console.log(wrappedArr instanceof Array) // true
console.log(Object.getPrototypeOf(wrappedArr) === Array.prototype) // true
(WraaperArray
は配列風オブジェクトではなく、オブジェクトに配列プロパティが入っています。)
まずはextends
使わずcall()
だけでthis
が変えられた状態ならinstanceof
とプロトタイプチェーンの様子を見ました。
単にこの二つの結果からみればSymbol.species
に近かったようです。
22/08/09修正:
結論から言いますと、call()
のやり方ではSymbol.species
の結果と一緒でした。
つまりcall()でもextends
がなくてもオブジェクトへの拡張を、指定したインスタンスに同じ効果を施すことができ、ただ副作用としては親クラスから与えられたプロトタイプチェーンが変えられてしまいます。
そしてextends
を加えたら、
// wrapper extends
class WrapperArray extends Array {
isEmpty() {
return this.length === 0
}
}
let arr3 = new WrapperArray(40, 50, 60)
console.log(arr3)
// WrapperArray(3) [ 40, 50, 60 ]
let wrappedArr2 = Array.prototype.filter.call(arr3, x => x <= 50)
console.log(wrappedArr2)
// WrapperArray(2) [ 40, 50 ]
console.log(wrappedArr2 instanceof Array) // true
console.log(Object.getPrototypeOf(wrappedArr2) === WrapperArray.prototype) // true
WrapperArray
はもちろん配列風オブジェクト、そしてinstanceof
とプロトタイプチェーンはclass extends Built-inと同じ結果でした。
もちろんcall()
使っても使わなくても同じ結果がくるし、これだったら何の説明にはならないかもしれないけど、ただfilter()
一つだけでclass extends Built-inのfilteredArr
がinstanceof PowerArr
⇒instanceof Array
の理由は何となくわかりました。
22/08/09修正:
instanceof PowerArr
⇒instanceof Array
になったのはextends
の作用でした。call()
とは何の関連もありませんでした。
Mixinsではextends
も[[Prototype]]
(.__proto__
、あるいはプロトタイプチェーンの参照)が一つしかないので、extends
使わない限り、call()
の実用性は限られていると感じています。親クラス.prototype
のプロパティやメソッドを完全放棄する理由があれば使ってもいいと思いますが。