模拟实现ES5中原生的bind函数

模拟原生bind


看到这个题目,首先要明确bind的用法,知道自己要完成一个什么样的目标。

1. 对于bind,大家都知道其可以改变this的指向。既然说到this,就回顾一下this的几种使用场景:

        I. 作为构造函数执行(在执行构造函数时,this <- {}, this.__proto__ <- 构造函数.prototype,执行构造函数中的代码(如this.属性名 <- 属性值), return this。这里需要注意的是,在构造函数中,如果没有显式return表达式,则返回this,如果return后面跟的是NumberStringBooleannullundefined,则忽略,返回值仍为this,如果return后面是一个对象,则返回该对象)

        II. 作为对象属性执行:方法调用(this就指向该对象)

        III. 作为普通函数执行 :函数调用(this<-window)

        IV. call、apply、bind 这三种方法都能改变this的指向
Foo.method = function(){
    alert(this)   //function Foo(){}
    function test(){
        alert(this) //window
    }
    test()
}

2. bind的语法:

fun.bind(thisArg[,arg1[,arg2[,...]]])//[]为可选项

bind方法会创建一个新的函数,这个新函数的this是bind的第一个参数,后续的参数是这个新函数的参数

3.bind返回的函数也能使用new操作符创建对象,这种行为就像把原函数当作构造器,忽略了bind指定的this值,同时调用时的参数被提供给新函数

实现方法


Function.prototype.bind = function(context){
    var slice = Array.prototype.slice;
    var args = slice.apply(arguments);
    var me = this; 
    return function(){
        return me.apply(context, args.slice(1).concat(slice.apply(arguments)));
    }
};

上面代码做到了改变this指向和bind的语法这两条,但是第三个目标没有实现。这里提一句,一定要搞清楚每个函数的用法,输入,输出,还有其自身特别的地方,比如这里的new 创建bind返回的函数,原先绑定的this失效。否则,这就不叫模拟!!!

上面这种方法其实是javascript语言精粹里面提到的柯里化(curry):把函数与传递给它的参数相结合,产生出一个新的函数。

加分项(1)


这是在一篇博客里看到的,里面提到上面的实现,是一个典型的”Monkey patching(猴子补丁) ” 即“给内置对象扩展方法”,也就是属性在运行时的动态替换。注意:一个错误特性被经常使用,那就是扩展object.prototype或者其他内置类型的原型对象,这样的monkey patching会破坏封装,虽然它被广泛的应用到一些JavaScript类库中比如Prototype,但为内置类型添加一些非标准的函数并不是一个好主意。扩展内置类型的唯一理由是为了和新的JavaScript保持一致,比如Array.forEach。所以,在这里可以进行一下“嗅探”,进行兼容处理。

Function.prototype.bind = Function.prototype.bind || function(context){
    ...
}

加分项(2)


需要注意:调用bind方法的一定是一个函数,所以可以在函数内部做一个判断:

if(typeof this !== "function"){
    throw new TypeError("Function.prototype.bind-what is trying to be bound is not callable")
}

写到这里,自我感觉还不错,回头看看我们的目标,咦,漏掉了第三条。。

要实现bind返回的绑定函数也能使用new操作符创建对象,就像把原函数当作构造器,之前提供的this值被忽略。要完成这个目标,首先还是要搞清楚new操作符的一系列动作(见文章顶部)。思考一下new的第二步操作,this.proto <- 构造函数.prototype,在我们的代码里,构造函数就是return的函数,所以我们应该把之前bind返回的函数的prototype属性赋值为原函数的prototype(在javaScript中所有函数都有prototype属性,网上以为细心的同学发现原生bind返回的函数没有prototype属性)。实现代码如下:

Function.prototype.bind = function(context){
    var slice = Array.prototype.slice;
    var args = slice.apply(arguments);
    var me = this; 
    var bound = function(){
        var thisArgs = slice.apply(arguments)
        return me.apply(context, args.slice(1).concat(thisArgs));
    }
    bound.prototype = this.prototype;
    return bound;
};

接着还需要考虑,怎么判断这个函数是被new的还是被直接调用的,这需要在bound里面判断,如果this是原函数的实例,则该函数作为构造函数使用,否则apply的第一个参数仍为context。


补充知识


如何判断一个对象是不是某函数的实例呢?这就得从原型链说起,下面简单来一张原型链的示意图来说明一下:

模拟实现ES5中原生的bind函数_第1张图片

