初めに
今回は組み込みメソッドのクラスの拡張についてまとめていきたいと思います。
初投稿では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のプロパティやメソッドを完全放棄する理由があれば使ってもいいと思いますが。




