カテゴリー : prototype

このカテゴリーの登録数:1件 表示 : 1 - 1 / 1

2007/12/17

javascriptを初めて学ぶ人についてのおさらい。その2

ポスト @ 8:16:56 , 修正 @ 2007/12/17 8:24:29 | , , ,     

前回のエントリが700users突入しました。ありがとうございます。参考になれば幸いです。

ということで、その2になります。
前回書いた通り、C/Javaについてはある程度の知識がある人なので、クラスなどのオブジェクト指向はちゃんと理解されているようですが、プロトタイプ指向は初めて学ぶようです。
javascript(ECMAScript)のプロトタイプは他のプロトタイプ指向言語とはひと味違う動作をするので、その点も含めておさらい

プロトタイプとはなんですか?プロトタイプとは継承パターンの一つでしかないです

プロトタイプは単なる継承パターンであり、単一の方向への継承しか行わない点についてはクラスベースと同じです。

var Hoge = function (){};
Hoge.prototype.methodA = function (){
    return "this is methodA";
};
var Foo = function (){};
Foo.prototype = new Hoge;

var foo = new Foo;
alert(foo.methodA()); // this is methodA
alert(foo instanceof Foo && foo instanceof Hoge); // true

また、上の記述は次のように書く事もできます

var Hoge = function (){
    this.methodA = function (){
        return 'this is methodA';
    };
    this.methodB = function (){
        return 'this is methodB';
    };
};

var Foo = function (){};
Foo.prototype = new Hoge;

var foo = new Foo;
alert(foo.methodA()); // this is methodA
alert(foo.methodB()); // this is methodB
alert(foo instanceof Hoge && foo instanceof Foo); // true

しかし、次のような記述を指定してしまうと、上記の動作とは違ってきます。

var Hoge = function (){
    this.methodA = function (){
        return 'this is methodA';
    };  
};
Hoge.prototype.methodB = function (){
    return 'this is methodB';
};

var Foo = function (){
    // apply
    Hoge.apply(this, arguments);
};

var foo = new Foo;
alert(foo.methodA()); // this is methodA
alert(foo.methodB); // undefined
alert(foo instanceof Hoge && foo instanceof Foo); // false

// Fooと同じ
var Bar = function (){};
Hoge.apply(Bar.prototype);

var bar = new Bar;
alert(bar.methodA()); // this is methodA
alert(bar.methodB); // undefined
alert(bar instanceof Hoge && bar instanceof Bar); // false

この違いが生まれることについては、後記

インスタンスにメソッドをつけてprototypeすることも可能です。特異メソッドのように

rubyの特異メソッド(以下)では、インスタンスにメソッドを付加することができます。

class Hoge
    def hello
        'hello'
    end
end

foo = Hoge.new

p foo.hello # hello

def Hoge.world
    'world'
end

p foo.respond_to?('world') #false

def foo.world2
    'world2'
end

p foo.respond_to?('world2') #true
p foo.world2 # world2

特異メソッドについては、javascriptでも実装可能です。また、特異メソッドをつけたインスタンスをprototypeすることが可能となっています。

var Hoge = function (){};
Hoge.prototype.hello = function (){
    return 'hello';
};

var foo = new Hoge;
alert(foo.hello()); // hello

foo.world = function (){
    return 'world';
};  
alert(foo.world()); // world

var Bar = function (){};
Bar.prototype = foo;

var bar = new Bar;
alert(bar.hello()); // hello
alert(bar.world()); // world

Javascriptのプロトタイプは、インスタンスベースにあらず。

上のインスタンスメソッドによる動きをサポートしている(?)おかげで、次のような記述が可能となり、プロトタイプ言語ならではな、ダイナミックなプログラミングが可能となります。

var Echo = {
    hello: function (){
        return 'hello';
    }
};

var Hoge = function (){};
Hoge.prototype = Echo;

var hoge = new Hoge;
alert(hoge.hello()); // hello

Echo.world = function (){
    return 'world';
};

alert(hoge.world()); // world

しかしながら、javascriptではクラスベースのインスタンスを生成するため、以下のように書いてしまう事で動作が不明になり、混乱してしまいます。

var Echo = function (){};
Echo.prototype = {
    hello: function (){
        return 'hello2';
    }
};

var Hoge = function (){};
Hoge.prototype = new Echo;

var hoge = new Hoge;
alert(hoge.hello()); // hello2

Echo.world = function (){
    return 'worl2';
};
alert(hoge.world); // undefined

Echo.prototype.world = function (){
    return 'world3';
};
alert(hoge.world()); // world3

上記undefinedになる原因は、Echoクラスに対してworldメソッドを追加したものの、既に生成済みインスタンスがEchoのインスタンスと違っているため、このような動きになります

javascriptのプロトタイプはインスタンスについてのプロトタイプではなく、prototype宣言されたものについてプロトタイプされる言語といえそうです。
このあたりの動きが、javascriptのプロトタイプが嫌われる原因なのかもしれません。(慣れない人は本当に慣れないですよね…)

また、prototypeを自由に切り替えれる点においてもなかなか慣れないみたいです(あまり有効な使い方は無いんですけどね)

var Hoge = function (){};
Hoge.prototype.a = function (){return 'a'};

var Foo = function (){};
Foo.prototype.b = function (){return 'b'};

var Bar = function (){};

Bar.prototype = new Hoge;
var bar = new Bar;
alert(bar.a); // a
alert(bar.b); // undefined

Bar.prototype = new Foo;
var bar = new Bar;
alert(bar.a); // undefined
alert(bar.b); // b

同一名プロパティ(slot)はプロトタイプできないのです

prototype言語のよいところは、未実装のメソッドについては、自動的に親のメソッドを探しに行ってくれる点にあると言えます。

var Hoge = function (){};
Hoge.prototype.methodA = function (){return 'a'};
var Foo = function (){}; 
Foo.prototype = new Hoge;
Foo.prototype.methodB = function (){return 'b'};
var Bar = function (){};
Bar.prototype = new Foo;
Foo.prototype.methodC = function (){return 'c'};

var bar = new Bar;
alert(bar.methodA()); // 'a'
alert(bar.methodB()); // 'b'
alert(bar.methodC()); // 'c'

しかしjavascriptでは、prototypeを上書きしてしまうので、同一プロトタイプでは親の実装を呼び出す事ができません。

var Hoge = function (){};
Hoge.prototype.methodA = function (){
    return 'this is Hoge#methodA';
};  

var Foo = function (){
    this.methodA = function (){
        return 'this is Foo#methodA';
    };
};
Foo.prototype = new Hoge;

var foo = new Foo;
alert(foo.methodA()); // this is Foo#methodA
alert(Foo.prototype.methodA()); // this is Hoge#methodA

ただし、prototypeを巡って呼び出す事もできます。

jsは3分の1の純情なインスタンスベース、クラスベース、プロトタイプベース。ならcloneがおすすめ。

ということで、複数のインスタンスを生成できてしまうjavascriptでは混乱を多く招いてしまいます。常に単一方向への継承をするように、以前書いたプロトタイプ継承のcloneを行うとすんなり理解できるのではないでしょうか

Object.prototype.clone = function (){
    var f = function (){};
    f.prototype = this;
    return new f;
};

var hoge = {}.clone();
hoge.a = function (){return 'a'};

var foo = hoge.clone();
foo.b = function (){return 'b'};

var bar = foo.clone();
alert(bar.a()); // a
alert(bar.b()); // b

hoge.c = function (){return 'c'};
alert(foo.c()); // c
alert(bar.c()); // c

継承ってなんですか?プロパティのコピーですか?

プロトタイプ指向言語においては、全てがインスタンスであり、インスタンスをcloneすることで、親オブジェクトの変更が子オブジェクトに対しても反映されます。

以下は、ioのコードになります

Echo := Object clone do(
    hello := method(
        "hello"
    )
)

hoge := Echo clone
hoge hello println // hello

Echo world := method(
    "world"
)

hoge world println // world

また、ioによく似た(というか、smalltalkによく似ている) Slate では次のように記述する事ができます(Slateもまたprototype指向の言語の一つです)

Slate 1> lobby addSlot: #Echo valued: Cloneable clone.
lobby
Slate 2> _@Echo hello [inform: 'hello'].
[hello]
Slate 3> lobby addSlot: #hoge valued: Echo clone.
lobby
Slate 4> hoge hello print.
hello
NilNil
Slate 5> _@Echo world [inform: 'world'].
[world]
Slate 6> hoge world print.
The method #world was not found for the following arguments:
{("Cloneable" )}
Nil
Slate 7> Echo removeSlot: #world.
("Cloneable" )
Slate 8> _@(Echo traits) world [inform: 'world'].
[world]
Slate 9> hoge world print.
world
NilNil
Slate 10> quit.

ioとslateは非常によく似ていますねえ。(slateはioと違いtraitsの概念がある分面倒になりますが)

で、jsのコード(using: prototype.js)

既に、記述のとおり、インスタンスに対してprototypeするのではなく、Class.createした関数のprototypeについて拡張を行います

var Echo = Class.create({
    hello: function (){
        return 'hello';
    }
});

var hoge = new Echo;
alert(hoge.hello()); // hello

Object.extend(Echo.prototype, {
    world: function (){
        return 'world';
    }
}); 
alert(hoge.world()); // world

ついでに、継承についてはprototype.jsのおかげで豊富に実装可能ですが、instanceofの結果は正しくない(?)みたいです。

var Hoge = Class.create({
    a: function (){return 'a'}
});
var Foo = Class.create({
    b: function (){return 'b'}
});
var Bar = Class.create({
    c: function (){return 'c'}
});

var Baz = function (){};
Object.extend(Baz.prototype, new Hoge);
Object.extend(Baz.prototype, new Foo);
Object.extend(Baz.prototype, new Bar);

var baz = new Baz;
alert(baz.a()); // a
alert(baz.b()); // b
alert(baz.c()); // c
alert(baz instanceof Hoge && baz instanceof Foo && baz instanceof Bar); // false

prototype.jsの1.6.0のコードでもObjext.extendは次のようになっているため、確かに、プロパティのコピーが行われているだけみたいです。(これも注意点?)

Object.extend = function(destination, source) {
  for (var property in source)
    destination[property] = source[property];
  return destination;
};

jsが持っているデフォルトオブジェクトにprototypeしよう

ということで、jsのデフォルトオブジェクトです。これをプロトタイプで拡張することができるので色々と楽しむ事ができます。が、度が過ぎる(Object.prototypeとかに設定すると)とhasOwnpropertyで何回も回ることになるのでほどほどに。

  • Object.prototype
  • String.prototype
  • Number.prototype
  • Boolean.prototype
  • Date.prototype
  • RegExp.prototype
  • Function.prototype
  • Array.prototype
  • Error.prototype

ref - Core JavaScript 1.5 Reference:Global Objects - MDC

おさらい。その2

ということで、長々と書きましたが、その2でした。
継承パターンとしてのプロトタイプと、その記述(実装)による動きの違いを身につけてもらえれば、結構便利にプロトタイププログラミングできるのではないでしょうか。
どちらかというと、クラス指向(?)の実装と比べ、プロトタイプ指向は馴染めない実装のようですが、仕組みが簡単なので理解してもらうと楽しいです。(学ぶことが多すぎるのが難点ですけどね)

その3へつづく(?)