初めに
今回はclass
の静的メソッドの書き方と使い分けをまとめていきたいと思います。
主な参考文章はこちらです。
Static properties and methods - javascript.info
Public and private class fields - v8.dev
Public class fields - MDN
Private class features - MDN
Static properties and methods
static - MDN
static
とはclass
で静的プロパティやメソッドを定義するキーワードです。
static
で扱われるメソッドのthis
は自分のいるclass field
に参照しています。
class Person {
static staticProperty = 'abc'
static staticMethod() {
console.log(this === Person)
}
}
//
console.log(Person.staticProperty) // abc
Person.staticMethod() // true
これからclass
内部のthis
と、static
メソッドへの応用などまとめてみたいと思います。
this
in static methods
まずはthis
の参照についてです。前のpart2で紹介したように、class
の内部でもどこに置かれたかと、どのように書かれるかと、this
の参照が変わってきます。
ここでは簡単な例でthis
の参照先を把握していきたいと思います。
class Person {
constructor() {
this.constructorMethod = () => {
console.log(this === Person)
}
}
static staticMethod() {
console.log(this === Person)
}
classMethod() {
console.log(this === Person)
}
}
// Person.constructorMethod()
// TypeError: Person.constructorMethod is not a function
Person.staticMethod()
// true
// Person.classMethod()
// TypeError: Person.classMethod is not a function
下のようにconstructor()
とclass
フィールドでのメソッドはインスタンスで反映されますが、インスタンスからstaticMethod()
利用できません。
let person = new Person()
person.constructorMethod() // false
// person.staticMethod()
// TypeError: person.staticMethod is not a function
person.classMethod() // false
ここで各自の所有プロパティを見てみたら
console.log(Object.getOwnPropertyNames(Person))
// [ 'length', 'prototype', 'staticMethod', 'name' ]
console.log(Object.getOwnPropertyNames(person))
// [ 'constructorMethod' ]
console.log(Object.getOwnPropertyNames(Person.prototype))
// [ 'constructor', 'classMethod' ]
明らかにstaticMethod
はclass Person
の所有メソッドになって、インスタンスに提供できませんでした。part2でわかったように、インスタンスperson
のプロトタイプチェーンはPerson.prototype
なので、class Person
内部所有のメソッドアクセスできません。
(インスタンスperson
のプロトタイプチェーンは、
prototypeのclass Person
からのthis
オブジェクト
↓
Person.prototype
↓
Object.prototype
)
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype) // true
staticMethod()
はclass Person
のconstructor(){}
のプロパティではないため、このルーツにはstaticMethod()
含まれていないし、class Person
を経由して呼び出さねばならない状態になっています。
ここから少し別の話ですが、extends
の子クラスとそのインスタンスだったらどうなるのでしょうか?
extends
で親クラスへ拡張した子クラスなら、ルーツのなかにclass Person
があったので、親クラスの静的メソッドでもアクセスできます。
//
class Taro extends Person { }
console.log(Object.getPrototypeOf(Taro) === Person) // true
// Taro.constructorMethod()
// TypeError: Taro.constructorMethod is not a function
Taro.staticMethod() // false
// Taro.classMethod()
// TypeError: Taro.classMethod is not a function
Taro
のextends
のプロトタイプチェーンは:
Taro
↓
Person
↓
Function.prototype
このルーツではclass Person
を経由していた。class Person
所有の静的メソッドが使えるけど、constructor()
と、Person.prototype
へ移行するclass
内部のメソッドが使えません。
console.log(Object.getPrototypeOf(Taro) === Person) // true
console.log(Object.getPrototypeOf(Person) === Function.prototype) // true
ならextends
のインスタンスはどうなったのでしょう?
extends
とインスタンスのメリットを兼ねてどちらのメソッドでもアクセスできるかな?
let taro = new Taro()
taro.constructorMethod() // false
// taro.staticMethod()
// TypeError: taro.staticMethod is not a function
taro.classMethod() // false
console.log(Object.getPrototypeOf(taro) === Taro.prototype) // true
console.log(Object.getPrototypeOf(Taro.prototype) === Person.prototype) // true
結局無理でした。インスタンスtaro
は
class Taro
からclass Person
のthis
オブジェクトを継承、
↓
Taro.prototype
↓
Person.prototype
。
taro.constructorMethod()
はthis
オブジェクトから、
taro.classMethod()
はPerson.prototype
からアクセスできましたが、
しかしどちらでもclass Person
へ経由することが不可能なのでstaticMethod()
は無理でした。
JavaScriptの継承の仕方を完全に理解するために、最初のthis
から、.__proto
、prototype
、インスタンスそしてclass
、ここまできたらextends
の拡張、プロトタイプチェーン、根本的に別のものだと感じています。
自分の母国語は中国語で、原文の英語はもちろん、日本語の文章も参考しながら自分の検証を加えて勉強してきました。ほかのプログラミング言語は大した勉強したことないのでこういう理解は適切かなー、こんなこと書いていいかなー、本当に不安でした。
でも、たぶんみんなの中でもともと継承やら拡張やら定義が違っていて、自分の納得できる結論にしててもいいではないかと。
自分の中ではextends
はあくまでもフィールドの拡張、そしてプロトタイプチェーンは継承のルーツにしたほうが納得できる分け方だと思います。
ではどうやって静的メソッドをアクセスするでしょうか。this
を使うのと、使わない方法をやってみたいと思います。
Static with using this
binding
class Person {
constructor(age = 18, favoriteColor = 'red') {
this.age = age
this.favoriteColor = favoriteColor
}
static change(age, favoriteColor) {
this.age = age
this.favoriteColor = favoriteColor
console.log(this)
return `I am ${this.age}. My favorite color is ${this.favoriteColor}`
}
}
let person1 = new Person()
console.log(person1)
// Person { age: 18, favoriteColor: 'red' }
person1.age = 20
console.log(person1)
// Person { age: 20, favoriteColor: 'red' }
console.log(person1.change(20))
// TypeError: person1.change is not a function
person1.age
値を差し替えるなら直接値を変えていいですが、静的メソッドをアクセスできません。
下のようにバインドメソッドを使って、
console.log(Person.change.call(person1, 30, 'blue'))
// Person { age: 30, favoriteColor: 'blue' }
// I am 30. My favorite color is blue
console.log(Person.change.apply(person1, [40, 'yellow']))
// Person { age: 40, favoriteColor: 'yellow' }
// I am 40. My favorite color is yellow
静的メソッドchange()
はclass Person
を通して呼び出さねばならなかったので、thisオブジェクトのage
が差し替えられるように、change(age, favoriteColor)
の内部にthis.age = age
、this.favoriteColor = favoriteColor
を書いておきました。
それでバインドメソッドcall()
、apply()
で関数を強制的に別の引数に参照させたり、あるいは下にある、別の例のようにオブジェクトコンテキストを参照させたりするのもできます。
class CallObj {
constructor(age = 18, favoriteColor = 'red') {
this.age = age
this.favoriteColor = favoriteColor
}
static change() {
console.log(this)
return `I am ${this.age}. My favorite color is ${this.favoriteColor}`
}
}
const obj = {
age: 50,
favoriteColor: 'white'
}
console.log(CallObj.change())
// [class CallObj]
// I am undefined. My favorite color is undefined
console.log(CallObj.change.call(obj))
// { age: 50, favoriteColor: 'white' }
// I am 50. My favorite color is white
インスタンスもオブジェクトだから同じこともできます。
let callObj = new CallObj(60, 'black')
console.log(callObj)
// CallObj { age: 60, favoriteColor: 'black' }
console.log(CallObj.change.call(callObj))
// CallObj { age: 60, favoriteColor: 'black' }
// I am 60. My favorite color is black
callObj.age = 70
console.log(CallObj.change.apply(callObj))
// CallObj { age: 70, favoriteColor: 'black' }
// I am 70. My favorite color is black
console.log(CallObj.change())
// [class CallObj]
// I am undefined. My favorite color is undefined
ただ、なぜかbind()
メソッドだけは変な動きがあって、調べてもまだ原因が分からなくて、課題として残しておきます。
console.log(CallObj.change.bind(callObj))
// [Function: bound change]
Static without using this
class Person {
constructor(age = 18, favoriteColor = 'red') {
this.age = age
this.favoriteColor = favoriteColor
}
static change(obj, age, favoriteColor) {
obj.age = age
obj.favoriteColor = favoriteColor
console.log(this)
return `I am ${this.age}. My favorite color is ${this.favoriteColor}`
}
}
let person4 = new Person(20, 'blue')
console.log(person4)
// Person { age: 20, favoriteColor: 'blue' }
console.log(Person.change(person4, 30, 'yellow'))
// [class Person]
// I am undefined. My favorite color is undefined
console.log(person4)
// Person { age: 30, favoriteColor: 'yellow' }
この方法、正直おすすめしません。(調べるなか出てきた方法ですが、仕組みが理解したら、なぜかくどくてもっと直観的な書き方があると思った。)
私から見れば、静的メソッドを利用してインスタンスのプロパティバリューを変えようとしたら、Static with using this
bindingのように値を書き換えたり、あるいはcall()
やapply()
で変えればいいのではないかと。もしくは一番根本的に、constructor()
の引数として入れればいいと思います。
大量生産を考えたら一番やりやすいのはたぶん、先にJSONのような固定したフォーマットのオブジェクトを立てて、ループでcall()
使ってオブジェクト要素を走査し、それならインスタンスが使わなくメモリーにも優しいし、静的メソッドのアクセス権限(extends
)をどこの子クラスにあげたかをいちいち覚えなくてもよいのではないかと。(ただの推測、実際テストしたことがありません。)
Class fileds
Public class fileds
ES6には、まずJavaのようにprivate variableを設定できないので、区分するために_
(underscore)を先頭に書くが、実際に値のアクセスや変更ができます。
class IncreasingCounter {
constructor() {
this._count = 0
}
}
const counter = new IncreasingCounter()
console.log(counter)
// IncreasingCounter { _count: 0 }
counter._count = 10
console.log(counter)
// IncreasingCounter { _count: 10 }
class IncreasingCounter {
constructor() {
this._count = 0
}
getValue() {
return `the current value is ${this._count}`
}
increment() {
this._count++
console.log(this)
}
}
const counter = new IncreasingCounter()
console.log(counter.getValue())
// the current value is 0
counter.increment()
// IncreasingCounter { _count: 1 }
console.log(counter.getValue())
// the current value is 1
console.log(Object.getOwnPropertyNames(IncreasingCounter.prototype))
// [ 'constructor', 'getValue', 'increment' ]
console.log(Object.getOwnPropertyNames(counter))
// [ '_count' ]
こちらの書き方のように、constructor()
使わず書いても同じです。
class IncreasingCounter {
_count = 0
getValue() {
return `the current value is ${this._count}`
}
increment() {
this._count++
console.log(this)
}
}
const counter = new IncreasingCounter()
console.log(counter.getValue())
// the current value is 0
counter.increment()
// IncreasingCounter { _count: 1 }
console.log(counter.getValue())
// the current value is 1
console.log(Object.getOwnPropertyNames(IncreasingCounter.prototype))
// [ 'constructor', 'getValue', 'increment' ]
console.log(Object.getOwnPropertyNames(counter))
// [ '_count' ]
(単にclass
フィールドに書いて、constructor()
にもう一度書いたら同じプロパティであれば最終的に上書きされるのですが。)
つまり_
を使っても使わなくても、class
フィールドでも、constructor()
でもPublic
な状態であることがわかった。
(static
で扱われた静的メソッド/プロパティも同じく、Publicな状態で自由に呼び出すことができます。)
Private class fileds
Private状態にしてほしいなら、#
を使えば制限できます。
class IncreasingCounter {
#count = 0
getValue() {
return `the current value is ${this.#count}`
}
increment() {
this.#count++
console.log(this)
}
}
const counter = new IncreasingCounter()
console.log(counter.getValue())
// the current value is 0
counter.increment()
// IncreasingCounter { }
console.log(counter.getValue())
// the current value is 1
class
フィールド内部なら、#count
へアクセスや値の変更などでき、関連のある関数も正しく動作してくれましたが、
// console.log(counter.#count)
// SyntaxError: Private field '#count' must be declared in an enclosing class
// console.log(IncreasingCounter.#count)
// SyntaxError: Private field '#count' must be declared in an enclosing class
console.log(Object.getOwnPropertyNames(IncreasingCounter.prototype))
// [ 'constructor', 'getValue', 'increment' ]
console.log(Object.getOwnPropertyNames(counter))
// [ ]
外部からのアクセスであればclass IncreasingCounter
ではプロパティが見つからないし、見つけることもできません。
Public & private static properties
なので、静的メソッドやプロパティも#
使えば設立できます。
class IncreasingCounter {
static #count = 1
static #digit = 1
static #digits() {
return IncreasingCounter.#count * (10 ** IncreasingCounter.#digit)
}
addDigit() {
IncreasingCounter.#digit++
}
getResult() {
return IncreasingCounter.#digits()
}
}
let counter = new IncreasingCounter()
console.log(counter)
// IncreasingCounter {}
counter.addDigit()
console.log(counter.getResult())
// 100
extends
で拡張したらどうなるかな。
class TestCounter extends IncreasingCounter {
test() {
// return `the current value is ${TestCounter.#count}`
// SyntaxError: Private field '#count' must be declared in an enclosing class
// return `the current value is ${IncreasingCounter.#count}`
// SyntaxError: Private field '#count' must be declared in an enclosing class
// return `the current value is ${this.#count}`
// SyntaxError: Private field '#count' must be declared in an enclosing class
return `the result is ${this.getResult()}`
}
}
let testCounter = new TestCounter()
console.log(testCounter.test())
// the result is 100
どうやらprivate状態のプロパティやメソッドが子クラスでは使用できませんでした。
うっかりして書き替えられたくないプロパティ、メソッドはprivateで一か所に集中管理したり、動作させたり、結果だけPublicのメソッドで取り出すようにしたら、何か安全かつ色んなことができて、面白いですね。
ほかの書き方等々
分割代入でデフォルト値の練習です。
class Person {
constructor({ age = 18, favoriteColor = 'red' } = {}) {
this.age = age
this.favoriteColor = favoriteColor
}
static change(age) {
this.age = age
return `I am ${this.age}.`
}
}
let person1 = new Person()
console.log(person1.age) // 18
// console.log(person1.change())
// TypeError: person2.change is not a function
let person2 = new Person({})
console.log(person2.age) // 18
console.log(person2)
// Person { age: 18, favoriteColor: 'red' }
let person3 = new Person({ age: 20 })
console.log(person3.age) // 20
console.log(person3)
// Person { age: 20, favoriteColor: 'red' }