[JavaScript]クラス、関数、オブジェクト

  • 120
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

オブジェクト

最も基本的なオブジェクト生成の例

オブジェクトリテラルを使った暗黙のオブジェクト生成
var obj = {'name': 'hoge'};
console.log(obj); // =>Object {name: "hoge"} 

あるいは

new演算子を使った明示的なオブジェクト生成
var obj = new Object({'name': 'hoge'});
console.log(obj); // =>Object {name: "hoge"} 

このようにして作られたオブジェクトはObject.prototypeを継承する

継承されたオブジェクトはconstructor.prototypeに格納される。

Object.prototype.test = 'test!';
console.log(obj); // =>Object {name: "hoge", test: "test!"} 

/* ECMAScript 3 */
console.log(obj.constructor.prototype); // =>Object {test: "test!"} 
/* ECMAScript 5 */
console.log(Object.getPrototypeOf(obj)); // =>Object {test: "test!"} 

オブジェクトの継承

objオブジェクトを継承してみる。
継承にはObject.create()を使う。

var o2 = Object.create(obj);
o2.age = 17;
console.log(o2); // =>Object {age: 17, name: "hoge", test: "test!"} 
console.log(Object.getPrototypeOf(o2)); // =>Object {name: "hoge", test: "test!"} 

o2オブジェクトの独自プロパティ、継承したプロパティが全て列挙されていることがわかる。

しかしconstructor.prototypeを見ると以下のようになっている。

console.log(o2.constructor.prototype);  // =>Object {test: "test!"} 
  • ECMAScript5ではObject.create()を用いてオブジェクトを継承できるが、
  • この場合、constructor.prototypeには常にObject.prototypeが入る。
  • 正しくプロトタイプを取得するには、Object.getPrototypeOf()を用いる。

_proto_プロパティ

  • 実際には処理系はconstructor.prototypeではなく、内部パラメータでプロトタイプチェーンを管理している。
  • FirefoxやCromeではこの内部プロパティを_proto_の名前で取得できる
  • _proto_は強い処理系依存なので注意(できる限りObject.getPrototypeOf()を使用すること)
console.log(o2.__proto__);

要点は以下の3つです. prototype と _proto_ は別物 いわゆる "プロトタイプチェーン" は _proto_ プロパティで実現されている オブジェクトを new するとき, コンストラクタ関数の prototype プロパティが指しているオブジェクトが, 生成されるオブジェクトの _proto_ に代入される
via prototype と _proto_ - フリーフォーム フリークアウト

ちなみにこの _proto_ というプロパティへのアクセスは標準ではく, 実装依存になるので注意が必要です. 開発者がアクセスできないかもしれないというだけで, プロトタイプの動作の理解には問題ありません
via prototype と _proto_ - フリーフォーム フリークアウト

関数

関数の生成/定義/宣言

関数定義式で暗黙に関数オブジェクトを生成
var func = function(x, y) {
    return x + y;
};
関数宣言で暗黙に関数オブジェクトを生成
function func(x, y) {
    return x + y;
}
関数オブジェクトを明示的に生成
var func = new Function('x', 'y', 'return x + y;');

関数はオブジェクト

  • 関数はオブジェクトなので、プロパティやメソッドを持つことができる。
  • 関数のプロパティ/メソッドにアクセスするには関数名を使う
func.count = 0; // 関数のプロパティを初期化
function func() {
    func.count ++;
    return func.count;
}

console.log(func()); // =>1
console.log(func()); // =>2

関数オブジェクトはFunction.prototypeを継承する

console.log(Function.prototype === Object.getPrototypeOf(func)); // =>true 

Function.prototypeにはconstructorプロパティが格納されている

console.log(Function.prototype.constructor); // =>function Function() { [native code] } 

console.log(func.constructor === Function.prototype.constructor); //=> true
/*上の比較はこれと同じこと */
console.log(Function === Function); // =>true

関数名は変数

  • 関数宣言文における関数名は変数として扱われる
  • この変数には関数オブジェクトが代入されている
  • 変数の多重定義はTypeErrorとはならない(新しいものに置き換わり古いものは隠蔽される)
"use strict";

function func() {
    return 'func!';
}
function func() { // 変数の再定義。
    return 'func2!';
}
var func = 123; // 変数の再定義。
var func = 234; // 変数の多重定義はTypeErrorとはならない
console.log(func); // =>234

クラス

基本的なクラスの定義

// コンストラクタ関数
function Human(param) {
    // 独自プロパティの定義(インスタンス間で変更を共有しない)
    this.firstname = param.firstname;
    this.lastname = param.lastname;
    this.age = param.age;
    this.sex = param.sex;

    // 独自メソッドの定義(インスタンス間で変更を共有しない)
    this.getMySecrets = function() {
        return 'none';
    };
};

