初めに
今回は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
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' ]
class
もconstructor function
もFunction
のインスタンスであり、プロトタイプチェーンも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
class
のget/set
はconstructor()
から返されたオブジェクトPerson { _name: 'Taro', _nation: 'Japan' }
のプロパティをアクセスして使用する。
しかしTaro
にはget
のgreetring
関数が持ってないのに、どこへアクセスしたんでしょうか。
console.log(Object.getOwnPropertyNames(Taro))
// [ '_name', '_nation' ]
console.log(Object.getOwnPropertyNames(Person.prototype))
// [ 'constructor', 'greeting', 'name', 'nation' ]
やはりget/set
もPerson.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
関数をclass
のconstructor
で宣言したらどうなるでしょう。さきほどの例を使いってみたら、
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
も同じく。ただclass
はfunction
のようにいつでも呼び出されていいのでなく、
let/const
と同じTDZ
にuninitialized
状態になるので、class
より前の呼び出しはReferenceError
投げてくるのです。
console.log(a)
// ReferenceError: Cannot access 'a' before initialization
class a {}
もう一度JavaScriptを理解する part1 -- let/const
なので、class
はvar
とfunction
のように再宣言することもできません。
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と違って、class
はblock scopeなので、name
のように外部の変数へのアクセスは可能です。
下はfunction scopeの例です。
const name = 'Taro'
function testScope() {
const name = name
return name
}
console.log(testScope())
// ReferenceError: Cannot access 'name' before initialization