Hatena::ブログ(Diary)

わからん

2011.10.30

[] 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"

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。

Google