// 継承プロパティの定義(インスタンス間で変更を共有する)
Human.prototype.eyes = 2;

// 継承メソッドの定義(インスタンス間で変更を共有する)
Human.prototype.getFullName = function() {
    return this.firstname + ' ' +  this.lastname;
};
Human.prototype.birthday = function() {
    this.age ++;
    return 'Happy Birthday!';
};

コンストラクタ関数はprototypeプロパティを持つ

console.log(Human.prototype);
  • クラスのインスタンスの生成は、コンストラクタとして実装した関数をnew演算子で呼び出すことで行います。
  • new演算子は、コンストラクタ関数のプロトタイプを継承したオブジェクトを返します。
var hanako = new Human({
    firstname: 'Hanako',
    lastname: 'Yamada',
    age: 17,
    sex: 'woman'
});

var ichiro = new Human({
    firstname: 'Ichiro',
    lastname: 'Tanaka',
    age: 23,
    sex: 'men'
});

console.log(hanako); // => Human {firstname: "Hanako", lastname: "Yamada", age: 17, sex: "woman", getMySecrets: function…}

/* Human.prototypeを継承している */
console.log(Human.prototype === Object.getPrototypeOf(hanako)); // =>true

ichiro.birthday();
console.log(ichiro); // =>Human {firstname: "Ichiro", lastname: "Tanaka", age: 24, sex: "men", getMySecrets: function…}

/* 独自プロパティ/メソッドは共有しない */
hanako.getMySecrets = function() {
    return 'I love ichiro!';
};
console.log(hanako.getMySecrets()); // =>I love ichiro! 
console.log(ichiro.getMySecrets()); // =>none 

/* 関数オブジェクトのprototype.constructorはコンストラクタ関数 */
console.log(Human.prototype.constructor === Human); // =>true

クラスの継承

クラスの継承は以下の手順で行う。

  • サブクラスのコンストラクタ関数を定義する。
  • コンストラクタ内では必要に応じて親クラスのコンストラクタを呼び出す
  • Object.create()で親クラスのprototypeを継承する
  • prototype.constructorをセットする
  • prototype.constructorを列挙不可にする
// サブクラスの定義
function Japanese(param) {
    this.lang = 'ja';
    Human.call(this, param); // thisとして親クラスのconstructorを呼び出し
};

// prototypeの継承
Japanese.prototype = Object.create(Human.prototype, {
    constructor: {
      value: Japanese, // prototype.constructor
      enumerable: false, // prototype.constructorは列挙不可
      writable: true,
      configurable: true
    }
});

var hanako = new Japanese({
    firstname: 'Hanako',
    lastname: 'Yamada',
    age: 17,
    sex: 'woman'
});

console.log(hanako); // =>Japanese {lang: "ja", firstname: "Hanako", lastname: "Yamada", age: 17, sex: "woman"…}

多重継承時のprototype調査

function Class1() {}
function Class2() {}
function Class3() {}

Class2.prototype = Object.create(Class1.prototype, {
    constructor: {
      value: Class2,
      enumerable: false,
      writable: true,
      configurable: true
    }
});
Class3.prototype = Object.create(Class2.prototype, {
    constructor: {
      value: Class3,
      enumerable: false,
      writable: true,
      configurable: true
    }
});

Class1.prototype.z = 100;
Class2.prototype.x = 12;
Class3.prototype.y = 50;

console.log(Class1.prototype); // =>Class1 {z: 100} 
console.log(Class2.prototype); // =>Class2 {x: 12, z: 100}
console.log(Class3.prototype); // =>Class3 {y: 50, x: 12, z: 100} 

クラスの静的プロパティ/メソッド

クラスの静的プロパティ、メソッドは以下のように実装できます。
これは実際には単なる(コンストラクタ)関数オブジェクトのプロパティ/メソッドです。

/* 人のヘルスチェックを行う関数 */
Human.check = function(hum) {
    return 2 === hum.eyes; // 目がちゃんと2個であれば正常
};
console.log(Human.check(ichiro)); // =>true

Human.BASE_PERSON = 'eve'; // 定数として扱う

カプセル化

  • JavaScriptにはprivateやpublicに類する仕組みがない
  • クロージャを使うことで関数内に変数を閉じ込められるのでそれっぽく実装できる
var Counter = (function() {

    // コンストラクタ関数
    function Counter() {
        // 公開プロパティ
        this.baseCount = 10;
    }

    // 公開メソッド。処理をプライベートメソッドへ移譲している
    Counter.prototype.add = function() {
        addCount();
    };
    Counter.prototype.get = function() {
        return getCount.apply(this);
    };

    // プライベートプロパティ※インスタンス間で共通
    var count = 0;

    // プライベートメソッド
    function addCount() {
        count ++;
    }
    function getCount() {
        return this.baseCount + count;
    }

    return Counter;
})();

