面试官问你:请实现bind完整功能

1,138 阅读8分钟

由于js中this的存在,bind显得尤其重要,它能够显式强绑定this到某特定环境中,用过react的朋友应该知道bind在代码中出现的频率,在绑定函数方法时常用bind来绑定上下文对象到方法上。因此很多面试中常常能看到实现bind方法的面试题,不了解的同学们还以为是啥高深的问题,实际上理解的bind的几个功能点,写出来是不难的,下面来详细探讨一下啦~~

bind具体用法:

下面是MDN里对bind的解释:

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

语法:

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

参数解释:

thisArg

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

arg1, arg2, ...

  • 当目标函数被调用时,被预置入绑定函数的参数列表中的参数

举个栗子:

var value = 2;

var foo = { value: 1 };

function bar(name, age) { return { value: this.value, name: name, age: age } };

bar.call(foo, "Jack", 20); // 直接执行了函数 // {value: 1, name: "Jack", age: 20}

var bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数 bindFoo1(); // {value: 1, name: "Jack", age: 20}

var bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数 bindFoo2(20); // {value: 1, name: "Jack", age: 20}

callapply也有绑定this的功能,但区别是callaplly是直接执行,而bind是返回一个全新函数,这个函数里面的this是你指定的环境,如上面bar函数指定了thisfoo的上下文,即相当于指定了barfoo的一个方法。

总的来说,结合上面的栗子以及文档的说法,bind具有以下几个特性,我们逐一来实现就好:

  • 可以指定函数执行的this
  • 返回一个新的函数
  • 可以传入参数
  • 实现柯里化

上面提到的柯里化我觉得有必要提一下,因为不熟悉柯里化的同学看到这么拗口的词就要关掉这个页面愤懑而去了,其实柯里化是个唬人的名字而已,你叫他参数简单化也行啊,来这里先让你明白简单化

柯里化

我的理解:柯里化的功能是函数里面返回一个函数,先传递一部分参数给函数来调用它,然后返回一个函数去处理剩余得参数

先看这个栗子:

var add = function(x) { //先传入函数x
  return function(y) { //返回的函数处理剩下的参数 y
    return x + y; //最终的结果是由两次传入的参数金额的
  };
};

var increment = add(1); var addTen = add(10); increment(2);// 3 increment(6);// 7 addTen(2);// 12 add(1)(2);// 3 一次性调用

这里定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 之后,返回的函数就通过闭包的方式记住了 add 的第一个参数

有些场景下,我们需要频繁调用某函数,假如该函数带有很多参数如:

function add(x1,x2,x3,x4, y1,y2) {
  return x1+x2+x3+x5+y1+y2
}
//调用
add(1,2,3,4,45,90)
add(1,2,3,4,3453,50)
add(1,2,3,4,535,3450)

各位注意到没有,前面4个参数基本都是一样的,只有后面两个不停的变,我们完全没必要每次都重复传入参数,借鉴柯里化的思想,我们可以写成这样:

function add(x1,x2,x3,x4) {
  return function(y1, y2) {
     return x1+x2+x3+x5+y1+y2
  }
}
//调用
const add_ = add(1,2,3,4)
add_(45,90)
add_(3453,50)
add_(535,3450)

对比前后两种调方式,聪明的你一定知道柯里化的好处了,合理利用柯里化,让你少写更少的代码,代码更具结构性。

有小伙伴就疑惑了,我搞个bind绑定关柯里化啥事啊,一套一套的。不要着急,你看,bind原本的功能是我绑定的时候可以传进几个参数,例如

var bindFoo1 = bar.bind(foo,12,45)
bindFoo1(5,88,5) //调用

那最终的参数就算按顺序2,45,5,88,5了呀,是不是很熟悉?没错,无形中bind也实现了柯里化了啊

bind实现

好了,扯到这,大家已经很轻松的开干了,按照我们的功能点来实现,那是相当的清爽:

  • 可以指定函数执行的this
  • 返回一个新的函数
  • 可以传入参数
  • 实现柯里化

首先:

如果是前面两个功能点,我闭着眼睛就能写:

Function.prototype.bind_ = function(ctx) {
  const self = this //暂存this
  return function() { //返回新函数
    return self.apply(ctx) 指定函数执行this
  }
}

简单版的就是这样,实现了前面两个功能,继续再往下面完善

