+1。
Function.prototype.bindは何がいいのか - 枕を欹てて聴くというわけでFunction.prototype.bindは単なる簡単な追加機能とか補足みたいなのじゃなくて, 凄まじい新機能(call, applyに匹敵)で, かつ非常に奥が深いのでした.
なのにSafariとiOSとAndroidでサポートしてないなんて。あんまりだよ、こんなのってないよ。
Prototype.jsにあった、ような…
Prototype.jsのころはこんな感じでした。
var oBind = Function.prototype.bind; /* preserve the original */
Function.prototype.bind = function bind(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);
}
};
try{
/* works fine */
function log(b, x){ return Math.log(x)/Math.log(b) };
var log10 = log.bind(null, 10);
p(log10(1e6));
/* but this does not */
function Test(a, b) {
this.a = a;
this.b = b;
};
Test.prototype.test = function Test_test() {
p([this.a, this.b]);
};
var BoundTest = Test.bind(null, 100);
new BoundTest(200).test();
}catch(e){
p(e);
}
oBind ? Function.prototype.bind = oBind : delete Function.prototype.bind;
ECMAScript 5のFunction.prototype.bindとPrototype.jsのそれの最大の違いは、bindされた関数をコンストラクターとして使えるかどうかです。
もうnewも恐くない
PolyfillといえばMDN。実に面白い方法で解決しています。なるほどね。そうやってprototypeをつないでいるのか。
しかしこの方法も、Dateのような変態コンストラクターの前には無力です。
var oBind = Function.prototype.bind; /* preserve the original */
Function.prototype.bind = function(oThis){
if (typeof this !== "function") throw new TypeError(
'Function.prototype.bind cannot bind type ' + typeof(this)
);
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(
this instanceof fNOP ? this : oThis || window,
aArgs.concat(Array.prototype.slice.call(arguments))
);
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
try{
/* works fine */
function log(b, x){ return Math.log(x)/Math.log(b) };
var log10 = log.bind(null, 10);
p(log10(1e6));
/* now works */
function Test(a, b) {
this.a = a;
this.b = b;
};
Test.prototype.test = function Test_test() {
p([this.a, this.b]);
};
var BoundTest = Test.bind(null, 100);
new BoundTest(200).test();
/* works, too! */
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return this.x + "," + this.y;
};
var pt = new Point(1, 2);
p(pt.toString()); /* "1,2" */
var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0 /* x */);
var axisPoint = new YAxisPoint(5);
p(axisPoint.toString()); /* "0,5" */
p(axisPoint instanceof Point); /* true */
p(axisPoint instanceof YAxisPoint); /* true */
p(new Point(17, 42) instanceof YAxisPoint); /* false */
/* but the following does not */
Function.prototype.construct = function(aArgs) {
if (aArgs.constructor !== Array)
throw new TypeError("second argument to Function.prototype.construct must be an array");
var aBoundArgs = Array.prototype.concat.apply([null], aArgs),
fBound = this.bind.apply(this, aBoundArgs);
return new fBound();
};
var aDateArgs = "2011-7-16 19:35:46".split(/[- :]/),
oMyDate1 = new Date(
aDateArgs[0], aDateArgs[1], aDateArgs[2], aDateArgs[3], aDateArgs[4], aDateArgs[5]
);
p(oMyDate1.toLocaleString());
var oMyDate2 = Date.construct("2011-7-16 19:35:46".split(/[- :]/));
p(oMyDate2.toLocaleString());
p(Point.construct([2, 4]).toString()); /* "2,4" */
}catch(e){
p(e);
}
oBind ? Function.prototype.bind = oBind : delete Function.prototype.bind;
evalも、魔法も、あるんだよ
諦めたらそれまでだ。でも、JavaScripterなら実装を変え(ry
というわけで行き着いたのがこの方法。元の関数でnewした後、__proto__を使って無理やりプロトタイプを繋ぎ変えています。よってIEでは動きません。でも元々の動機はSafariで動かすことにあったので、これでもいいかなって。
eval()の使い方に注目。引数に応じたnewを一回だけ生成しそれを再利用することで、
Function.prototype.bindは何がいいのか - 枕を欹てて聴く
// なぜなら現行仕様はnew呼び出しの場合に引数として配列を与えるということができない
// 苦し紛れー
switch (a.length) {
case 0:
return new that();
case 1:
return new that(a[0]);
case 2:
/* … */
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");
}
という羽目に陥ることを防いでいます。
var oBind = Function.prototype.bind; /* preserve the original */
Function.prototype.bind = function bind(thisArg) {
"use strict";
var that = this,
args = Array.prototype.slice.call(arguments, 1),
slice = Array.prototype.slice,
fnop = function(){},
news = [];
var bound = function bound() {
var a = args.concat(slice.call(arguments)),
i = 0, l = a.length, as = [], evs, ret;
if (this instanceof bound) { /* I am a constructor! */
if (!news[l]){
for (; i < l; i++) { as.push('a['+i+']'); };
evs = 'news[l]=function(){return new that('
+ as.join(',')
+ ');};';
/* console.log(evs); */
eval(evs);
}
ret = news[l]();
if (typeof ret.__proto__ === 'object') {
ret.__proto__ = bound.prototype;
}
return ret;
} else {
return that.apply(thisArg, a);
}
};
fnop.prototype = that.prototype;
bound.prototype = new fnop();
return bound;
};
try{
/* works */
function Point(x, y) {
this.x = x;
this.y = y;
}
/* does not work IE because of __proto__ */
Point.prototype.toString = function() {
return this.x + "," + this.y;
};
var pt = new Point(1, 2);
p(pt.toString()); /* "1,2" */
var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0 /* x */);
var axisPoint = new YAxisPoint(5);
p(axisPoint.toString()); /* "0,5" */
p(axisPoint instanceof Point); /* true */
p(axisPoint instanceof YAxisPoint); /* true */
p(new Point(17, 42) instanceof YAxisPoint); /* false */
/* works at last! */
Function.prototype.construct = function(aArgs) {
if (aArgs.constructor !== Array)
throw new TypeError("second argument to Function.prototype.construct must be an array");
var aBoundArgs = Array.prototype.concat.apply([null], aArgs),
fBound = this.bind.apply(this, aBoundArgs);
return new fBound();
};
var aDateArgs = "2011-7-16 19:35:46".split(/[- :]/),
oMyDate1 = new Date(
aDateArgs[0], aDateArgs[1], aDateArgs[2], aDateArgs[3], aDateArgs[4], aDateArgs[5]
);
p(oMyDate1.toLocaleString());
var oMyDate2 = Date.construct("2011-7-16 19:35:46".split(/[- :]/));
p(oMyDate2.toLocaleString());
p(Point.construct([2, 4]).toString()); /* "2,4" */
/* but boundFun.length is wrong */
var theDay = Date.bind(null, 2011, 12-1, 18);
p( (new theDay(12,34,56)).toLocaleString() );
p( theDay.length ); /* supposed to be 4 but 0 */
}catch(e){
p(e);
}
oBind ? Function.prototype.bind = oBind : delete Function.prototype.bind;
最後に残った引数長
ここで、MDNのPolyfillの問題点をおさらいしておきましょう。
- The partial implementation relies
Array.prototype.slice,Array.prototype.concat,Function.prototype.callandFunction.prototype.apply, built-in methods to have their original values.- The partial implementation creates functions that do not have immutable "poison pill"
callerandargumentsproperties that throw aTypeErrorupon get, set, or deletion. (This could be added if the implementation supportsObject.defineProperty, or partially implemented [without throw-on-delete behavior] if the implementation supports the__defineGetter__and__defineSetter__extensions.)- The partial implementation creates functions that have a
prototypeproperty. (Proper bound functions have none.)- The partial implementation creates bound functions whose
lengthproperty does not agree with that mandated by ECMA-262: it creates functions with length 0, while a full implementation, depending on the length of the target function and the number of pre-specified arguments, may return a non-zero length.
boundされた関数の引数の長さなんて、実際の利用において一体誰が使うんだとは思うのですが、ここまで来たら少しでもECMAScript 5ネイティブのbindに近づけたい。というわけで出来上がったのがこちら。
var oBind = Function.prototype.bind; /* preserve the original */
Function.prototype.bind = function bind(thisArg) {
"use strict";
var that = this,
args = Array.prototype.slice.call(arguments, 1),
slice = Array.prototype.slice,
fnop = function(){},
news = [],
alen = that.length - arguments.length + 1,
farg = [], i = 0,
evs, bound;
for(;i < alen; i++) farg.push('_'+i);
evs = 'bound=function bound('+farg.join(',')+'){' +
[
"var a = args.concat(slice.call(arguments))," ,
"i = 0, l = a.length, as = [], evs, ret;" ,
"if (this instanceof bound) {" ,
"if (!news[l]){" ,
"for (; i < l; i++){ as.push('a['+i+']'); };" ,
"evs = 'news[l]=function(){return new that('" ,
"+ as.join(',') " ,
"+ ');};';" ,
"eval(evs);" ,
"}" ,
"ret = news[l]();" ,
"if (typeof ret.__proto__ === 'object') {" ,
"ret.__proto__ = bound.prototype;" ,
"}" ,
"return ret;" ,
"} else {" ,
"return that.apply(thisArg, a);" ,
"}"
].join('\n') +
"};";
eval(evs);
fnop.prototype = that.prototype;
bound.prototype = new fnop();
return bound;
}
try{
/* works */
function Point(x, y) {
this.x = x;
this.y = y;
}
/* does not work IE because of __proto__ */
Point.prototype.toString = function() {
return this.x + "," + this.y;
};
var pt = new Point(1, 2);
p(pt.toString()); /* "1,2" */
var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0 /* x */);
var axisPoint = new YAxisPoint(5);
p(axisPoint.toString()); /* "0,5" */
p(axisPoint instanceof Point); /* true */
p(axisPoint instanceof YAxisPoint); /* true */
p(new Point(17, 42) instanceof YAxisPoint); /* false */
/* works at last! */
Function.prototype.construct = function(aArgs) {
if (aArgs.constructor !== Array)
throw new TypeError("second argument to Function.prototype.construct must be an array");
var aBoundArgs = Array.prototype.concat.apply([null], aArgs),
fBound = this.bind.apply(this, aBoundArgs);
return new fBound();
};
var aDateArgs = "2011-7-16 19:35:46".split(/[- :]/),
oMyDate1 = new Date(
aDateArgs[0], aDateArgs[1], aDateArgs[2], aDateArgs[3], aDateArgs[4], aDateArgs[5]
);
p(oMyDate1.toLocaleString());
var oMyDate2 = Date.construct("2011-7-16 19:35:46".split(/[- :]/));
p(oMyDate2.toLocaleString());
p(Point.construct([2, 4]).toString()); /* "2,4" */
/* finally... */
var theDay = Date.bind(null, 2011, 12-1, 18);
p( (new theDay(12,34,56)).toLocaleString() );
p( theDay.length ); /* 4 */
}catch(e){
p(e);
}
oBind ? Function.prototype.bind = oBind : delete Function.prototype.bind;
関数の、最高の友達
Jobsが生前言っていたように。HTML5のサポートは、App Storeの厳格なコントロールを正答化する最高の理由にもなっています。
それだけに、主要ブラウザーの中でSafari(とWebKit)だけがFunction.prototype.bindをネイティブ実装していないことが余計気になります。
404 Blog Not Found:javascript - そろそろECMAScript 5を使いたい少なくとも3つの理由
- IEは9以上以降かつStandard Modeなら使える
- Safari 5は
Function.prototype.bindのみ使えない- iOS5も同様
- Android 2.3ではさらに加えて
Object.sealなどObjectをロックする機能が使えない
数多の世界の運命を束ね、ポストPCの特異点となった君なら、どんな途方もない仕様だろうと、叶えられるだろう、Apple?
Dan the JavaScripter
このブログにコメントするにはログインが必要です。
さんログアウト
この記事には許可ユーザしかコメントができません。