組み込みのStringクラスにメソッドを追加してみる話です。ただし、String.prototypeには手を入れない方針。Stringクラスを“継承”したユーザー定義クラスを作ります。
継承の方法としては、__proto__プロパティを使います。__proto__プロパティの問題点は「継承についてもう少し」で指摘して、その対処法は「こんな継承はどう?」で述べました。今回は単にアイディアを述べるだけだし、FirefoxでもRhinoでも__proto__プロパティが使えるので、単純にSubclass.prototype.__proto__ = Superclass.prototype;
として継承します。
やってみる
StringのサブクラスであるEnhancedStringクラスでは、メソッド noripie() が追加されています。さらに、EnhancedStringクラスのサブクラスであるMoreEnhancedStringクラスでは、メソッドmammoth()が追加されています。メソッドに時事(?)ネタを使ったので、3ヶ月後には「それなに?」って話になっていそうですね(苦笑)。
/* === Stringのサブクラス EnhancedString === */ function EnhancedString(arg) { if (arg === undefined) arg = ""; // 引数なしの場合 if (arg instanceof String) { // argが、 // EnhancedString またはそのサブクラスのインスタンスであっても // ここで正しく処理される arg = arg.toString(); } // 実質的な処理 if (typeof arg === 'string') { this._str = arg; } else { throw new Error("bad arg"); } } EnhancedString.prototype.__proto__ = String.prototype; // EnhancedString独自のメソッド EnhancedString.prototype.toString = function () { return this._str; }; EnhancedString.prototype.noripie = function () { return new EnhancedString(this._str.replace(/しい/, "ピー")); }; /* === EnhancedStringのサブクラス MoreEnhancedString === */ function MoreEnhancedString(arg) { // スーパークラスのコントラクタを呼ぶ EnhancedString.apply(this, [arg]); }; MoreEnhancedString.prototype.__proto__ = EnhancedString.prototype; // MoreEnhancedString独自のメソッド MoreEnhancedString.prototype.mammoth = function () { return new MoreEnhancedString("マンモス" + this._str); };
なんかウマクないところ
当然ではありますが、文字列リテラルに対して "うれしい".noripie()
とかできなくて、new EnhancedString("うれしい").noripie()
とか、var es = new EnhancedString("うれしい"); s.noripie()
としなくちゃなりません。次のような便利関数を定義しておけば、キーボードからのタイプ量は節約できますが、それでもイマイチ。どうにもならんけどね。
/* === 便利関数 === */ function ES(arg) { return new EnhancedString(arg); } function MES(arg) { return new MoreEnhancedString(arg); }
いろいろ試した際の、Firebugとのセッションを引用しておきましょう。(最新のFirebugの表示では、「_str=おいピー、うれしい」だけではなく、「おいピー、うれしい _str=おいピー、うれしい」と表示されます。)
>>> var s = new String("おいしい、うれしい");
>>> var es = ES(s);
>>> var mes = MES(es);
>>> s.bold();
"<b>おいしい、うれしい</b>"
>>> s.noripie();
TypeError: s.noripie is not a function source=with(_FirebugCommandLine){s.noripie();\n};
>>> s.mammoth();
TypeError: s.mammoth is not a function source=with(_FirebugCommandLine){s.mammoth();\n};
>>> es.bold();
"<b>おいしい、うれしい</b>"
>>> es.noripie();
_str=おいピー、うれしい
>>> es.mammoth();
TypeError: es.mammoth is not a function source=with(_FirebugCommandLine){es.mammoth();\n};
>>> mes.bold();
"<b>おいしい、うれしい</b>"
>>> mes.noripie();
_str=おいピー、うれしい
>>> mes.mammoth();
_str=マンモスおいしい、うれしい
>>> es.noripie().noripie();
_str=おいピー、うれピー
>>> es.noripie().bold();
"<b>おいピー、うれしい</b>"
>>> es.bold().noripie();
TypeError: es.bold().noripie is not a function
>>> mes.mammoth().noripie()
_str=マンモスおいピー、うれしい
>>> mes.noripie().mammoth();
TypeError: mes.noripie().mammoth is not a function
>>>
esがEnhancedStringのインスタンスでも、es.bold() を実行すると、戻ってくるのはStringのインスタンスです。それで、es.bold().noripie();
は失敗してしまいます。次のようなメソッドbold()をEnhancedString側で定義しておけばOKです。が、こんなものをイチイチ定義するのはバカみたいです。
EnhancedString.prototype.bold = function () { return new EnhancedString(this._str.bold()); };
手作業の代わりをする関数inheritMethods()を定義してみました。([追記]下のソースコード内、「var m = names[i];」の1行が抜けていました。編集中に間違って消していたみたい。[/追記])
function inheritMethods(names) { var superproto = String.prototype; var subproto = EnhancedString.prototype; for (var i = 0; i < names.length; i++) { var m = names[i]; if (typeof superproto[m] === 'function') { subproto[m] = new Function("", "var r = String.prototype." + m + ".apply(this._str, arguments);return new EnhancedString(r);" ); } } }
対話例:
>>> var es = ES("www, うれしい");
>>> es.toUpperCase().noripie();
TypeError: es.toUpperCase().noripie is not a function
>>> inheritMethods(["toUpperCase", "toLowerCase"]);
>>> var es2 = ES("www, うれしい");
>>> es2.toUpperCase().noripie();
_str=WWW, うれピー
>>> es2.toUpperCase().toLowerCase().noripie()
_str=www, うれピー
>>>
inheritMethodsをEnhancedStringのコンストラクタクラス初期化処理に組み入れることはできますが、マンモス格好悪くて、ちっともうれピーじゃないなー。String、EnhancedString と露骨なクラス名をハードコードで書いてあるところは、関数定義ボディをテキスト処理で作るときのプレイスホルダーにすればパラメータ化できますが、さらにおぞましくなりますね。ウマクいかねー。