美文网首页
手写bind

手写bind

作者: 方_糖 | 来源:发表于2021-03-30 18:04 被阅读0次
1. 利用call实现一个最基础的bind
var getName = function(){
    console.log(this.name)
}
//call
getName.call({name:"Jan"})  //直接输出"Jan"
//bind
var f = getName.bind({name:"Jan"});
f();        //执行f才输出"Jan"

bind和call的区别 : bind返回的是函数,函数调用后才运行。而call直接运行。所以得到如下bind实现

//bind是给所有函数来调用的
Function.prototype.bind = function(obj){
    var fn = this;
    return function(){
        //返回call的调用形式
        return fn.call(obj)     
    }
}
2. 传递多个参数

bind和call一样,都可以传递多个额外参数, 这里我们可以用ES6的...args,也可以用ES5的arguments(通过Array.prototype.slice.call(arguments, 0)arguments转换成普通数组)

//bind是给所有函数来调用的
Function.prototype.bind = function(obj, ...args){
    var fn = this;
    return function(){
         //返回call的调用形式
        return fn.call(obj, ...args)    
    }
}

但是bind除了支持

var getName = function(age, city){
    console.log(this.name + "," + age + "," + city)
}
var f = getName.bind({name:"Jan"}, 11, "BeiJing");
f();        //Jan,11,BeiJing

还支持在f()中传递多个参数(预设

var f = getName.bind({name:"Jan"}, 11);
f("Beijing");        //Jan,11,BeiJing

所以我们要把bind函数改写成如下:

Function.prototype.bind = function(obj, ...args){
    var fn = this;
    return function(...others){
         //返回call的调用形式
         fn.call(obj, ...args, ...others) 
    }
}
3. 作为构造函数使用

可以用new操作符创建一个由bind实现的绑定函数,如:

function Person(name) {
    this.name = name
}
Person.prototype.getName = function(){
    console.log(this.name)
}
var PersonBind = Person.bind({}, "Jan")
var p = new PersonBind();
p.getName()     //Jan
console.log(p instanceof PersonBind)    //true
console.log(p instanceof Person)        //true

这是我们要返回的不能是一个简单的函数,而应该是一个继承了Person属性和方法的对象。
首先我们先使用原型链的方式继承,原型链的写法为B.prototype = new A()(B继承A),所以可以得到如下的bind写法

Function.prototype.bind = function(obj, ...args){
    var fn = this;
    var res = function(...others){
        //返回call的调用形式
        fn.call(obj, ...args, ...others) 
    }
    res.prototype = new fn(...args);  //通过原型链继承
    return res
}

乍一看没什么问题,而且结果输出也都是正确的,但是当我们输出var p = new PersonBind();中的p,发现在使用了我们的bind后,浏览器输出是这样的

我们改写的bind输出
而原始的bind输出是这样的
原始的bind输出

bind要求我们只是继承方法,而传递的参数我们通过借用构造函数来继承,即通过组合继承来实现(原型链继承和组合继承的区别请参考js对象——继承(2)
使用组合继承实现如下

Function.prototype.bind = function(obj, ...args){
    var fn = this;
    var res = function(...others){
        fn.call(this, ...args, ...others);  //借用构造函数继承属性
    }
    res.prototype = fn.prototype;   //原型链的方式继承方法
    return res
}

但是我们突然发现,虽然实现了用new创建,但传统的bind使用方式失效了

4. 使用组合继承来改造

构造函数(new创建)和普通函数的调用有什么区别呢?使用构造函数时,内部的this指向的是该对象,而调用普通函数是,内部的this指向的是window,所以我们可以通过这一点来返回不同的内容

Function.prototype.bind = function(obj, ...args){
    var fn = this;
    var res = function(...others){
        if(this == window){
            return fn.call(obj, ...args, ...others);
        }else{
            return fn.call(this, ...args, ...others);  //借用构造函数继承属性
        }
    }
    res.prototype =new f();   //原型链的方式继承方法
    return res
}

存疑: return fn.call(this, ...args, ...others)和obj并没有联系

5. 官方实现

链接:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

if (!Function.prototype.bind) (function(){
  var ArrayPrototypeSlice = Array.prototype.slice;
  Function.prototype.bind = function(otherThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var baseArgs= ArrayPrototypeSlice.call(arguments, 1),
        baseArgsLength = baseArgs.length,
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          baseArgs.length = baseArgsLength; // reset to default base arguments
          baseArgs.push.apply(baseArgs, arguments);
          return fToBind.apply(
                 fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
          );
        };

    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype;
    }
    fBound.prototype = new fNOP();

    return fBound;
  };
})();

其实变成ES6写法就可以简化成如下:

Function.prototype.myBind = function(obj, ...args){
    var fn = this;
    //仿制一个Person的构造函数
    var F = function(){};   
    if(fn.prototype){
        //将Person的原型全部拷贝到F
        F.prototype = fn.prototype;     
    }
    var res = function(){
        //this: BindPerson{}对象 / window
        //F: function Person
        var callObj = F.prototype.isPrototypeOf(this) ? this : obj;
        return fn.call(callObj, ...args)
    }
    res.prototype = new F();
    return res
}

我们用一个例子来说明上面代码的干了什么

function Person(name){
    this.name = name
}
Person.prototype.getName = function(){
    return this.name
}

var BindPerson = Person.bind({}, "xxx");
var bp = new BindPerson();
console.log(bp.getName())

(1)复制Person构造函数,名为F
(2)创建一个全新构造函数res,通过原型链的方法继承F的方法
(3)返回构造函数res,BindPerson = res
(4)当通过bp = new BindPerson();来创建对象时,此时res里面的this指向bp对象,而bp对象是res的实例,res又是继承了Person,所以F.prototype.isPrototypeOf(this) = true。再通过构造函数继承Person的属性fn.call(this)
(5)当通过普通方式调用时BindPerson (),此时的this指向window,F.prototype.isPrototypeOf(this) = false。就只需要直接调用fn.call(obj)

6. 总结

对比官方提供的标准答案我们发现,其实唯一的区别在于:判断是不是通过new创建的,简单的用this == window并不严谨,如果不是通过浏览器调用,而是通过node运行,这时的this就不等于window了

相关文章

网友评论

      本文标题:手写bind

      本文链接:https://www.haomeiwen.com/subject/gnsghltx.html