var counter = new Counter();
counter.add();
console.log(counter.get()); // =>11

// countが共有されるので、複数のインスタンスを持つ場合は期待通りの動作とはならない
var counter2 = new Counter();
counter2.add();
console.log(counter2.get()); // =>12
counter.add();
console.log(counter.get()); // =>13

あるいは

// paramはプライベート変数
function Human(param) {
    // プライベート変数へのアクセサメソッド
    function getName() {
        return param.name;
    };
    function setName(name) {
        param.name = name;
    };
    function getAge() {
        return param.age;
    };
    function setAge(age) {
        param.age = age - 2;
    };

    // Getter/Setterを指定して、再定義を不可に
    Object.defineProperties(this, {
        'name': {get: getName, set: setName, configuration: false},
        'age': {get: getAge, set: setAge, configuration: false},
    });
};

var hanako = new Human({
    name: 'Hanako Yamada',
    age: 17,
});
hanako.age = 20;
console.log(hanako.age); //=>18

抽象化

// 抽象クラス
function abstructMobileSuite() {};
abstructMobileSuite.prototype.wakeUp = function() {
    return this.getName() + " stand on earth.";
}
// 抽象メソッド
abstructMobileSuite.prototype.getName = function() {
    // 再定義せずに呼び出すとエラーにする
    throw new Error("Can't instatiate abstruct classes.");
};

// 具象サブクラス
function Gundom() {}
// 継承
Gundom.prototype = Object.create(abstructMobileSuite.prototype);
Gundom.prototype.constructor = Gundom;
// 具象メソッドの定義
Gundom.prototype.getName = function() {
    return "Gundam";
}

var gundom = new Gundom;
console.log(gundom); // =>Gundom {constructor: function, getName: function, wakeUp: function}

console.log(gundom.wakeUp());  // =>Gundam stand on earth. 

動的なクラスの生成

サイ本に載ってた列挙型のクラスを動的に生成して返すファクトリの例です。

/* 列挙型クラスを返すファクトリ関数 */
function enumFactory(namesToValues) {
    // ダミーのコンストラクタ関数
    var Enumeration = function() {
        throw new Error("Can't Enum.");
    }

    // 列挙型の値のprototypeとなるオブジェクト
    Enumeration.prototype = {
        constructor: Enumeration,
        toString: function() {
            return this.name;  
        },
        valueOf: function() {
            return this.value;
        },
        toJSON: function() {
            return this.name;
        }
    };

    // 列挙型の値を格納
    Enumeration.values = [];

    // namesToValuesに指定された値ごとにオブジェクトを生成して格納する
    for (name in namesToValues) {
        var e = Object.create(Enumeration.prototype, {
            constructor: {
                 value: Enumeration,
                 enumerable: false,
                 writable: true,
                 configurable: true
            },
             toString: {
                 value: Enumeration,
                 enumerable: false,
                 writable: true,
                 configurable: true
            },
             valueOf: {
                 value: Enumeration,
                 enumerable: false,
                 writable: true,
                 configurable: true
            },
             toJSON: {
                 value: Enumeration,
                 enumerable: false,
                 writable: true,
                 configurable: true
            },
        });

        e.name = name;
        e.value = namesToValues[name];
        Enumeration[name] = e;
        Enumeration.values.push(e);
    }

    // クラスのインスタンスを巡回する静的メソッド
    Enumeration.foreach = function(f, c) {
        for (var i = 0; i < this.values.length; i ++) {
            f,call(c, this.values[i]);
        }
    };
    // コンストラクタ関数を返却
    return Enumeration;
}

var Coin = enumFactory({Penny: 1, Nickel: 5, Dime: 10, Quarter: 25});
console.log(Coin.Dime);  // =>Object {name: "Dime", value: 10} 

prototypeとconstructorの関係まとめ

(1) コンストラクタ関数はprototypeプロパティを持つ
(2) オブジェクトはconstructorプロパティでコンストラクタ関数を参照する
(3) 故にオブジェクトはprototypeプロパティを持つ(参照する)

function Human() {} // コンストラクタ関数(オブジェクト)
var human = new Human; // インスタンスオブジェクト

console.log(!!Human.prototype); // =>true (1)
console.log(Human === human.constructor); // =>true (2)
console.log(Function === Human.constructor); // =>true (2)
console.log(Human.prototype === human.constructor.prototype); // =>true (3)

参考URL

JavaScriptの継承について
[JavaScript] そんな継承はイヤだ - クラス定義 - オブジェクト作成 - Qiita
Taso.compute(more);: Object.createを使ってはいけない
Taso.compute(more);: Object.createを使ってはいけないの続き
wise9 › enchant.jsのクラス継承とJavaScriptのクラス継承のちょっとした違い