LoginSignup
0
0

More than 1 year has passed since last update.

JavaScriptのClassについて part4

Last updated at Posted at 2022-08-07

初めに

今回は組み込みメソッドのクラスの拡張についてまとめていきたいと思います。
初投稿ではextendscall()バインドメソッドの互換性に誤解しましたので、もう一度チェックしたうえ修正しました。

参考資料はこちらです。
Extending built-in classes - javascript.info

part3でまとめたように、私のなかではclassextendsもインスタンスもプロトタイプチェーンのルールで作動しているが、違うものとして捉えています。
今回は組み込みメソッドの拡張を紹介する前に、ここでは少し話を巻き戻して、classが`普通の状態ではプロトタイプチェーンがどうなっているかを、図で表していきたいと思います。
(図を作るには面倒くさいけど、そうしないとめちゃくちゃ混乱してしまいます...。)

exclassextending1.png

今回組み込みメソッドの代表 ⇒ 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

exclassextending2.png

(今思えばこれがextendsの正体かもしれません。もともとclassFunctionのインスタンスで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からのインスタンスでなければ使えないんので、filteredArrArrayのインスタンスになったわけです。

exclassextending3.png

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.prototypeObject等々全部一通りやってました。)

図にしてみたら、
exclassextending4.png

うん、SpeciesArraySpeciesArray.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修正:
exclassextending5.png
exclassextending6.png
結論から言いますと、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のfilteredArrinstanceof PowerArrinstanceof Arrayの理由は何となくわかりました。

22/08/09修正:
instanceof PowerArrinstanceof Arrayになったのはextendsの作用でした。call()とは何の関連もありませんでした。

exclassextending7.png

Mixinsではextends[[Prototype]].__proto__、あるいはプロトタイプチェーンの参照)が一つしかないので、extends使わない限り、call()の実用性は限られていると感じています。親クラス.prototypeのプロパティやメソッドを完全放棄する理由があれば使ってもいいと思いますが。

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