読者です 読者をやめる 読者になる 読者になる

CoffeeScript の継承 extends を読んでみる

CoffeeScript の継承を読んでみる

CoffeeScript で次のように書くと

class Animal
  constructor: (@name) ->

  alive: ->
    false

class Parrot extends Animal
  constructor: ->
    super("Parrot")

  dead: ->
    not @alive()

次のようにコンパイルされる。(整形、コメントは入れてます)

(function () {
    var Animal, Parrot,
        __hasProp = {}.hasOwnProperty,
        __extends = function (child, parent) {
            for (var key in parent) {
                if (__hasProp.call(parent, key)) {
                    child[key] = parent[key];
                }
            }

            function ctor() {
                this.constructor = child;
            }

            // ctor の prototype に parent の prototype を代入。
            // オブジェクトの参照は同じ。
            ctor.prototype = parent.prototype;
            // サブクラスは ctor を継承。
            // ctor は prototype に parent の prototype を持つので
            // constructor は Animal だが、
            // new 演算子を使ったときに constructor を
            // child 自身(Parrot)に変更して、
            // これを child の prototype に代入。
            // ctor の prototype と parent の prototype の参照は同じだが、
            // new すると ctor の prototype のオブジェクトが
            // __proto__ にコピーされるような動きで同じ参照とはならず、
            // parent の constructor は変更されない。
            child.prototype = new ctor();
            child.__super__ = parent.prototype;
            return child;
        };

    Animal = (function () {

        function Animal(name) {
            this.name = name;
        }

        Animal.prototype.alive = function () {
            return false;
        };

        return Animal;

    })();

    Parrot = (function (_super) {

        __extends(Parrot, _super);

        function Parrot() {
            Parrot.__super__.constructor.call(this, "Parrot");
        }

        Parrot.prototype.dead = function () {
            return !this.alive();
        };

        return Parrot;

    })(Animal);

}).call(this);

このコードを理解するためのことをいくつか。

javascript の apply と call

このへんを参照。

javascript の constructor

このへん参照。
オブジェクトの初期化で使用されたコンストラクタ関数を参照する。

var a = new Array();
a.constructor == Array;//>trueになる

Array の constructor は?

function Array(){
    // Array の定義
}

↑と↓は、ほぼ同義です。(コンテキストが違うくらい?)

var Array = new Function("Array の定義");

こうすると Array の constructor は Function だとわかる。

※で、たぶんこれ↓もほぼおなじかな。function の宣言とはコンテキストが違うんだろうけれども。

var Array = function(){
    // Array の定義
};

※※ コンテキストが違うというのは、次のようになるということ。

hoge(); // 実行可能
function hoge(){};

fuga(); // 実行時エラー
var fuga = function fuga(){};

javascript の prototype

このへん参照。
関数が定義された時に自動的に生成される。初期値は constructor プロパティのみを持つオブジェクト。
prototype といえば、チェーンだったり継承という話にはなるんだけれども、そこは割愛しています。

javascript の prototype.constructor

このへん参照。
コンストラクタ関数自身を参照する。

で、ここでひとつ。次は実行するとエラーになります。

function Hoge(){
}
var hoge = new Hoge();
console.log(hoge.prototype.constructor == Hoge); // Error

これは、 prototype が関数が定義された時に自動的に生成されるものでり、変数 hoge は関数として定義されたものではないためだそうです。もう少しいうと、new 演算子を使ってオブジェクトを作成したタイミングで prototype は参照できなくなり、proto に prototype のオブジェクトはコピーされています。

これらより、つぎが理解できるはず。

hoge.constructor == Hoge.prototype.constructor; //>true

Hoge.constructor == Function.prototype.constructor; //>true

javascript の new 演算子

このへんを参照。
既存のオブジェクト (プロトタイプ) をベースに、新しいオブジェクトを生成するための演算子

javascript の 関数とメソッド

このへんを参照。 関数はオブジェクトであり、関数名は変数。

function Hoge(){
}

var Hoge = function(){}

var Hoge = new Function("");

が関数(/関数オブジェクト)。

メソッドは関数オブジェクトの参照が代入されているプロパティのこと。

var Hoge = function(){};
var hoge = new Hoge();
hoge.method = function(){console.log("execute method")}; // オブジェクトのプロパティに関数オブジェクトを代入。このプロパティがメソッド。
hoge.method(); // メソッドを実行。

javascript の this のスコープ

こことかここを参照。
ざっくりいうと、メソッドとして呼び出すとオブジェクト自身、関数として呼び出すとグローバルオブジェクトになる。例は次。

var obj = {
    func: function(){
        print(this);
    }
}
// 以下は等価
obj.func();
obj.func.call(obj);

↑これは、メソッドとして呼び出す場合。

var obj = {
    func: function(){
        var local = function(){
            print(this); // グローバルオブジェクトを参照することになる
        }
        local(); // ここで関数として実行することになり
    }
}
obj.func(); // まずはここでメソッドとして実行するが、、、

↑これは、関数として呼び出す場合。

あと、次の時、this はオブジェクト自身で、グローバルオブジェクトではないです。

function Hoge(){
    console.log(this);
    console.log(this.constructor);
}
var hoge = new Hoge();

コールバック関数のときは、関数として呼び出されるので、this はグローバルオブジェクトをさします。

function Hoge(){}
var hoge = new Hoge();
hoge.method = function(){
    console.log(this.constructor);
}
hoge.method(); // メソッドとして実行なので、this は hoge になる
setTimeout(hoge.method, 1); // 関数としてコールバックされるので、this は グローバルオブジェクトになる。

javascript の prototype の値

CoffeeScript で extends すると次のコードが出力されるが、このとき、parent.prototype を ctor.prototype に代入してそのあと ctor をnew したときに constructor の値を変更しているので、parent.prototype.constructor が変更されてるんじゃないかと思った。調べてみたけっかそんなことはなかったです。

function ctor() {
    this.constructor = child;
}
ctor.prototype = parent.prototype;
child.prototype = new ctor();

これはどういうことかというと、prototype の参照をもとにして new で新しいオブジェクトを作っているだけで、同じ参照が引き継がれているわけではないです。次のようになります。

function Hoge(){}
Hoge.prototype = {
    "a":"a",
    "b":"b"
}
var ttt = Hoge.prototype;
ttt.c = "c";
var hoge = new Hoge("xxx");
console.log(hoge.a); // a
console.log(hoge.b); // b
console.log(hoge.c); // c
console.log(hoge.prototype); // エラーになる
console.log(hoge.__proto__); // プロトタイプチェーンはこれを参照していくみたいですが、このプロパティが参照できることは保証されていないみたいです。