Skip to content

模拟实现 bind #19

@chenqf

Description

@chenqf

模拟实现 bind

什么是 bind

bind方法,顾名思义,就是绑定的意思,到底是怎么绑定然后怎么用呢?

我们首先来看一下MDN中对于bind的定义以及bind的语法。

bind 的定义:

bind() 方法创建一个新的函数,在调用时设置this关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。

bind 的语法:

function.bind(thisArg[, arg1[, arg2[, ...]]])

参数:

  • thisArg
    • 调用绑定函数时作为this参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。当使用bind在setTimeout中创建一个函数(作为回调提供)时,作为thisArg传递的任何原始值都将转换为object。如果bind函数的参数列表为空,执行作用域的this将被视为新函数的thisArg。
  • arg1, arg2, ...
    • 当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。

返回值:

  • 返回一个原函数的拷贝,并拥有指定的this值和初始参数。

一个简单的例子

let foo = {
    value: 1
};
function bar(arg) {
    console.log(this.value,arg);
}
// 返回了一个函数
let bindFoo = bar.bind(foo,2);
bindFoo(); // print: 1  2

对bind的简单归纳

  • 返回一个新函数
  • 为新函数指定 this
  • 为新函数指定参数

开始实现我们的 bind 函数

先考虑第一个特点,返回一个新函数

Function.prototype.myBind = function() {
    let func = this;
    return function(...args){
        return func(...args);
    }
}

再考虑第二个特点,为新函数指定 this

我们知道可以通过使用 apply 或 call 来为函数指定 this , 在这里我们用 apply 来实现

Function.prototype.myBind = function(thisArg) {
    let func = this;
    return function(...args){
        return func.apply(thisArg,args);
    }
}

再考虑第三个特点,为新函数指定参数

对于参数来说,有很多不确定性,我们可以在 bind 的时候指定参数,也可以在新函数中指定参数,那具体的规则是什么呢?

来看关于 bind 函数,参数的一个例子

let fn = function(a,b){
    console.log(a,b);
}
let fn1 = fn.bind(null);
fn1('a','b'); // print: a b
let fn2 = fn.bind(null,1);
fn2('a','b'); // print: 1 a
let fn3 = fn.bind(null,1,2)
fn3('a','b'); // print: 1 2

由此我们得出这样的结论:

bind 函数可以在指定 this 的同时,同时指定为新函数预设参数,当然也可以不预设参数。

接下来我们来实现预设参数的特性

Function.prototype.myBind = function(thisArg,...args) {
    let func = this;
    return function(){
        //将 arguments 转换为 数组 并与 myBind 函数的 args 合并
        let _args = [...args,...Array.prototype.slice.call(arguments)];
        return func.apply(thisArg,_args);
    }
}

那么我们模拟实现bind完成了么? NO~! bind 还有一些隐藏特性~

作为构造函数使用的绑定函数

来自MDN的解释:
绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。

我们来用一个例子来解释:

function Point(x,y) {
  this.x = x;
  this.y = y;
}
Point.prototype.print = function() {
  console.log(this.x,this.y);
}
let NewPoint = Point.bind({},1,2)
new NewPoint().print(3,4); // print: 1 2 

从这个例子,我们可以看到,如果 bind() 返回的函数用作构造函数:

  • 忽略传入 bind() 的 this
  • 原始函数作为构造函数的形式被调用
  • 传入 bind() 时提供的参数,作为构造函数的参数

依照上面的规则,简单修改下我们的模拟代码:

Function.prototype.myBind = function(thisArg,...args) {
    let func = this;
    //最终返回的新函数
    let fBound = function(){
        //将 arguments 转换为 数组 并与 myBind 函数的 args 合并
        let _args = [...args,...Array.prototype.slice.call(arguments)];
        //当作为构造函数执行时,传递的this为当前实例,否则为传入的this
        let context = this instanceof fBound ? this : thisArg;
        return func.apply(context,_args);
    }
    //将新函数的原型链重置为原函数的原型链
    //作为构造函数执行时,返回的对象获取了原函数原型链上的所有属性及方法
    fBound.prototype = func.prototype;
    return fBound;
}

在上文中的这段代码:

fBound.prototype = func.prototype;
  • 其目的是模拟作为构造函数执行时,原函数作为构造函数被执行
  • 因为构造函数被执行后生成的对象从原函数中继承 prototype
  • 所以通过将新函数的 prototype 重置为原函数的 prototype

但这样做,引入了一个新的问题,如果我们修改 fBound.prototype 时,也会同时修改原函数的 prototype

所以我们考虑用一个空函数来进行中转:

Function.prototype.myBind = function(thisArg,...args) {
    let func = this;
    //最终返回的新函数
    let fBound = function(){
        //将 arguments 转换为 数组 并与 myBind 函数的 args 合并
        let _args = [...args,...Array.prototype.slice.call(arguments)];
        //当作为构造函数执行时,传递的this为当前实例,否则为传入的this
        let context = this instanceof fBound ? this : thisArg;
        return func.apply(context,_args);
    }
    //中转函数
    let fNOP = function(){}
    fNOP.prototype =func.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

最后一点小问题

我们知道对于函数来说,函数有一个属性 length 代表这个函数形参的个数

let fn = function(a,b,c,d) {};
console.log(fn.length); // print : 4

而对于 bind() 返回的函数对象的 length 属性是绑定函数的形参个数减去绑定实参的个数(length 不能小于零)

let fn = function(a,b,c,d) {};
let newFn = fn.bind(null,1,2);
console.log(newFn.length); // print: 2

简单实现:

Function.prototype.myBind = function(thisArg,...args) {
    let func = this;
    //最终返回的新函数
    let fBound = function(){
        //将 arguments 转换为 数组 并与 myBind 函数的 args 合并
        let _args = [...args,...Array.prototype.slice.call(arguments)];
        //当作为构造函数执行时,传递的this为当前实例,否则为传入的this
        let context = this instanceof fBound ? this : thisArg;
        return func.apply(context,_args);
    }
    //中转函数
    let fNOP = function(){}
    fNOP.prototype =func.prototype;
    fBound.prototype = new fNOP();
    //修改新函数的length属性
    fBound.length = Math.max(0,func.length - args.length)
    return fBound;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions