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

JavaScriptのprototypeについて

こんばんは、south37です。ちょっと前に宣言した通り、今日はjsネタでいこうと思います。
JavaScript-The-Good-Partsというとても評判のいい本を結構前に読んだんですが、読み返してみるとむちゃくちゃ忘れてて再び勉強になったので、読み直しながら考えた事とかをちょろっとまとめてみたいと思います。

jsにおけるprototype

この本はjsのprototypeに対する理解が深まるような内容になってます。そもそもprototypeってなんやねんって話ですが、jsにおけるオブジェクトのメソッド置き場かつ継承を実現する為のプロパティです。全てのオブジェクト(関数オブジェクトも)はprototypeプロパティを持っていて、prototypeプロパティに指定されたオブジェクトのメソッドやプロパティにアクセス出来るようになっています。
簡単な例として、次のコードを見てみましょう。(p34参照)

var Quo = function (string) {
  this.status = string;
};
Quo.prototype.get_status = function () {
  return this.status;
};
myQuo = new Quo("confused");
console.log(myQuo.get_status()); // 'confused'が出力される

jsでnew演算子を使ってオブジェクトを生成する際は、まずconstructorとなる関数を定義します。一応約束事としては、constructorの中でthis.~みたいな形で生成されるべきオブジェクトのプロパティを定義し、メソッドはprototypeプロパティに設定されたオブジェクトの中で定義する事になっています。ただ、jsはこのへんの流れに関して何の制約も課さないので、どんな関数だって(もともとconstructorとして使うつもりの無かったものでも)new演算子を作用させればオブジェクトが生成されちゃいます。ひどい仕様ですね。そもそも、関数にプロパティが設定出来るって仕様に違和感を覚えますし、javascript: the good parts的にはjsのnewは最悪の記法らしいですwwww

上の例ではprototypeプロパティのオブジェクトに必要なメソッドだけを設定していますが、ここでprototypeとして既存のオブジェクトを設定してやる事で、継承(っぽいもの)を実現する事が出来ます。ただし、メソッドだけでなく他のプロパティなんかも受け継がれちゃうんで、継承と呼んでいいかは微妙です。例としてまたコードを書いてみます。上で定義したmyQuoが存在する前提として、

var childQuo = function () {};
childQuo.prototype = myQuo;
var myChildQuo = new childQuo();

// この時点ではmyQuo.status = "confused"
console.log(myChildQuo.get_status()); // "confused"が出力される

myQuo.status = "busy";
// この時点ではmyQuo.status = "busy"
console.log(myChildQuo.get_status()); // "busy"が出力される

って感じになります。prototypeで指定したオブジェクトの状態の変化をそのまま反映しちゃうので、とっても微妙です。ポインタが透けて見えるというか、本当にただprototypeで指定したオブジェクトのプロパティを追いかけてるだけなんだなーって感じですね。

jsのオブジェクトってただのhashですし、jsっていろいろ荒い作りな感じがします。

prototypeの拡張

で、good partsではjsの機能を補うような形で、いろんな関数を定義しまくります。しかもそーやって定義した関数を次からガンガン使って説明をしていくので、途中からパラ読みしようとすると知らない関数が当然のように出てきてビビります。例えば、引数として渡したオブジェクトをprototypeとして持つようなオブジェクトを作りたければ、

Object.create = function (o) {
  var C = function () {};
  C.prototype = o;
  return new C();
};

のようにして定義したcreate関数を使えば良いです。使い方は、

var another_child = Object.create(child);

のような感じです。これで、childをprototypeとして持つanother_childオブジェクトが生成出来る訳です。

他にもガンガンいろんな関数を定義してたので、次回以降そのへんを説明していきたいと思います。

追記

javascriptのnew演算子を使うとして、一度construcotrを定義してからprototypeにメソッドを追加してくってやり方がどうも気持ち悪かったので、Class.defメソッドってのをちょろっと書いてみました。

Class = {};
Class.def = function (className, classBody) {
  this[className] = classBody.constructor;
  for (var methodName in classBody) {
    var method = classBody[methodName];
    if ( (typeof method === 'function') && (methodName !== 'constructor') ) {
      this[className].prototype[methodName] = method;
    }
  }
};

Class.def('Quo', {
  constructor: function (string) {
    this.status = string;
  },
  get_status:  function () {
    return this.status;
  }
});

myQuo = new Class.Quo('ok');
console.log(myQuo.get_status());

見たまんまの作りですが、Class.defメソッドにクラス名とクラスの中身(constructorやその他のメソッド)を定義してやるとClassオブジェクトの中に該当するコンストラクタが作られる感じになってます。普通のclass定義を再現してみたつもりです。あんま深く考えてないですが、coffee scriptとかで書けばそれっぽくなりそう...?