1
1

More than 1 year has passed since last update.

JavaScriptのClassについて part1

Last updated at Posted at 2022-07-20

初めに

今回はES6のClassの基礎概念をまとめていきたいと思います。

主な参考文章:
Class basic syntax - javascript.info
Are ES6 classes just syntactic sugar for the prototypal pattern in Javascript? - stackoverflow

Classとは

同じオブジェクトを大量に作成するテンプレートです。
コンストラクタ関数とnewインスタンスの関係に似ています。MDNの説明では、

class キーワードは ES6 で導入されましたが、シンタックスシュガーであり、JavaScript は引き続きプロトタイプベースです
継承とプロトタイプチェーン

たしかにclassはコンストラクタ関数の動きと似ていますが、構造としては違いがあります。それにただのシンタックスシュガーではありません。

syntax

今回デモのコードです。

class Person {
  constructor(name) {
    this.name = name
  }
  nation = 'Japan'

  greeting() {
    return `Hello, ${this.name}`
  }
}

コンストラクタ関数と比べるためのコードです。

function Person(name) {
  this.name = name
  this.nation = 'Japan'

  this.greeting = function () {
    return `Hello, ${this.name}`
  }
}

まずはclassへの検証です。
上はclass Person、下はfunction Personのインスタンス、

class vs. constructor function
// class
console.log(typeof Person) // function
console.log(Person.prototype.constructor) // [class Person]
console.log(Object.getPrototypeOf(Person) === Function.prototype) // true
console.log(Person instanceof Function) // true
console.log(Object.getOwnPropertyNames(Person.prototype))
// [ 'constructor', 'greeting' ]

// constructor function
console.log(typeof Person) // function
console.log(Person.prototype.constructor) // [Function: Person]
console.log(Object.getPrototypeOf(Person) === Function.prototype) // true
console.log(Person instanceof Function) // true
console.log(Object.getOwnPropertyNames(Person.prototype))
// [ 'constructor' ]

classconstructor functionFunctionのインスタンスであり、プロトタイプチェーンもFunction.prototypeとつながっています。

しかしPerson.prototypeでは、なかみの構成が違います。どちらもインスタンスを創れるので必ずconstructorを持っていますが、classオブジェクトの中にある関数はconstructorによって定義されていませんので、class Person.prototypeオブジェクトに移行され、また、class Person.prototypeで新しいプロパティやメソッドを書き込みできないread-onlyになります。
下のコード見てみましょう。

function Test() {
  this.hello = function hello() { }
}
const test = new Test()
console.log(test.hello) // [Function: hello]
console.log(Object.getPrototypeOf(test.hello) === Function.prototype) // true

const Hello = test.hello
const newHello = new Hello()
console.log(newHello) // hello {}

コンストラクタ関数なら、中の関数がまた新しいインスタンスを創ることができる。それ自体がconstructorに定義されているから。

class Test {
  hello() { }
}
const test = new Test()
console.log(test.hello) // [Function: hello]
console.log(Object.getPrototypeOf(test.hello) === Function.prototype) // true

const Hello = test.hello
const newHello = new Hello()
// TypeError: Hello is not a constructor

// note: [[IsClassConstructor]]

しかしclassはオブジェクト中の関数を、自分のprototypeに移行して保存するのでread-only読み取りのみの状態になり、newでインスタンスを創ることができませんでした。

ほかにも異なるところがありますが、class vs. constructor functionにご参考になればと。

getter/setter

オブジェクトのgetter/setterはclassでも使えます。
JavaScriptのObjectについて -- get & set

class Person {
  constructor(name, nation) {
    this.name = name
    this.nation = nation
  }

  get greeting() {
    return `Hello, I am ${this._name}, I come from ${this._nation}`
  }

  set name(value) {
    if (value.length < 1) {
      console.log('Name is too short.')
      return
    }
    this._name = value
  }

  set nation(value) {
    if (value < 2) {
      console.log('Nation name is too short.')
      return
    }
    this._nation = value
  }
}

const Taro = new Person('Taro', 'Japan')
console.log(Taro)
// Person { _name: 'Taro', _nation: 'Japan' }
console.log(Taro.greeting)
// Hello, I am Taro, I come from Japan

classget/setconstructor()から返されたオブジェクトPerson { _name: 'Taro', _nation: 'Japan' }のプロパティをアクセスして使用する。

しかしTaroにはgetgreetring関数が持ってないのに、どこへアクセスしたんでしょうか。

console.log(Object.getOwnPropertyNames(Taro))
// [ '_name', '_nation' ]
console.log(Object.getOwnPropertyNames(Person.prototype))
// [ 'constructor', 'greeting', 'name', 'nation' ]

やはりget/setPerson.prototypeに移行されました。

console.log(Object.getOwnPropertyDescriptors(Person.prototype))
// {
//   constructor: {
//     value: [class Person],
//       writable: true,
//         enumerable: false,
//           configurable: true
//   },
//   greeting: {
//     get: [Function: get greeting],
//       set: undefined,
//         enumerable: false,
//           configurable: true
//   },
//   name: {
//     get: undefined,
//       set: [Function: set name],
//         enumerable: false,
//           configurable: true
//   },
//   nation: {
//     get: undefined,
//       set: [Function: set nation],
//         enumerable: false,
//           configurable: true
//   }
// }