Function.prototype.bind_ = function(ctx) {
  const self = this //暂存this
  const args_1 =  Array.prototype.slice.call(arguments,1) //由于bind的参数第一个是this对象,第二个开始才是真正的参数,所以从位置1开始分割参数
  return function() { 
    const args_2 = Array.prototype.slice.call(arguments) //这是执行bind返回的新函数中输入的参数,如var c  = a.bind(b,78), 执行c(10,23),则参数10,23是这里所求 
    return self.apply(ctx,args_1.concat(args_2))
  }
}

验证一下:

var foo ={quota:45}

function calculate(x,y) { console.log(this.quota + x + y) }

var objCal = calculate.bind_(foo,20)

objCal(30)

//输出95,妥啊

这样看起来好像没啥问题了,但是细看文档里还有这样的一段话:

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

天啦噜,就是说这个bind返回的函数如果给用去new个实例也是可以的,只不过会忽略绑定的this信息,但参数还是有用的。

就拿上面的举例:

var foo ={quota:45}
function calculate(x,y) {
    console.log(this.quota + x + y)
}

foo.protype.name = 'foo---la' var objCal = calculate.bind_(foo,20)

var newObjCal = new calculate(30) //输出 NaN, 因定的this丢失了,执行new时 函数内部的this指向新生成的对象,故this.quota为undefined, undefined + 20 + 30当然为NaN

newObjCal.name// 输出foo---la

这下懂了吧,反正把bind返回的函数当作构造函数的话就忽略指定的this,当成正常的构造函数就行了。

清楚了需求,现在把我们模拟实现的bind功能来补全一下

Function.prototype.bind_ = function(ctx) {
  const self = this //暂存this
  const args_1 =  Array.prototype.slice.call(arguments,1)
  function newBind() { 
    const args_2 = Array.prototype.slice.call(arguments)
    const reallyCtx = this instanceof newBind ? this : ctx   //这里是拿真正要指定的this, 如果当前this的原型链上有newBind那证明就是new了bind返回的函数,否则就是正常的绑定调用
    return self.apply(reallyCtx,args_1.concat(args_2))
  }
  newBind.prototype=this.prototype //这里是把构造函数的原型覆盖掉newBind的原型
  return newBind
}

验证一下:

var foo = {name:122}
function bar() {
    console.log(this.name)
    this.age = 678
}
var objBind = Hi.bind(foo)
objBind()// 输出122
var newObj = new() //输出undefined
newObj.age //输出678

但是这样有个弊端啊,你看上面的 newBind.prototype=this.prototype这个原型赋值,那万一我改个newBind的原型那么新建的实例newObj也会变啊 来看下试着给bar原型加个属性,bar.prototype.sex = 123 然后看下 newObj.sex //输出123 看果然是给影响到了啊,针对这种情况,有两者比较通用的做法,

  • 一是通过Object.assign(this.prototype) 的形式赋值给返回函数的原型,但这与bind一样属于ES5功能,IE9一下的不支持
  • 二是通过创建一个新的构造函数作为中介,先把this.prototype赋值给新构造函数的原型,再把实例化的中介实例赋给返回函数的原型即可
Function.prototype.bind_ = function(ctx) {
  const self = this //暂存this
  const args_1 =  Array.prototype.slice.call(arguments,1)
  const newFun = function() {}
  function newBind() { 
    const args_2 = Array.prototype.slice.call(arguments)
    const reallyCtx = this instanceof newFun ? this : ctx  //记住这里是判断this原型是否为中介函数
    return self.apply(reallyCtx,args_1.concat(args_2))
  }
  newFun.prototype = this.prototype //把this的原型给到newFun的原型
  newBind.prototype = new newFun() //这里返回新对象作为newBind的原型,由于new出来的内存是新开辟的,后续修改调用bind的函数的原型不会影响到实例
  return newBind

最终版:

Function.prototype.bind_ = function(ctx) {
  const self = this //暂存this
  const args_1 =  Array.prototype.slice.call(arguments,1)
  const newFun = function() {}
  function newBind() { 
    const args_2 = Array.prototype.slice.call(arguments)
    const reallyCtx = this instanceof newFun ? this : ctx
    return self.apply(reallyCtx,args_1.concat(args_2))
  }
  newFun.prototype = this.prototype
  newBind.prototype = new newFun()
  return newBind

妙啊

结语

经过这次详细的分析以及分步实现,让我们清楚认识到bind自身的功能以及实现方式。这也是我真正开始对bind的一次全面梳理,确实对bind熟悉了不少,之前一直都是在用而不知其为何如此,溯源真是解惑的好方法啊哈哈哈,希望经过这次学习梳理也能帮助大家,有不对的地方大家也可以在评论区纠错噢~~~

参考

本文使用 mdnice 排版