ECMA262 Edition5. Function.prototype.bind
ES5新增 API .
15.3.4.5.Function.prototype.bind (实现:IE9+,Chrome?懒得去确定版本了,Firefox4+,可惜Safari5没有实现此接口.不过这个完全不是问题.)
(产生一个特殊的函数对象,并以第一个参数作为this值去调用被bind的函数,同时把bind时的参数列表与,bind产生的函数对象被调用时的参数列表连接.作为新的参数列表.)
bind方法,以一个或多个参数, thisArg以及(可选的)arg1,arg2等等..根据下面的步骤,返回一个新的 函数对象:
1. 令Target 为 this的值.
2. 如果 isCallable(Target)是false, 抛出一个 TypeError异常.
3. 令 A为(也许是空的) 按顺序,包含所有实参(thisArg除外)的List内部对象.
4. 令 F为一个新的原生ECMAScript对象.(注1)
5. 把8.12章节中定义的那些个内部方法中,除[[Get]]以外,通通给F添加上.(注2)
6. 给F添加一个 15.3.5.4中定义的内部方法[[Get]].(注3)
7. 把F的内部属性[[TargetFunction]](注4),设置为Target.
8. 把F的内部属性[[BoundThis]](注5),设置成thisArg的值.
9. 把F的内部属性[[BoundArgs]](注6)设置成 A.
10. 把F的内部属性[[Class]]设置成 'Function'.
11. 把F的内部属性[[Prototype]] 设置成内置对象 Function.prototype.(参见15.3.3.1)
12. 把F的内部属性[[Call]]设置为 15.3.4.5.1中描述的那个样子(注7)
13. 把F的内部属性[[Construct]]设置为 15.3.4.5.2中描述的那个样子
14. 把F的内部属性[[HasInstance]]设置为 15.3.4.5.3描述的那样子
15. 如果 Target的内部属性[[Class]],是'Function',则
a. 令L为 Target.length 减去 A.length的计算结果.
b. 把F.的自身独占属性 length ,设置为 0和L中更大的那个.
16. 否则 把F的自身独占属性 length,设置为0.
17. 把F的自身独占属性 length的特性集,设置成 15.3.5.1定义的那样.({writable:false,enumerable:false,configurable:false})
18. 把F的内部属性[[Extensible]]设置为true.
19. 令thrower 为 内部函数对象[[ThrowTypeError]](参见13.2.3)
20. 以'caller', 属性描述符{[[Get]]:thrower,[[Set]]:thrower,[[Enumerable]]:false,[[Configurable]]:false}, false 为参数,调用F的内部方法[[DefineOwnProperty]].
21. 以'arguments', 属性描述符{[[Get]]:thrower,[[Set]]:thrower,[[Enumerable]]:false,[[Configurable]]:false}, false 为参数,调用F的内部方法[[DefineOwnProperty]].
22. 返回F.
bind方法的形参个数是1个,所以bind.length是1.
注意: 由Function.prototype.bind,创建的函数对象.不具备 prototype属性(注8),以及[[Code]],[[FormalParameters]].[[Scope]] 内部属性.
15.3.4.5.1.[[Call]] - 见注释3.
15.3.4.5.2 [[Construct]]()
当(P128)
注1: 原生/本地(native)对象指:
由 ECMAScript 提供实现,并独立于宿主环境的任何对象。标准原生对象由ECMAScript标准定义.有些原生对象也是内置对象(内置对象指,在 ECMAScript 程序刚开始执行时就出现的原生对象.如Global、Math String Number Function Object Array 等等...),某些原生对象是在某个ECMAScript程序执行过程中构造出来的.
好吧,这些是官方概念. 这里其实他是指创建一个干净的原生对象.用于为他进行装饰.以实现,某种不可告人的目的.
注2: 指每一个ECMAScript原生对象所需要具备的内部方法:
[[GetOwnProperty]], [[GetProperty]], [[Get]], [[CanPut]],[[Put]], [[HasProperty]], [[Delete]], [[DefaultValue]], [[DefineOwnProperty]]. 这些8.10中所定义的内部方法中.除了[[Get]].都要添加给指定的那个原生对象F.
注3: 函数对象所使用的[[Get]](P)方法: (这个方法主要就是为了处理严格模式下,调用函数对象的caller属性时,抛出异常的.其他同默认的[[Get]].)
函数对象所用的[[Get]]方法,和其他ECMAScript原生对象,所用的(8.12.3那个版本)的,有所区别.
假设F是一个函数对象,那么当以属性名P为参数,调用F的内部方法[[Get]]时,就按下面的步骤进行操作:
1. 令v为 以 P为属性名参数,在F上调用默认的,8.12.3所定义的内部方法[[Get]]的执行结果.
2. 如果 P 的值是'caller', 并且 v是一个严格模式的函数对象,则抛出一个 TypeError异常(即严格模式下,不允许调用function对象的caller属性了).
3. 返回 v.
注4: [[TargetFunction]],此内部属性,是个专有属性,只有通过Function.prototype.bind,所创建的那个函数对象,才会具备这个内部属性.
好吧,其实这玩意就是,在具备这个属性的那个函数对象调用[[Call]]时,真正去执行的那个函数...这东西所以被设计出来,就是为了避免一个函数去包裹另一个函数实现.bind的功能.
注5: [[BoundThis]],此内部属性也是个专有属性,只有通过Function.prototype.bind,所创建的那个函数对象,才会具备这个内部属性.
这玩意,也是在具备这个属性的那个函数对象调用[[Call]]时,会把这个东西作为调用[[TargetFunction]].[[Call]]时,的this值.
this值为undefined,或null的时候.就要参考 10.4.3章节.关于进入function code阶段. 如果是严格模式,就保持不变(这并不意味着,一定要抛出一个异常,但是chrome,Firefox都抛出了异常,这是我无法理解的.除非你在函数内部调用了 this.xxx.这样抛出异常才合理.又或者ES5有严格要求,说bind方法thisArg在严格模式是不能为null或undefined.但是显然,ES5,并没有交代这个问题.但我个人认为抛出异常是合理的,用于提醒我们,this不指定,调用bind,还有什么意义?).如果非严格模式,就把this,设置为global(同es3一样).其他原始类型,则做ToObject操作.
注6: [[BoundArgs]](这货,可能应该叫[[BoundArguments]]. 老道在前面起了名字,然后在用的时候把名字记错了.)这个也是个专有属性.只有通过Function.prototype.bind,所创建的那个函数对象,才会具备这个内部属性.
这玩意也是在具备此属性的对象在调用其内部方法[[Call]]的时候,会把这个作为调用其[[TargetFunction]]时候参数列表.传进去.
注7: [[Call]](15.3.4.5.1), 当某个函数对象 F(这个对象,一定是由Function.prototype.bind所产生的)的内部方法[[Call]],以一个this值和一个参数列表 ExtraArgs作为参数,被调用时,就:
1. 令 boundArgs为 F的内部属性[[BoundArgs]].
2. 令 boundThis 为 F的内部属性[[BoundThis]].
3. 令 target 为 F的内部属性[[TargetFunction]]
4. 令 args 为一个,包含boundArgs列表对象的参数列表,以及紧随其后的ExtraArgs列表对象,并遵守其原始顺序和参数的,新的列表对象.
(连接bind调用时的参数列表和,bind产生的函数对象,被调用时的参数列表.)
5. 以 boundThis 作为this的值, args作为参数列表,去调用 target的内部方法[[Call]].
ps: 这就是bind,真正的实现目的和原理了.全靠这个特殊的call啊.
注8: Chrome15,才开始遵守此标准. Chrome14之前的版本.则仍然存在prototype属性. 其他如IE9,Firefox4+ 都ok.
模拟实现的思考.
demo :
//'use strict'; if(!Function.prototype.bind){ // 模拟实现, 其中因函数的length属性为只读.所以此处不给力. Function.prototype.bind = function (boundThis) { var targetFunction = this, boundArgs = [].slice.call(arguments, 1), //A = [].slice.call(arguments, 1), //L = targetFunction.length, F = function () { targetFunction.apply(boundThis, boundArgs.concat([].slice.call(arguments))); }; //F.length = Math.max(L - A.length, 0); F.prototype = undefined; //此处有风险. 在于 一个普通的函数对象,如果没有prototype.则 ,其作为instanceof 运算符的右运算元时,会因为检查不到prototype属性而被抛出异常 return F; }; } var obj = { name:'franky' }, fn = function(a, b, c, d, e){ alert([this.name, a, b, c, d, e]); }.bind(obj, 1, 2, 3) fn(4,5); // franky,1,2,3,4,5 F.prototype = undefined; 此处有风险. 在于 一个普通的函数对象,如果没有prototype.则 ,其作为instanceof 运算符的右运算元时,会因为检查不到prototype属性而被抛出异常. 而 原生bind之所以没有这个问题是因为 ,原生bind所产生的函数对象.的内部方法[[Construct]].实质上会去调用该函数对象的[[TargetFunction]].作为真正的构造器使用,同样,instanceof运算符 导致调用 函数对象的内部方法[[HasInstance]],该方法也易于一般函数对象.实际也是去调用[[TargetFunction]]的内部方法[[HasInstance]]. 也就是说. bind产生函数对象的部分方法.都是对 [[TargetFunction]]的对应方法的包裹. |
demo2:
if(!Function.prototype.bind){ Function.prototype.bind = function (boundThis) {//模拟版本2 .增加 bind产生的函数对象,作为构造器使用的情况. var _slice = [].slice, targetFunction = this, boundArgs = _slice.call(arguments, 1), //A = [].slice.call(arguments, 1), //L = targetFunction.length, F = function () { if(this instanceof F){// F.F = F; 就是悲剧. 无法分辨 new F.F(), 还是 F.F() var obj, ret, temp = function () {}; temp.prototype = targetFunction.prototype; obj = new temp; ret = targetFunction.apply(obj, boundArgs.concat(_slice.call(arguments))); return typeof ret == 'object' && ret !== null ? ret : obj; } return targetFunction.apply(boundThis, boundArgs.concat(_slice.call(arguments))); }; //F.length = Math.max(L - A.length, 0); //F.prototype = undefined; return F; }; } var obj = { name:'franky' }, f = function(a, b, c, d, e){ alert([this.name, a, b, c, d, e]); return 'aaa'; }, fn = f.bind(obj, 1, 2, 3) var obj2 = new fn(); ok 现在所有浏览器在这个 测试用例中,行为是一致的了. 但是显然 ,为了简单的检查是否是作为构造器调用.我们必须舍弃 其prototype = undefined 的部分. |