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 的部分.

posted @ 2011-09-21 23:50  Franky  阅读(2836)  评论(3编辑  收藏  举报