methods in class constructor

関数をclassconstructorで宣言したらどうなるでしょう。さきほどの例を使いってみたら、

class Person {
  constructor(name, nation) {
    this.name = name
    this.nation = nation
    this.test = function () {
      return `${this._name} ${this._nation}`
    }
  }
...
}

console.log(Object.getOwnPropertyNames(Taro))
// [ '_name', '_nation', 'test' ]
console.log(Object.getOwnPropertyNames(Person.prototype))
// [ 'constructor', 'greeting', 'name', 'nation' ]
console.log(Object.getOwnPropertyDescriptors(Taro))
// {
//   _name: {
//     value: 'Taro',
//     writable: true,
//     enumerable: true,
//     configurable: true
//   },
//   _nation: {
//     value: 'Japan',
//     writable: true,
//     enumerable: true,
//     configurable: true
//   },
//   test: {
//     value: [Function (anonymous)],
//     writable: true,
//     enumerable: true,
//     configurable: true
//   }
// }

constructor()で定義されたらPerson.prototypeに移行されず、そして一般の関数のように使えて、newでインスタンスを創ることもできます。

const test = new Taro.test()
console.log(test) // {}

さらにいろいろ試してみますと、

class Person {
  constructor(name, nation) {
    this.name = name
    this.nation = nation
  }
...
  test() {
    return `${this._name} ${this._nation}`
  }
}

console.log(Object.getOwnPropertyNames(Taro))
// [ '_name', '_nation' ]
console.log(Object.getOwnPropertyNames(Person.prototype))
// [ 'constructor', 'greeting', 'name', 'nation', 'test' ]
console.log(Taro.test()) // Taro Japan

さきの結論と同じ、関数test()Person.prototypeに移行。
しかしもし変数に入れられたら、

class Person {
...
...
  test = function test() {
    return `${this._name} ${this._nation}`
  }
}
console.log(Object.getOwnPropertyNames(Taro))
// [ 'test', '_name', '_nation' ]
console.log(Object.getOwnPropertyNames(Person.prototype))
// [ 'constructor', 'greeting', 'name', 'nation' ]
console.log(Taro.test()) // Taro Japan

まとめてみると、constructor()内部に宣言した関数や、classで変数で保存する関数はconstructor()に定義され、普通の関数のようにインスタンスができます。
class内部で書いた宣言不要?の関数(get/setも).prototypeに移行されて、再定義できないメソッドになります。

(その違いで何が起こるかまだわかりませんが、とりあえずここで記録として残します)

class vs. constructor function

ここからはclassとコンストラクタ関数の違いをまとめていきたいと思います。

methods is non-enumerable or enumerable in class

さきの例を再利用して検証してみると、

class Person {
  constructor(name, nation) {
    this.name = name
    this.nation = nation
    this.test = function () {
      return `${this._name} ${this._nation}`
    }
  }
...
}
for (let key in new Person('Taro', 'Japan')) {
  console.log(key)
}
// _name
// _nation
// test

//
class Person {
...
...
  test = function test() {
    return `${this._name} ${this._nation}`
  }
}

for (let key in new Person('Taro', 'Japan')) {
  console.log(key)
}
// test
// _name
// _nation
class Person {
...
...
  test() {
    return `${this._name} ${this._nation}`
  }
}

for (let key in new Person('Taro', 'Japan')) {
  console.log(key)
}
// _name
// _nation

constructor()で定義されたら ⇒ enumerable
classで定義されて.prototypeに移行 ⇒ non-enumerable
(そもそもPersonのなかにいなくなったから、列挙できないとは言えない気がする)

classes hoisted with uninitialized state

すべての宣言はhoisting状態になります、classも同じく。ただclassfunctionのようにいつでも呼び出されていいのでなく、
let/constと同じTDZuninitialized状態になるので、classより前の呼び出しはReferenceError投げてくるのです。

console.log(a)
// ReferenceError: Cannot access 'a' before initialization
class a {}

もう一度JavaScriptを理解する part1 -- let/const

なので、classvarfunctionのように再宣言することもできません。

class a { }
class a { }
// SyntaxError: Identifier 'a' has already been declared
class a { }
{
  class a { } // it is work
  class b { }
}

new b()
// ReferenceError: b is not defined

もちろん別のスコープであれば同じ名前でもいけました。
これも当たり前ですが外部から内部へのアクセスはダメで、内部から外部へのアクセスは大丈夫です。

class b { }
{
  new b()
}

block scope

const name = 'Taro'

class testScope {
  name = name
  nation = 'Japan'
  constructor(birthPlace) {
    this.birthPlace = birthPlace
  }

  scope() {
    return `${this.nation} ${this.birthPlace}`
  }
}

const test = new testScope('Tokyo')
console.log(test)
// testScope { name: 'Taro', nation: 'Japan', birthPlace: 'Tokyo' }
console.log(test.name) // Taro
console.log(test.scope()) // Japan Tokyo

function scopeと違って、classblock scopeなので、nameのように外部の変数へのアクセスは可能です。

下はfunction scopeの例です。

const name = 'Taro'
function testScope() {
  const name = name
  return name
}

console.log(testScope())
// ReferenceError: Cannot access 'name' before initialization
1
1
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
1
1