初めに
今回はクラスの基本概念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
の作用でobj
がclass 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がないの知らなかったのでいろんな試行錯誤で苦戦してやっとわかりました。(user1
、user2
にそのあと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()
を利用したsayMixin
のsayHi()
と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
への勉強はとりあえずここまでです。