JavaScript (ES5) でクラスを実現するための基本

  • 67
    いいね
  • 1
    コメント

前書き

ES5 までの内容となりますので注意してください。
プロトタイプベースオブジェクト指向を利用してクラスを実現します。
ES6 でコード書ければもっと幸せになると思います。

クラスとコンストラクタ

JavaScript では関数をクラスのコンストラクタのように利用することができます。

そのため関数を使ってクラスとコンストラクタを同時に定義します。

// Personクラスとコンストラクタの定義
var Person = function(name, age) {
    // メンバ変数 (インスタンス変数)
    this.name = name;
    this.age  = age;
}

インスタンス

インスタンスの生成

クラスのインスタンスの生成とコンストラクタの呼び出しには new 演算子を使います。

var Person = function(name, age) {
    this.name = name;
    this.age  = age;
}

// Personインスタンスを生成
var taro = new Person('太郎', 20);

console.log(taro.name); // '太郎'
console.log(taro.age);  // 20

インスタンスの生成においての注意点

thisのパターン

JavaScript の this は、呼び出し方法によって表すものが変化するので、注意が必要です。

呼び出し方法 thisが指すもの
関数 グローバルオブジェクト
コンストラクタ 生成するオブジェクト
メソッド 呼び出し元のオブジェクト
イベントリスナー イベントの発生元
call / apply 引数で指定したオブジェクト

javascript の this について理解できない方は下記を参照することをお勧めします。

JavaScriptの「this」は「4種類」?? - Qiita

new 演算子と一緒に関数呼び出し

var Person = function(name, age) {
    this.name = name;
    this.age  = age;
}

// new 演算子をつけて関数を呼び出し ( つまり、コンストラクタ呼び出し )
var taro = new Person('太郎', 20);

このように new と一緒に関数を呼び出すと、まず新しい空のオブジェクト (つまり {} ) が生成されます。

次に関数 ( コンストラクタ ) が呼び出されますが、その際に コンストラクタ呼び出し となり、 関数 ( コンストラクタ ) 内の this が生成されたオブジェクトを指す ようになります。

関数が実行された後、生成されたオブジェクトが new の実行結果として返されます。

new 演算子のつけ忘れによる関数呼び出し

var Person = function(name, age) {
    this.name = name;
    this.age  = age;
}

// new 演算子のつけ忘れ ( つまり、関数呼び出し )
var taro = Person('太郎', 20);

これは new 演算子のつけ忘れによる 関数呼び出し となり、 関数内の this がグローバルオブジェクトを指す ようになります。

つまりPerson関数内のnameとageが二つともグローバル変数として定義されることになります。

次の コンストラクタを new 演算子で呼び出すようにする をご覧下さい。

コンストラクタを new 演算子で呼び出すようにする

new 演算子のつけ忘れに対処するためには、 コンストラクタを new 演算子で呼び出すようにする必要があります

this が生成されたオブジェクトであるかどうかを調べ、そうでない場合には強制的に new 演算子でコンストラクタを再度呼び出しをおこなうことが必要になります。

var Person = function(name, age) {
    // thisがPersonのインスタンスでない際に、 new 演算子でコンストラクタを呼び出すようにする
    if(!(this instanceof Person)) {
        return new Person(name, age);
    }
    this.name = name;
    this.age  = age;
}

// コンストラクタ呼び出し
var constract = new Person('コンストラクタ太郎', 20);
console.log(constract instanceof Person); // true

// 関数呼び出し
var function = Person('関数太郎', 20);
console.log(constract instanceof Person); // true

メソッド

JavaScriptでオブジェクトにメソッドを定義する方法には次の2つがあります。

コンストラクタ関数で定義

var Person = function(name, age) {
    if(!(this instanceof Person)) {
        return new Person(name, age);
    }

    this.name = name;
    this.age  = age;

    // コンストラクタ内でメソッドを定義
    this.setName = function(name) {
        this.name = name;
    }
    this.getName = function() {
        return this.name;
    }
}

var taro = new Person('太郎', 20);

// コンストラクタ内のメソッド呼び出し
taro.setName('日本太郎');
console.log(taro.getName()); // 日本太郎

この方法の問題点は、インスタンスを生成するたびにコンストラクタ内で定義されたメソッドがコピーされてしまうことです。
つまり、 new Person () でインスタンスを生成するたびに、 setName() メソッド及び getName() メソッドもその数だけ作成されてしまいます。

一般的にメソッドは、どのインスタンスのものであるかにかかわらず、処理の内容は同じのはずです。

同じものをいちいちコピーして別々のメモリ領域を消費するのは無駄です。

次の プロトタイプで定義 をご覧下さい。

プロトタイプで定義

