0
0

More than 1 year has passed since last update.

JavaScriptのClassについて part5

Last updated at Posted at 2022-08-11

初めに

今回はクラスの基本概念instanceof演算子と、単一継承でのミックスインの実現をまとめていきたいと思います。

参考文章はこちらです。
Class checking: "instanceof"
Mixins

instanceof

インスタンスと親の所属関係を表す演算子です。プロトタイプチェーンのチェックにもよく使われています。
基本の構文:instanceof - MDN

object instanceof constructor

インスタンスの関連図は前の文章でもよく使っていました。
JavaScriptの.__proto__とprototypeとinstance
JavaScriptのClassについて part4

オブジェクトが複数の親を持つインスタンスであることが可能です。
例えば、クラスの場合はextendsで拡張したら自分のインスタンスは拡張先(インスタンスにとってもうひとつの親)の所属関係を結びつけることができ、そしてpart3 this in static methodsの検証でわかったように、拡張先の静的メソッドやプロパティ以外は利用できるようになります。

ただプロトタイプチェーンの.__proto__も、extendsも一つの親に限られているので勝手に変えたらパフォーマンスが落ちたり全く違う結果に導いたりします。インスタンスならこういう問題ありません、複数の親を持っても問題にならないです。

でも、プロトタイプチェーンを変えずextends拡張も使わず、果たしてインスタンスの所属関係だけで親へのアクセスが実現できるのかな?

[Symbol.hasInstance]

参考文章の例を借りて検証を入れてみました。

class Animal {
  static [Symbol.hasInstance](obj) {
    if (obj.canEat) return true
  }

  constructor() {
    this.name = 'Taro'
    this.constructorMethod = () => {
      console.log('Hi')
    }
  }

  sayHi() {
    return `Hi, ${this.name}`
  }
}

let obj = {
  canEat: 1,
  name: 'Mick'
}

console.log(obj instanceof Animal) // true
console.log(obj instanceof Object) // true
console.log(obj instanceof Function) // false
console.log(obj.name) // Mick
console.log(obj.sayHi()) // TypeError: obj.sayHi is not a function
console.log(obj.constructorMethod()) // TypeError: obj.constructorMethod is not a function
console.log(obj.sayHi()) // TypeError: obj.sayHi is not a function

確かにSymbol.hasInstanceの作用でobjclass Animalのインスタンスになったけれど、実際instanceofの結果を変えても

console.log(Object.getPrototypeOf(obj) === Object.prototype) // true
console.log(Object.getPrototypeOf(obj) === Animal.prototype) // false
console.log(Object.getPrototypeOf(obj) === Animal) // false

objのプロトタイプチェーン何も変わらなかったです。instanceof演算子の結果だけを変えてもプロトタイプチェーンは変えられません。
このメソッドは初見でどこに使われるかはよくわかりませんが、自分の感覚では単にインスタンスの管理なら使えると思います。

class Animal {
  static [Symbol.hasInstance](obj) {
    if (obj.myMethod === this.prototype.myMethod) return true
  }

  myMethod() {
    console.log('Animal Method')
  }
}

function myMethod() {
  console.log('Global Method')
}

let obj1 = {
  myMethod: Animal.prototype.myMethod
}

let obj2 = {
  myMethod: myMethod
}

obj1.myMethod() // Animal Method
obj2.myMethod() // Global Method

console.log(obj1 instanceof Animal) // true
console.log(obj2 instanceof Animal) // false

Object.prototype.toString()

基本の使い方

console.log(Object.prototype.toString(obj1)) // [object Object]
console.log(obj1.toString()) // [object Object]

console.log(Object.prototype.toString.call([])) // [object Array]
console.log(Object.prototype.toString.call(null)) // [object Null]
console.log(Object.prototype.toString.call(123)) // [object Number]
console.log(Object.prototype.toString.call(new Date())) // [object Date]

ここから少し別の話ですが、
Object.prototype.toString()、MDNの説明ではtoStringを上書きできると書いてあります。

console.log(Animal.prototype) // {}
console.log(Animal.prototype.toString) // [Function: toString]
console.log(Animal.prototype.hasOwnProperty('toString')) // false
// Animal.prototype => Object.prototype.toString

Animal.prototype.toString = function () {
  return `${this.name}`
}

console.log(Animal.prototype) // { toString: [Function (anonymous)] }
console.log(Animal.prototype.toString) // [Function (anonymous)]
console.log(Animal.prototype.hasOwnProperty('toString')) // true
// Animal.prototype.toString

console.log(cat.toString()) // cat

でもAnimal.prototypeで新設されたtoStringが先に見つかったことで返されたのではないかと。

[Symbol.toStringTag]

このメソッドでクラスの独自のオブジェクトタグをつけられます。

let user1 = {
  id: 1,
  [Symbol.toStringTag]: 'User'
  // this property has no length
}

let user2 = {
  id: 2,
  [Symbol.toStringTag]: 'Admin'
}
console.log(Object.prototype.toString.call(user1))
// [object User]
console.log(user1[Symbol.toStringTag])
// User

