2011.10.30
■[JS] JavaScript での Mixin の書き方
JavaScript で Mixins がどう書けるのかを、A fresh look at JavaScript Mixins « JavaScript, JavaScript… のサンプルコードを流用して説明します。各アプローチのパフォーマンス比較など、ここでの主題でないことは元記事を参照して下さい。
プロトタイプをコピーするための関数 extend を用いたミックスイン
以下は RoundButton に、Button と Circle の両方の機能を追加、つまりミックスインさせる例です。extend(arg1, arg2) という関数を定義しています。第1引数に機能を追加したいオブジェクトのプロトタイプを渡せば、そのプロパティとして、第2引数のオブジェクトのプロパティがコピーされる、という仕組みです。
var RoundButton = function(radius, label) { this.radius = radius; this.label = label; }; var Button = { shrink: function() { this.radius--; } }; var Circle = { grow: function() { this.radius++; } }; function extend(destination, source) { for (var k in source) { if (source.hasOwnProperty(k)) { destination[k] = source[k]; } } return destination; } extend(RoundButton.prototype, Circle); extend(RoundButton.prototype, Button); // 検証 var button1 = new RoundButton(4, 'hoge'); console.log(button1.label); //=> "hoge" console.log(button1.radius); //=> 4 button1.grow(); console.log(button1.radius); //=> 5 button1.shrink(); console.log(button1.radius); //=> 4
もっと関数型ふうの、call() を用いたミックスイン
RoundButton に、asButton と asCircle の両方の機能を追加、つまりミックスインさせる例です。asButton, asCircle のメンバは this.xxx という形式で定義されています。この this が、asButton.call(RoundButton.prototype); といった call() を用いた呼び出しにより、RoundButton.prototype を指すことになり、RoundButton のプロトタイプに asButton と asCircle のメンバが設定されるという仕組みです。
var RoundButton = function(radius, label, action) { this.radius = radius; this.label = label; this.action = action; }; var asButton = function() { this.fire = function() { return this.action(); }; return this; }; var asCircle = function() { this.area = function() { return Math.PI * this.radius * this.radius; }; this.grow = function() { this.radius++; }; this.shrink = function() { this.radius--; }; return this; }; asButton.call(RoundButton.prototype); asCircle.call(RoundButton.prototype); // 検証 var button1 = new RoundButton(4, 'yes!', function() {return 'RoundButton のサンプルアクション'}); console.log(button1.area()); //=> 50.26548245743669 button1.grow(); console.log(button1.radius); //=> 5 button1.fire(); //=> "RoundButton のサンプルアクション"
call() を使ったミックスインではオプションを追加できる
call() を使ったミックスインの場合、追加機能を提供する側のオブジェクトにパラメータを設定しておいて、ミックスインする時点でその値を設定することができます。以下の例では、OvalButton に対し、asButton と asOval が持つ機能を追加しています。そのさい、asOval では、call() 時に設定したい値を渡しています。
var OvalButton = function(longRadius, shortRadius, label, action) { this.longRadius = longRadius; this.shortRadius = shortRadius; this.label = label; this.action = action; }; var asButton = function() { this.fire = function() { return this.action(); }; return this; }; var asOval = function(options) { // options に注目。上の例にはなかった this.ratio = function() { return this.longRadius/this.shortRadius; }; this.grow = function() { this.shortRadius += (options.growBy/this.ratio()); this.longRadius += options.growBy; }; return this; } asButton.call(OvalButton.prototype); asOval.call(OvalButton.prototype, {growBy: 2, shrinkBy: 2}); //オプションの値を指定 // 検証 var button2 = new OvalButton(3, 2, 'send', function() {return 'message sent'}); console.log(button2.shortRadius); //=> 2 console.log(button2.longRadius); //=> 3 button2.grow(); console.log(button2.shortRadius); //=> 3.333333333333333 console.log(button2.longRadius); //=> 5 button2.fire(); //=> "message sent"
関数定義をキャッシュする
これまでの call() を用いたミックスインでは、呼び出しのたびに関数が再定義されていました。この問題は、初期化処理のための即時関数を使うことで解決できます。
▼ 関数を返す即時関数の例
var getResult = (function(){ var res = 1 + 2; //この行は一度きりの作業 return function(){ return res; }; }());
今回の場合は、関数を返す即時関数を導入することで、一度かぎりの処理として内部関数の定義を書き、それをパブリックなメンバに詰める処理を行う関数を返すのです。
var RectangularButton = function(length, width, label, action) { this.length = length; this.width = width; this.label = label; this.action = action; } var asRectangle = (function() { function area() { return this.length * this.width; } function grow() { this.length++, this.width++; } return function() { this.area = area; this.grow = grow; return this; }; })(); var asButton = (function() { // this.fire = function() { // return this.action(); // }; // return this; var fire = function () { return this.action(); }; return function() { this.fire = fire; return this; }; })(); asButton.call(RectangularButton.prototype); asRectangle.call(RectangularButton.prototype); // 検証 var button3 = new RectangularButton(4, 2, 'delete', function() {return 'deleted'}); console.log(button3.area()); //=> 8 button3.grow(); console.log(button3.area()); //=> 15 button3.fire(); //=> "deleted"
カリー関数を導入して上の方法を仕上げる
上の関数定義をキャッシュするアプローチでは、ミックスイン時にオプション指定ができなくなりました。curry を導入することで、この問題を解決できます。"this.関数名 = プライベート関数" という形式で、左辺で作成される関数に、すでに設定していた引数を渡した効果を得るのに、カリー化を使うわけです。
Function.prototype.curry = function() { var fn = this; var args = [].slice.call(arguments, 0); return function() { return fn.apply(this, args.concat([].slice.call(arguments, 0))); }; }; var RectangularButton = function(length, width, label, action) { this.length = length; this.width = width; this.label = label; this.action = action; }; var asButton = (function() { var fire = function () { return this.action(); }; return function() { this.fire = fire; return this; }; })(); var asRectangle = (function() { function area() { return this.length * this.width; } function grow(growBy) { this.length += growBy, this.width +=growBy; } return function(options) { this.area = area; this.grow = grow.curry(options['growBy']); // this.grow は options['growBy'] を保持している return this; }; })(); asButton.call(RectangularButton.prototype); asRectangle.call(RectangularButton.prototype, {growBy: 2, shrinkBy: 2}); // 検証 var button4 = new RectangularButton(2, 1, 'add', function() {return 'added'}); console.log(button4.area()); //=> 2 button4.grow(); console.log(button4.area()); //=> 12 button4.fire(); //=> "added"