我们都知道instanceof用于判断引用类型属于哪个构造函数:
例如objF instanceof Foo的判断逻辑:
从objF的proto开始一层一层往上找,看能否找到Foo.prototype.

objF.__proto__ === Foo.prototype //true
objF.__proto__.__proto__ === Object.prototype //true
objF instanceof Object //true

话不多说,继续探讨上面未完的问题,想要知道this是不是原函数的实例,也就是从this._proto_开始一层一层网上找,看能不能找到原函数.prototype.因此,我们的代码修正为:

Function.prototype.bind = function(context){
    var slice = Array.prototype.slice;
    var args = slice.apply(arguments);
    var me = this;
    var tempProto = this.prototype 
    var bound = function(){
        var thisArgs = slice.apply(arguments)
        return me.apply(this.__proto__ === tempProto ? this:context, args.slice(1).concat(thisArgs));
    }
    bound.prototype = this.prototype;
    return bound;
};

经测试,以上代码确实达到了目标三。

Function.prototype.bind = function(context){
    var slice = Array.prototype.slice;
    var args = slice.apply(arguments);
    var me = this;
    var tempProto = this.prototype 
    var bound = function(){
        var thisArgs = slice.apply(arguments)
        alert(this.__proto__ === tempProto)
        return me.apply(this.__proto__ === tempProto ? this:context, args.slice(1).concat(thisArgs));
    }
    bound.prototype = this.prototype;
    return bound;
};
function fn1(name){
    alert(name)
    console.log(this)
}
var fn2 = fn1.bind({x:2}) 
fn2("summer")            //输出:false,summer,{x:2}
var obj = new fn2('summer')  //输出:true,summer,bound{}
typeof obj //输出:"object"
obj instanceof fn1 //输出:true
obj instanceof Function //输出:false
obj instanceof Object //输出:true
obj.__proto__ === fn1.prototype //true

到此为止,实现了最开始说的三个目标。对比一下简书里某大神的答案,发觉自己写的代码健壮性不行,也不美观,一些情况没有考虑到。
比如,context为null或者undefined,这种情况下返回this,也就是全局变量window。这里目前我还不太理解,我还是觉得没必要,因为巨酸context是null或者undefined,那么bound里return的第一个参数为null或者undefined不就行了么,也不报语法错误。。加上this指向全局变量也行。。

除此之外,和大神的代码相差最大的就是中间那一段

F = function(){};
F.prototype = this.prototype;
...
this instanceof F

我觉得这样写的目的也就是代码更美观了,其实我们的目的是一样的,可能一般很少会直接把this.prototype存到一个变量,所以添加一个构造函数,让该构造函数来承接原函数的prototype属性。在代码return的上一行,我直接从this.prototype里取值复制给Bound.prototype,大神说是这样会污染this原型,至于怎么算是污染,我还不懂,以后明白了,再来补充吧!最后把完整的代码贴上来:

Function.prototype.bind = Function.prototype.bin || function(context){
    if (typeof this !== "function") {
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }
    var slice = Array.prototype.slice;
    var args = slice.apply(arguments);
    var me = this;
    var F = function(){};
    F.prototype = this.prototype;
    var bound = function(){
        var thisArgs = slice.apply(arguments)
        return me.apply(this instanceof F ? this : context || this, args.slice(1).concat(thisArgs));
    }
    bound.prototype = new F();
    return bound;
};

以上就是对实现bind函数的一些思考,有哪些理解错误的东西,还望留言批评指正,共同学习☺(●’◡’●)

这里还有一个进阶的问题,不使用apply,call实现bind,这里先mark一下。先贴上一个大神的解决方案http://qdxmq.com/2017/05/02/%E4%B8%80%E9%81%93%E9%9D%A2%E8%AF%95%E9%A2%98%EF%BC%9A%E4%B8%8D%E7%94%A8call%E5%92%8Capply%E6%96%B9%E6%B3%95%E7%94%A8%E5%8E%9F%E7%94%9FJavaScript%E6%A8%A1%E6%8B%9F%E5%AE%9E%E7%8E%B0ES5%E7%9A%84bind%E6%96%B9%E6%B3%95/

参考资源


  • 我可能看了假源码:http://www.jianshu.com/p/6958f99db769
  • JavaScript秘密花园:http://bonsaiden.github.io/JavaScript-Garden/zh/
  • 观V8源码中的array.js,解析Array.prototype.slice为什么能将类数组对象转为真正的数组:http://www.cnblogs.com/henryli/p/3700945.html

你可能感兴趣的:(js,bind)