プリミティブ値でもプロトタイプ的継承2006年10月18日 23時50分

書き上げた後に元記事の続きが出ているのに気づいたが、方向性が違うようなのでそのまま掲載。

404 Blog Not Found:javascript - プロトタイプ的継承 (元記事: Prototypal Inheritance) より。継承という言葉は意味が広いので、この操作に対してはチャイルドの作成といったほうが個人的にはわかりやすい。

さて、元記事で紹介されているコードではプリミティブ値からのチャイルドの作成 (継承) ができなかった。これはなぜかといえば、オブジェクト作成の際、プリミティブ値をプロトタイプ ([[Prototype]] 内部プロパティ、__proto__ プロパティ) に設定することはできないからである。

そこで、プリミティブ値が渡された場合は、それをラッパオブジェクトに変換することにする。といっても場合分けの必要はない。Object 関数を使えば、プリミティブ値が渡されたときは対応するラッパオブジェクトを、オブジェクトが渡されたときはそれをそのまま返してくれる。

function object(o) {
  function F() {}
  F.prototype = Object(o);
  return new F();
}

var myStr = object("子");
myStr.x = function (n) {
  var s = "";
  while (n--) s += this;
  return s;
};
print(myStr.x(12)); // doesn't work as expected

ところが、このままでは期待通りには動かない。JScript では空文字列が出力されるし、JavaScript (SpiderMonkey) では例外 (TypeError: String.prototype.valueOf called on incompatible Object) が投げられる。

その理由は、Kazuho@Cybozu Labs: JavaScript の String 型を継承するでも触れられているとおり、String#toString および String#valueOf が String 型以外のオブジェクト上では (そのオブジェクトのプロトタイプが String 型のオブジェクトであったとしても) 呼び出せないからだ。ECMAScript 仕様ではこのことを not generic (汎用的ではない) と表現している。

これは何も String 型に限った話ではない。Array#toString も汎用的ではなく、実際 JScript では以下のように文字列に変換しようとすると例外が投げられる。

String(object([1, 2, 3]));
// Microsoft JScript 実行時エラー: Array オブジェクトがありません。

対応策として考えられるのが、汎用的でないメソッドが呼び出されたときには処理をプロトタイプに委譲してしまうことだ。一部の実装では __proto__ プロパティからプロトタイプにアクセスできるので以下のように書ける。

var myStr = object("子");
myStr.valueOf = function () {
  return this.__proto__.valueOf();
};
myStr.x = function (n) { ... };
print(myStr.x(12)); // 子子子子子子子子子子子子

しかし、一般に ECMAScript ではオブジェクトのプロトタイプにアクセスする手段が提供されていない。そこで、チャイルドを作成する際に汎用的でないメソッドを上書きすることにする。

function object(o) {
  o = Object(o);
  function F() {}
  F.prototype = o;
  var obj = new F();
  var methods = ["toString", "toLocaleString", "valueOf"];
  for (var i = 0; i < methods.length; i++)
    with ({ method: methods[i] })
      obj[method] = function () { return o[method].apply(o, arguments); };
  return obj;
}

var myStr = object("子");
myStr.x = function (n) { ... };
print(myStr.x(12)); // 子子子子子子子子子子子子

これでプリミティブ値からもチャイルドを作成できるようになった。とはいえ、このままでは Number#toFixedDate#getTime などは使えないが、それらにも対応したものを prototypal.js として置いておく。

コメント

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

※投稿には管理者が設定した質問に答える必要があります。

名前:
メールアドレス:
URL:
次の質問に答えてください:
「ハイパーテキストマークアップ言語」をアルファベット4文字でいうと?

コメント:

トラックバック

このエントリのトラックバックURL: http://nanto.asablo.jp/blog/2006/10/18/566348/tb