Subscribed unsubscribe Subscribe Subscribe

枕を欹てて聴く

香炉峰の雪は簾を撥げて看る

Function.prototype.bindは何がいいのか

ES5からFunction.prototype.bindが入りました. これに準ずるものは数々のFrameworkで提供され続けてきたので, あまり馴染みの無さはないのではないでしょうか.
このFunction.ptototype.bindは実はとても面白いので.
結論から言えば, Function.prototype.bindはtarget functionのConstructに完全に移譲するのでConstructorの引数束縛が行えます. またConstructor callに対して配列を渡すapply形式のものといったapplyConstructorやcallConstructorをbindを使って実装することができます.

基本

Function.prototype.bindは基本的にはthisとargumentsに特定の値を束縛する関数です.
bindは第一引数にthis, 次からの引数が順番にarguments[0], arguments[1]に束縛されていきます.

function test(a, b) {
  print(a, b);
  print(this);
}
var target = {test: "TEST"};
var bound = test.bind(target, "1", "2");
bound();  // print("1", "2") and print(target)

例えば

// こんなのが
var some001 = {
  func: function() {
    var that = this;
    print(["a", "b", "c"].map(function(elm) {
      return that.treat(elm);
    }));
  },
  treat: function(str) {
    return str.toUpperCase();
  }
};

// こんなふうに
var some002 = {
  func: function() {
    print(["a", "b", "c"].map(this.treat.bind(this)));
  },
  treat: function(str) {
    return str.toUpperCase();
  }
};

上の例だとなんだろ? って感じですけど,

var some003 = {
  func: function() {
    document.addEventListener('click', this.clicked.bind(this), false);
  },
  clicked: function(ev) {
    // clicked function
    console.assert(this === some003);
  }
};

こういう感じにできるとなると嬉しいですね. EventListenerなどでいい感じに効果を発揮します.


あと, 引数束縛ですね.

function test(a, b) {
  print(a, b);
}

var bound = test.bind(null, "test");
bound("test2");  // print("test", "test2");

第一引数に必ずthisArgを要求されるけど, 普通に関数呼び出しの時みたいにしたくて, ただただ引数だけ束縛したいという場合は, nullを渡せばすみます. このときthisはstrict modeの時以外はGlobal Objectに束縛され, まんま普通に関数として呼び出したときと同じになりますね. (これはFunction.prototype.bindの機能ではなく関数呼び出しの時の仕様です)

応用

ここまでなら「へーなるほど」という感じなのですが, ここからが気になる感じで.
Function.prototype.bindはCall, Construct internal Functionを透過的にtarget functionに移譲します. これは, なかなか面白いことで.

function Test(arg1, arg2) {
  this.arg1 = arg1;
  this.arg2 = arg2;
}
Test.prototype.test = function Test_test() {
  print(this.arg1, this.arg2);
}

var test1 = new Test(0, 0);
test1.test();  // print(0, 0);

var BoundTest = Test.bind(null, 100);
var test2 = new BoundTest(200);
test2.test();  // print(100, 200);

このようにConstructor Testをbindして新たなBoundTestを生成しています. なるほど, 普通じゃないかと思った方はちょっと待ってください. BoundTestのtest関数はどこから来ているのですか? 本来そのまま考えれば, BoundTest.prototype.testになければおかしいはずです. しかしそんな物定義した覚えはありません.

実はこのようにConstruct, Callといった物が完全にtarget functionに移譲されるのがFunction.prototype.bindの特徴です. 上の場合, test2のPrototypeはTest.prototypeになっています. ちなみにBoundTest.prototypeはundefinedになります.

この面白さは自分でFunction.prototype.bindを実装するとわかります. 以下の実装は間違った例です.

Object.defineProperty(Function.prototype, 'myBind', {
  enumerable: false,
  configurable: true,
  writable: true,
  value: function myBind(thisArg) {
    var that = this;
    var args = Array.prototype.slice.call(arguments, 1);
    return function bound() {
      var a = args.concat(Array.prototype.slice.call(arguments));
      return that.apply(thisArg, a);
    }
  }
});

では, newしてみてください.

function Test(a, b) {
  this.a = a;
  this.b = b;
}
Test.prototype.test = function Test_test() {
  print(this.a, this.b);
}
var BoundTest = Test.myBind(null, 100);
new BoundTest(200).test();

