30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

クラス構文(ES2015)の基本

ES2015で追加された構文に、クラスがある。
これを用いることで、JavaScriptでもクラスを使うことが出来る。

ただ、これまでもnew演算子prototypeを活用することで、クラスのような機能を実現することが出来ていた。
そしてクラス構文は、こういった機能の糖衣構文であり、内部的な動作は今までと変わらない。
何か新しい機能が追加されたわけではなく、あくまでも、新しい記述方法が追加されたに過ぎない。

概要

クラスの記述方法には、クラス宣言とクラス式の2つがある。

// クラス宣言
class クラス名{
    クラスを定義
};

// クラス式
const クラス名 = class{
    クラスを定義
};

この2つがあるのは関数と似ているが、関数と違い、クラス宣言によって定義しても巻き上げは行われない。

クラスの定義部分には、メソッドを記述していく。従来とは異なる記法を用いるので、注意が必要。

class Person{
    sayHello(){
        console.log('Hello!');
    };
    sayBye(){
        console.log('Bye!');
    };
};

インスタンスの生成は、new演算子を使う。これは今までと同様。

class Person{
    sayHello(){
        console.log('Hello!');
    };
    sayBye(){
        console.log('Bye!');
    };
};

let tom = new Person();

tom.sayHello(); // Hello!
tom.sayBye();   // Bye!

クラスの定義部分には、メソッドしか記述できない。それ以外のものを記述するとエラーになる。

class Person{
    sayHello(){
        console.log('Hello!');
    };
    sayBye(){
        console.log('Bye!');
    };
    this.prop = 1;   // エラー
    console.log('hoge');   // これもエラー
};

では、プロパティはどのように定義すればいいのか。
constructorメソッドを使うことで解決する。
これは特殊なメソッドであり、new演算子インスタンスを生成する際に自動的に呼び出される。
各クラスに1つだけ定義することができ、複数定義してあるとエラーになる。

クラス定義部分のthisは、生成されるインスタンスを指すので、これを用いてプロパティを定義すればいい。

class Person{
    constructor(name, age){
        this.name = name;
        this.age = age;
        console.log('インスタンスを生成しました。');
    };
    sayHello(){
        console.log('Hello!');
    };
    sayBye(){
        console.log('Bye!');
    };
    sayName(){
        console.log(this.name);
    };
};

const tom = new Person('Tom', 30); // インスタンスを生成しました。
tom.sayName();  // Tom

constructorメソッドで定義したプロパティは、生成されるインスタンス自身が、それを保持する。
一方、constructor以外のメソッドは、インスタンス自身は保持せず、プロトタイプに格納される。
インスタンスは、プロトタイプチェーンによって、メソッドにアクセスする。

class Person{
    constructor(name, age){
        this.name = name;
        this.age = age;
        console.log('インスタンスを生成しました。');
    };
    sayHello(){
        console.log('Hello!');
    };
    sayBye(){
        console.log('Bye!');
    };
    sayName(){
        console.log(this.name);
    };
};

const tom = new Person('Tom', 30); // インスタンスを生成しました。
tom.sayName();  // Tom

// [ 'name', 'age' ]
console.log(Object.getOwnPropertyNames(tom));
// [ 'constructor', 'sayHello', 'sayBye', 'sayName' ]
console.log(Object.getOwnPropertyNames(Person.prototype));

なお、constructorのなかでメソッドを定義することも出来る。
プロトタイプに同名のメソッドがあった場合は当然、マスキングが行われる。

class Person{
    constructor(name, age){
        this.name = name;
        this.age = age;
        this.sayHi = () => { console.log('Hi!') };
        this.sayBye = () => { console.log('Good Bye!') };
    };
    sayHello(){
        console.log('Hello!');
    };
    sayBye(){
        console.log('Bye!');
    };
};

const tom = new Person('Tom', 30);

tom.sayHi();    // Hi!

tom.sayBye();   // Good Bye!
// インスタンス自身がsayByeを持つため、プロトタイプチェーンは発生しない
// つまり、'Bye!'と表示するほうのメソッドは呼ばれない

生成されるのはただのオブジェクト

先述のように、クラス構文は糖衣構文であり、何か新しい概念が追加されたわけではない。
そのため、生成されるインスタンスは、従来通りのただのオブジェクトである。
したがって例えば、生成後のプロパティの追加や削除も、問題なく出来る。

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

const tom = new Person('Tom', 30);

console.log(tom.job);   // undefined
tom.job = 'student';
console.log(tom.job);   // student
delete tom.job;
console.log(tom.job);   // undefined

もちろん、sealfreezeを行うことで、それを防止することも出来る。

デフォルト値

ES2015から、引数のデフォルト値を設定できるようになった。
クラス構文のなかでも使うことが出来る。

class Person{
    constructor(name='Bob', age='50'){
        this.name = name;
        this.age = age;
    };
    sayMessage(x='Hello!'){
        console.log(x);
    };
};

const tom = new Person('Tom', 30);
const bob = new Person();

console.log(tom.name, tom.age); // Tom 30
console.log(bob.name, bob.age); // Bob 50

tom.sayMessage('Hi');  // Hi
tom.sayMessage();   // Hello!

ゲッタとセッタ

ゲッタとセッタを設定することも出来る。
ゲッタとセッタそのものについては、以下を参照。
アクセサプロパティ(getterとsetter)

class Person{
    constructor(name){
        this._name = name;
    };
    get name(){
        return this._name;
    };
    set name(x){
        this._name = x;
    };
};

const tom = new Person('Tom');
console.log(tom.name);  // Tom
tom.name = 'Bob';
console.log(tom.name);  // Bob

静的メソッド

クラス構文を使えば、静的メソッドを簡単に定義できる。
静的メソッドとは、インスタンスを作ること無く、クラスから直接呼び出すメソッドのこと。
メソッド名の前にstaticと記述すると、それが静的メソッドとなる。

let personList = [];
class Person{
    constructor(name){
        this.name = name;
        personList.push(name);
    };
    static showPersonList(){
        console.log(personList);
    };
};

const tom = new Person('Tom');
const bob = new Person('Bob');
const john = new Person('John');
const taro = new Person('Taro');

// [ 'Tom', 'Bob', 'John', 'Taro' ]
Person.showPersonList();

// Person自身が、showPersonListメソッドを持っている
// [ 'length', 'name', 'prototype', 'showPersonList' ]
console.log(Object.getOwnPropertyNames(Person));

strictモードで動く

クラス構文の中は、特に宣言していなくても、strictモードとなる。
そのため、strictモードを使用していないプログラムにおいては、クラス構文の中だけがstrictモードになるので注意が必要。

strictモードの一例として、プロパティの操作がある。
削除できないプロパティを削除しようとした場合、通常のモードでは、削除は行われないがエラーにもならない。
だがstrictモードで同様のことを行うと、エラーが発生する。

let globalObj = {prop: 1};
Object.freeze(globalObj);

delete globalObj.prop;
console.log(globalObj.prop);  // 1
// 削除されないが、エラーにはならない。

class Test{
  deleteProp(){
    let obj = {prop: 1};
    Object.freeze(obj);
    delete obj.prop;
  };
};

let ins = new Test;
ins.deleteProp(); // TypeError: Cannot delete property 'prop' of #<Object>

// classの中は、strictモード

継承

クラス構文には、継承のための機能も用意されている。
詳しくは以下を参照。
クラス構文(ES2015)による継承とプロトタイプチェーン