JavaScript:探索bind和new之间的关系

JavaScript: 手写 bind 和探索 bind 与 new 之间的关系

首先了解一下 bind 的定义

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

也就是说 bindapplycall 不一样,bind 是返回一个待执行函数,并且可以传入参数。来看一看 bind 的例子

var foo = {
    value: 1
};
function bar() {
    console.log(this.value);
}
// 返回了一个函数
var bindFoo = bar.bind(foo); 
bindFoo(); // 1
// bindFoo() 相当于 bar.bind(foo)();

所以 bind 返回的是一个函数(这也是为什么 new 可以改变 bind 后函数的 this 指向的原因)并且返回的函数可以继续传入参数。

var foo = {
    value: 1
};
function bar(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);
}
var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18

所以要对参数进行处理。

// 第二版
Function.prototype.myBind = function (context) {

    var self = this;
    // 获取myBind第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);

    return function () {
        // 这个时候的arguments是指bind返回的函数传入的参数,这个arguments和上面的不是同一个
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context, args.concat(bindArgs));
    }
}

到这里 bind 的基本功能差不多完成了,但是 bind 还有一个特点,那就是和 new 配合使用的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。下面这个例子就可以说明。

var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'game';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
var bindFoo = bar.myBind(foo, 'hcb');
var obj = new bindFoo('18'); // 这里bind绑定的this值已经失效了
// undefined
// hcb
// 18

所以我们要通过修改返回的函数的原型来实现

特别要注意下面的代码执行完只是返回 fBound ,函数里面的语句还未执行,所以这就是为什么这里能判断 fBound 里面的 this 到底指向普通函数还是构造函数

Function.prototype.myBind = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
      	// 这里有同学就问了,这里怎么判定this是属于构造函数还是普通函数的呢?
      	// 因为bind返回的是一个函数的定义,在new中这个函数被调用,所以这里的this是到调用的时候才确定的
      	// 
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值,因为js中对象是引用传递
    fBound.prototype = this.prototype;
    return fBound;
}

但是在这个写法中,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。为什么也会直接修改呢?因为这里是引用类型,这个时候,我们可以通过一个空函数来进行中转。

Function.prototype.myBind = function (context) {

  	// 只有函数才能调用bind,所以要加这个判断
    if (typeof this !== "function") {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () { };

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

来看一下 new 的手写实现:

function myNew4(fn, ...args) {
    if (typeof fn !== 'function') {
        throw 'fn must be a function';
    }
    let obj = {};
    obj.__proto__ = fn.prototype;
    // 如果fn返回值是一个对象的话,那么就返回这个对象里面的东西
    // 如果不是的话,那么就返回构造出来的对象
    // console.log(obj);
  	
  	// 这里的fn可以看作bind里面return出来的fBound,所以到这里fBound的this就被改变了
    let res = fn.apply(obj, args); // obj变成fn这个函数的this
    // console.log(obj);
    return res instanceof Object ? res : obj;
}
------------- 本文结束 感谢阅读 -------------