0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScriptのClassについて part3

Posted at

初めに

今回は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' ]

明らかにstaticMethodclass 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 Personconstructor(){}のプロパティではないため、このルーツには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

Taroextendsのプロトタイプチェーンは:
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 Personthisオブジェクトを継承、

Taro.prototype

Person.prototype

taro.constructorMethod()thisオブジェクトから、
taro.classMethod()Person.prototypeからアクセスできましたが、
しかしどちらでもclass Personへ経由することが不可能なのでstaticMethod()は無理でした。

JavaScriptの継承の仕方を完全に理解するために、最初のthisから、.__protoprototype、インスタンスそして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 = agethis.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' }
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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?