そして↓もSymbol.toStringTag MDNの例を参考して書いてみました。

class ValidatorClass {
  constructor(obj) {
    // console.log(Object.keys(obj).length) // 1
    if (Object.keys(obj).length > 0) {
      this.obj = obj
    }
  }

  get [Symbol.toStringTag]() {
    if (this.obj[Symbol.toStringTag] === 'User') {
      return 'ValidUser'
    } else if (this.obj[Symbol.toStringTag] === 'Admin') {
      return 'ValidAdmin'
    }
  }
}

console.log({}.toString.call(new ValidatorClass(user1)))
// [object ValidUser]
console.log({}.toString.call(new ValidatorClass(user2)))
// [object ValidAdmin]

最初は空のオブジェクトなら排除してほしいからif (Object.keys(obj).length > 0)を設定してたけど、[Symbol.toStringTag]はlengthがないの知らなかったのでいろんな試行錯誤で苦戦してやっとわかりました。(user1user2にそのあとidを付けました。)
MDNもう一度みたら、このプロパティは列挙不可と書いてあったの...。

Mixins

Object.assign()

Object.assign()はディープコピーを実現できるメソッドです。(元のオブジェクトに同じプロパティ名がすでに存在してたら上書きします。)
↓は基本の例です。

let mixinMethods = {
  sayHi() {
    console.log(`Hello, ${this.name}`)
  },
  sayBye() {
    console.log(`Bye, ${this.name}`)
  }
}

class User {
  constructor(name) {
    this.name = name
  }
}

Object.assign(User.prototype, mixinMethods)
new User('Mick').sayHi() // Hello, Mick

コピーしたメソッドを自分の.prototypeに置くので、extendsしても問題ありません。自分のインスタンスのプロトタイプチェーンが変えられなければもちろんメソッドたちが使えます。

↓はObject.assignを応用した別の方法です。

let sayMethod = {
  say(phrase) {
    console.log(phrase)
  }
}

let sayMixin = {
  __proto__: Object.create(sayMethod), // return new object
  sayHi() {
    super.say(`Hello, ${this.name}`)
  },
  sayBye() {
    super.say(`Bye, ${this.name}`)
  }
}

class User {
  constructor(name) {
    this.name = name
  }
}

Object.assign(User.prototype, sayMixin)
new User('Lucy').sayBye() // Bye, Lucy

プロパティ__proto__を操作することで、sayHi()sayBye()のなかでsuper経由して__proto__であるsayMethodオブジェクトにアクセスしsay()を利用した。
そしてsayMixinがコピーされUser.prototypeに置かれてもsuperの特性でメソッドのthisを自分の内部プロパティ[[HomeObject]]へ参照させるため、say()を利用したsayMixinsayHi()sayBye()が自分の__proto__へたどり着いた。
super()でオブジェクトのthisを[[HomeObject]]へ参照させるのが前の文章では書いてありますすが、メソッドも[[HomeObject]]プロパティが持ってるのここで初めて知りました。

EventMixin

完全に初見なので参考文章の例をそのまま使用しました。

let eventMixin = {
  on(eventName, handler) {
    // if event Handler does not exist, create it
    if (!this._eventHandlers) this._eventHandlers = {}
    // if there didn't exist a matched event name, according to the event name(property name), assign []
    if (!this._eventHandlers[eventName]) {
      this._eventHandlers[eventName] = []
    }
    // find the matched array and push handler(callback function)
    this._eventHandlers[eventName].push(handler)
  },
  off(eventName, handler) {
    // select eventHandler and its matched eventHandler name property
    let handlers = this._eventHandlers && this._eventHandlers[eventName]
    if (!handlers) return
    for (let i = 0; i < handlers.length; i++) {
      // note: why use == ?
      if (handlers[i] == handler) {
        // delete the matched handler property and start at the same index (i--)
        handlers.splice(i--, 1)
      }
    }
  },
  trigger(eventName, ...args) {
    // if didn't exist event handlers or event name then return
    if (!this._eventHandlers || !this._eventHandlers[eventName]) return
    // if it exist, call all of the event handlers, and put arguments to create those events
    this._eventHandlers[eventName].forEach(handler => handler.apply(this, args))
  },
}

注釈の英語は説明に合わせて自分なりの解釈です。正しいかどうかわかりませんが。
on()はセットメソッド、off()は削除メソッド、trigger()はイベントのトリガーです。

class Menu {
  choose(value) {
    // call trigger
    this.trigger('select', value)
  }
}

Object.assign(Menu.prototype, eventMixin)

let menu = new Menu()
// set 'select' event
menu.on('select', value => console.log(`Value selected: ${value}`))
menu.choose('123') // Value selected: 123

例の使い方としては、さきにclassに欲しいイベントトリガー(イベント名とコールバック関数)を設置しておいて、on()でイベント名に応じてメソッドの詳しい動作を設定し、最後はclassのメソッドchoose()を呼び出し、値を入れて処理させてもらいました。

イベントミックスインの応用は完全に理解するまで自分にはまだまだ難しいと思いますが、今のところclassへの勉強はとりあえずここまでです。

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