手写bind

2,340

什么是bind

引用MDN的解释

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

翻译一下,bind主要做了三件事

  • 返回一个新的函数
  • 新函数this指向bind的第一个参数
  • 其余参数作为新函数的参数传入

基础的使用

一个最简单的例子

var tea = {
    value: '奶茶'
}

function drink(name) {
    console.log(`${name} drink ${this.value}`);
}

// 1、返回了一个函数
// 2、新函数的this是第一个参数
// 3、其余参数作为新函数的参数
var drinkTea = drink.bind(tea, 'dongchuan');

drinkTea();  // 'dongchuan drink 奶茶'

简单实现bind

分析了bind做了的三件事情,我们就可以来简单的实现bind了,我们先使用ES6进行实现,方便快捷好看懂

// 第一版
function _bind(asThis, ...args1) {
    let fn = this;  // 函数调用时,原this其实就是这个调用函数
    return function(...args2) {  // 同时,返回的新函数也可以接受参数
        return fn.call(asThis, ...args1, ...args2);
    }
}

构造效果的模拟实现

其实考虑到上面bind做的三件事情已经基本完成了bind的实现,但是在看其他同学些的bind实现时,发现还有一个,如果对返回的新函数使用new操作符,bind重新定义的this会失效!!!

具体参考博客链接 JavaScript深入之bind的模拟实现 #12

其实在MDN上也有相关描述,只是没有在最上方写明

MDN关于使用bind后再使用new的介绍

绑定函数也可以使用new运算符构造,它会表现为目标函数已经被构建完毕。提供的this值会被忽略,但前置参数仍会提供给模拟函数

重要信息提取:提供的this值会被忽略

用一个例子来理解一下

var tea = {
    value: '奶茶'
}

function drink(name) {
    console.log(`${name} drink ${this.value}`);
}

var drinkTea = drink.bind(tea, 'dongchuan');

drinkTea();  // 'dongchuan drink 奶茶'

var obj = new drinkTea();  // 'dongchuan drink undefined'

console.log(obj.value);  // undefined

// 使用new操作符后,this指向为obj

需要注意的点:

  • drinkTeathis其实还是指向的tea
  • 对绑定函数drinkTea使用new操作符,原来绑定的this会失效,此时的this指向obj

所以,我们需要对bind返回的这个新函数的this做一个判断

  • 如果新函数的this是在新函数的原型上,即是用构造函数
    // 简单的实现一个new
    function _new(resultFn) {
        let tmp = {};
        tmp.__proto__ = resultFn.prototype;
        resultFn.call(tmp);
        return tmp;
        
        // 验证
        console.log(tmp instanceof resultFn);  // true
    }
    
  • 否则就是正常情况

重新设计返回新函数的this指向

// 第二版,支持new
function _bind(asThis, ...args1) {
    let fn = this;
    let resultFn = function(...args2) {
        console.log(this instanceof resultFn);  // 如果使用new操作符,这里为true
        return fn.call(this instanceof resultFn ? this : asThis, ...args1, ...args2);
    }
    // 针对创建实例做处理,使实例能够继承绑定函数的原型
    resultFn.prototype = fn.prototype;
    return resultFn;
}

针对返回函数的prototype的优化

这里还有一个问题,如果直接用resultFn.prototype = fn.prototype,修改resultFn.prototype也会造成fn.prototype的修改

  • 解决方式:使用一个空函数作为中转
// 第三版
function _bind(asThis, ...args1) {
    let fn = this;
    let resultFn = function(...args2) {
        return fn.call(this instanceof resultFn ? this : asThis, ...args1, ...args2);
    }
    let fnNo = new Function();
    fnNo.prototype = fn.prototype;
    resultFn.prototype = new fnNo();  // 使用fnNo做中转 resultFn.prototype.__proto__ === fnNo.prototype === fn.prototype;
    return resultFn;
}

用ES5替代ES6的实现

以上所有bind的实现都是通过ES6实现的,但是如果都需要手写bind,那么很多ES6新语法支持度可能也不那么高,所以,我们把实现通过ES5进行重写,那我们再重新梳理一下实现的思路

  • 返回一个新的函数
  • 新函数的this在正常情况下指向第一个参数
  • 其他参数传给新函数
  • new的支持,包括this不能被第一个参数替换,支持绑定函数的prototype属性的访问

使用ES5重写

// 第四版
function _bind(asThis) {
    var fn = this;
    var args1 = Array.prototype.slice.call(arguments, 1);
    var resultFn = function() {
        var args2 = Array.prototype.slice.call(arguments);
        return fn.apply(this instanceof resultFn ? this : asThis, args1.concat(args2));
    }
    
    var fnNo = new Function();
    fnNo.prototype = fn.prototype;
    resultFn.prototype = new fnNo();
    return resultFn;
}

最终代码

最后,加上异常情况判断

  • 判断调用bind的是不是个函数
  • 在线上使用,判断是否存在bind

所以,最后的实现代码就是:

// 最终版
function _bind(asThis) {
    // 判断调用bind的是不是个函数
    if (typeof this !== 'function') {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }
    var fn = this;
    var args1 = Array.prototype.slice.call(arguments, 1);
    var resultFn = function() {
        var args2 = Array.prototype.slice.call(arguments);
        return fn.apply(this instanceof resultFn ? this : asThis, args1.concat(args2));
    }
    
    var fnNo = new Function();
    fnNo.prototype = fn.prototype;
    resultFn.prototype = new fnNo();
    return resultFn;
}

Function.prototype.bind = Function.prototype.bind || _bind;

完成!