初めに
今回は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