はい, Errorになりましたね. 理由は簡単, なぜならBoundTest.prototype.testなんていう関数は存在しないからで, new BoundTestした結果のPrototypeがBoundTest.prototypeになっていることが原因です. しかし仕様上のFunction.prototype.bindはなんと, このPrototypeがtarget functionのprototyeが設定されるのです.

ではどういう事なのか, ぎりぎり仕様通りに実装しようと思えばこんな感じになります. 実は現行仕様では内部機能なしにFunction.prototype.bindを完全に実装することは不可能です.

Object.defineProperty(Function.prototype, 'myBind', {
  enumerable: false,
  configurable: true,
  writable: true,
  value: function myBind(thisArg) {
    var that = this;
    var args = Array.prototype.slice.call(arguments, 1);
    return function bound() {
      var a = args.concat(Array.prototype.slice.call(arguments));

      // [[Constructor]] callかどうかを見分ける必要がある.
      // しかし実際には不完全...
      if (Object.getPrototypeOf(this) !== bound.prototype) {
        return that.apply(thisArg, a);
      } else {
        // thisArgは無視される
        // ここも無理
        // なぜなら現行仕様はnew呼び出しの場合に引数として配列を与えるということができない
        // 苦し紛れー
        switch (a.length) {
          case 0:
            return new that();
          case 1:
            return new that(a[0]);
          case 2:
            return new that(a[0], a[1]);
          case 3:
            return new that(a[0], a[1], a[2]);
          case 4:
            return new that(a[0], a[1], a[2], a[3]);
          case 5:
            return new that(a[0], a[1], a[2], a[3], a[4]);
          case 6:
            return new that(a[0], a[1], a[2], a[3], a[4], a[5]);
          case 7:
            return new that(a[0], a[1], a[2], a[3], a[4], a[5], a[6]);
          case 8:
            return new that(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]);
          case 9:
            return new that(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]);
          case 10:
            return new that(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9]);
          default:
            throw new Error("myBind not support more than 10 length arguments as Constructor");
        }
      }
    }
  }
});

理由はnew Funcという呼び出し形式に対してapplyのようなことができないから, また, 本当にConstructor呼び出しかどうか (call(...)で紛らわしくできる. 本当にnew Funcという呼び出しなのか) が判断不可能であるということがあります.

深淵

先ほどConstructor callに対してapplyのようなことができないからmyBindは実装できないと言いました. ということは
Function.prototype.bindは内部でConstructor callに対してapplyのようなことをしている!!
という事実の裏返しです. これを利用すれば, applyのconstructor call version, applyConstructorや, callのConstructor version, callConstructorをつくることができます.

Object.defineProperties(Function.prototype, {
  'applyConstructor': {
    enumerable: false,
    configurable: true,
    writable: true,
    value: function applyConstructor(list) {
      var args = Array.prototype.slice.call(list);
      args.unshift(null); // dummy
      var Bound = this.bind.apply(this, args);
      return new Bound();
    }
  },
  'callConstructor': {
    enumerable: false,
    configurable: true,
    writable: true,
    value: function callConstructor() {
      var args = Array.prototype.slice.call(arguments);
      args.unshift(null);  // dummy
      var Bound = this.bind.apply(this, args);
      return new Bound();
    }
  }
});

これ, 今までだったらどうひっくり返っても実装できなかったのにお気づきでしょうか?

function Test(a, b) {
  this.a = a;
  this.b = b;
}
Test.prototype.test = function Test_test() {
  print(this.a, this.b);
}
new Test(10, 20).test();;
Test.callConstructor(10, 20).test();

これでcallConstructorみたいにできるので, Factoryが作れますね! しかも全部の引数を引き渡す形式の!!

function Test(a, b) {
  this.a = a;
  this.b = b;
}
Test.create = Test.callConstructor.bind(Test);

var test = Test.create(10, 20);
test.test();

これもいままでは出来なかったのにお気づきでしょうか? 少なくともargumentsをすべてひきわたすことはできなかったはずです.
このようにFactory生成も完全に自動化できます.

まとめ

というわけでFunction.prototype.bindは単なる簡単な追加機能とか補足みたいなのじゃなくて, 凄まじい新機能(call, applyに匹敵)で, かつ非常に奥が深いのでした.