メソッドによるメモリー領域の消費を避けるために、JavaScriptでは、オブジェクトにメソッドを追加するための仕組みとして prototype というプロパティを用意しています。

prototype プロパティは最初、空のオブジェクトを参照しています。このオブジェクトをプロトタイプと呼びます。

プロトタイプで定義されたメソッドは、インスタンス化されたオブジェクトから暗黙的に参照されます。 インスタンスが持っているのは参照なので、無駄なメモリ領域の消費を防げます

var Person = function(name, age) {
    if(!(this instanceof Person)) {
        return new Person(name, age);
    }

    this.name = name;
    this.age  = age;
}

// プロトタイプ内でメソッドを定義
Person.prototype.setName = function(name) {
    this.name = name;
}
Person.prototype.getName = function() {
    return this.name;
}

var taro = new Person('太郎', 20);

// プロトタイプ内のメソッド呼び出し
taro.setName('日本太郎');
console.log(taro.getName()); // 日本太郎

こうすることで、インスタンス化されたPersonオブジェクトは、 Person.prototype.setName() メソッド及び Person.prototype.getName() メソッドを 参照 するので、無駄なメモリ領域の消費を防げます。

プロトタイプとクロージャで定義

処理内容としては、 プロトタイプで定義 と変わりはありませんが、よりまとまった綺麗なクラスを書きたい場合は下記のように書くことができます。

【javascript】やさしいクラスの作り方 - Qiita

var Person = (function() {
    // クラス内定数
    var COUNTRY = 'Japan';

    // コンストラクタ
    var Person = function(name, age) {
        if(!(this instanceof Person)) {
            return new Person(name, age);
        }

        this.name = name;
        this.age  = age;
    }

    var p = Person.prototype;

    // プロトタイプ内でメソッドを定義
    p.setName = function(name) {
        this.name = name;
    }
    p.getName = function() {
        return this.name;
    }

    return Person;
})();

var taro = new Person('太郎', 20);

// プロトタイプ内のメソッド呼び出し
taro.setName('日本太郎');
console.log(taro.getName()); // 日本太郎

カプセル化

JavaScriptでは、 private または protected なメソッドやプロパティという形が存在しません。

プロパティに設定したすべてのメンバは外部に公開されます。

メンバの中には外部からアクセス不可にしてほしいものもあります。

var Person = function(name, age) {
    if(!(this instanceof Person)) {
        return new Person(name, age);
    }

    this.name = name;
    this.age  = age;
}

Person.prototype.setName = function() {
    this.name = name;
}
Person.prototype.getName = function() {
    return this.name;
}

var taro = new Person('太郎', 20);

// 外部からメンバにアクセス可能となってしまう
console.log(taro.name); // 太郎

外部からメンバにアクセス不可とする場合、JavaScriptでカプセル化を実現する方法には次の2つがあります。

どちらもメリットデメリットがあり、使い分けは時と場合によるみたいです。

コーディング規約によるカプセル化

プライベート変数を無理に作成しないでコーディング規約のみ処理するのであれば、プライベートにしたい変数名に _ ( アンダースコア ) をつけます。
あくまで「アクセスしてほしくない」という意図を他のプログラマに伝えているだけで、 仕組みとしてアクセス不可なわけではありません

var Person = function(name, age) {
    if(!(this instanceof Person)) {
        return new Person(name, age);
    }

    // プライベート変数 ( プライベートにしたい変数 )
    this._name = name;
    // パブリック変数
    this.age  = age;
}

Person.prototype.setName = function() {
    this._name = name;
}
Person.prototype.getName = function() {
    return this._name;
}

var taro = new Person('太郎', 20);

// アクセス可能
console.log(taro.getName()); // 太郎
// アクセス可能
console.log(taro.age);       // 20
// 仕組みとしてアクセス不可なわけではないのでアクセス可能
console.log(taro._name);     // 太郎

ローカル変数によるカプセル化

プライベートにしたい変数をローカル変数で宣言することでカプセル化を実現します。

しかし、 コンストラクタ関数で定義 で説明した通り、プロトタイプでメソッドを定義していないため、無駄なメモリ領域の消費が発生します。

var Person = function(name, age) {
    if(!(this instanceof Person)) {
        return new Person(name, age);
    }

    // プライベート変数 ( メンバ )
    var private_name = name;
    this.public_age  = age;

    // コンストラクタ内でメソッドを定義
    this.setName = function(name) {
        private_name = name;
    }
    this.getName = function() {
        return private_name;
    }
}

var taro = new Person('太郎', 20);

// アクセス可能
console.log(taro.getName());    // 太郎
// アクセス可能
console.log(taro.public_age);   // 20
// アクセス不可
console.log(taro.private_name); // undefined

参考文献