bind 模拟实现——手写题

228 阅读6分钟

你好,我是南一。这是我在准备面试八股文的笔记,如果有发现错误或者可完善的地方,还请指正,万分感谢🌹

bind 模拟实现

MDN对bind参数的介绍

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

thisArg

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

arg1, arg2, ...

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

返回值

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

MDN对bind的介绍是这样的

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

返回函数模拟实现

基于这一点,我们利用apply,实现一个简单版bind

先看第一个例子

function func() {
  console.log(this.name);
}

var obj = {
  name: '南一'
}

var returnFunc = func.bind(obj)
returnFunc() // 南一
  • 调用bind返回一个新函数
  • this 被指定为 bind() 的第一个参数

代码实现第一步

Function.prototype.myBind = function (thisArg) {
  var self = this //保存原函数
  return function () {
    self.apply(thisArg)
  }
}
  • 考虑到绑定函数可能是有返回值的,看第二个例子
function func() {
  return this.name
}

var obj = {
  name: '南一'
}

var returnFunc1 = func.bind(obj)
console.log(returnFunc1()); // 南一

代码实现第二步

Function.prototype.myBind = function (thisArg) {
  var self = this //保存原函数
  return function () {
    return self.apply(thisArg)
  }
}

传参模拟实现

  • 传参数,调用bind方法传参,执行bind的返回函数也可以传参,第三个例子
function func(name, age) {
  console.log(name); // 北三
  console.log(age);  // 18
  console.log(this.name);  // 南一
}

var obj = {
  name: '南一'
}

var returnFunc1 = f·unc.bind(obj, '北三')
returnFunc1(18)

函数func需要两个参数,调用bind方法传入参数name,调用返回函数returnFunc1传入参数age。实际上是,传给被bindreturnFunc1的参数合并后传入func函数

代码实现第三步

  • 采用arguments对象,对参数进行合并

arguments对象

arguments对象是所有(非箭头)函数中都可用的局部变量。可以使用arguments对象在函数中引用函数的参数。只作用在当前函数作用域。

“类数组” 意味着 arguments长度 属性 并且属性的索引是从零开始的,但是它没有 Array的 内置方法

Function.prototype.myBind = function (thisArg) {
  var self = this //保存原函数
  //将类数组arguments转化成数组
  var args = Array.prototype.slice.call(arguments)
  //去掉数组第一个元素
  Array.prototype.shift.call(args)
  return function () {
    //将两部分参数合并
    return self.apply(thisArg, Array.prototype.concat(args, Array.prototype.slice.call(arguments)))
  }
}

这里频繁借用Array.prototype上面的方法,Array.prototype.slicearguments转化成数组, Array.prototype.shift去除数组第一个元素,Array.prototype.concat合并两个数组

构造函数效果的模拟实现

  • 一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

第四个例子

function func(name, age) {
  console.log(name);  // 北三
  console.log(age);   //  18
  console.log(this.name); // undefined
  this.skill = 'sing'
}

func.prototype.friend = 'mike'

var obj = {
  name: '南一'
}

var returnFunc1 = func.bind(obj, '北三')
var test = new returnFunc1(18)

console.log(test.friend); // mike
console.log(test.skill);  // sing

可以看到,函数func输出this.nameundefined,说明绑定的this失效了,此时的func函数内的this是指向new运算符创建的新对象test,所以test可以拿到skill的值和在func原型上的属性。

代码实现第四步

Function.prototype.myBind = function (thisArg) {
  var self = this //保存原函数
  //将类数组arguments转化成数组
  var args = Array.prototype.slice.call(arguments)
  //去掉数组第一个元素
  Array.prototype.shift.call(args)
  //这个bound就是调用bind返回的函数
  var bound = function () {
    //将传到myBind和bound的参数,转成数组合并起来
    var mergeArg = Array.prototype.concat(args, Array.prototype.slice.call(arguments))
    //这里判断是用new运算符调用bound函数,还是直接调用:
    //1、new调用的话, this会指向以bound为构造函数创建的新对象,所以用instanceof会找到新对象原型链上存在bound的 prototype 属性,就会是true
    //2、如果是直接调用,this默认指向全局对象
    if (this instanceof bound) {
      var result = self.apply(this, mergeArg);
      return result instanceof Object ? result : this;
    } else {
      //将两部分参数合并
      return self.apply(thisArg, mergeArg)
    }
  }
  // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
  bound.prototype = func.prototype
  return bound
}

经过测试实现了同样的输出结果,但是这样写是正确的吗?我们来看看这几处代码

提出问题

1、多余的模拟new操作

var result = self.apply(this, mergeArg);
return result instanceof Object ? result : this;

这两句本意是判断绑定函数(self)返回的值是否为对象,是就返回此对象,否则返回新建的对象(这里this就指向新建对象)。However,没有必要这样写,这里的bound是构造函数,在构造函数内哪里需要这一步操作呢?new运算符已经帮我们做好这一步了。这里我是看到一些博客这样写,我觉得不对。

//正确写法
return self.apply(this, mergeArg);

2、调用bind的不是函数怎么办?

报错处理!!!

if (typeof this !== 'function') {
    return new TypeError(this + ' is not a function')
}

3、实例原型可能被改动

// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
bound.prototype = func.prototype

这里如果func.prototype被人误改了,那bound.prototype也会跟着一起改变,为了防止被改变,可以用一个空函数来中转

function Empty() { }
Empty.prototype = self.prototype;
bound.prototype = new Empty();

4、绑定函数是箭头函数怎么办呢?

if (this instanceof bound) {
  //只有当用了new运算符,而且绑定函数为箭头函数,才需要抛出错误
  if (!self.prototype) {
    return new TypeError(`Arrow function expressions cannot be a constructor`)
  }
  return self.apply(this, mergeArg);
}

最终代码实现

Function.prototype.myBind = function (thisArg) {
  //1.如果调用bind不是函数,抛出TypeError
  if (typeof this !== 'function') {
    return new TypeError(this + ' is not a function')
  }
  var self = this //保存原函数
  //将类数组arguments转化成数组
  var args = Array.prototype.slice.call(arguments)
  //去掉数组第一个元素
  Array.prototype.shift.call(args)
  //这个bound就是调用bind返回的函数
  var bound = function () {
    //将传到myBind和bound的参数,转成数组合并起来
    var mergeArg = Array.prototype.concat(args, Array.prototype.slice.call(arguments))
    //这里判断是用new运算符调用bound函数,还是直接调用:
    //1、new调用的话, this会指向以bound为构造函数创建的新对象,所以用instanceof会找到新对象原型链上存在bound的 prototype 属性,就会是true
    //2、如果是直接调用,this默认指向全局对象
    if (this instanceof bound) {
      //只有当用了new运算符,而且绑定函数为箭头函数,才需要抛出错误
      if (!self.prototype) {
        return new TypeError(`Arrow function expressions cannot be a constructor`)
      }
      return self.apply(this, mergeArg);
    } else {
      //将两部分参数合并
      return self.apply(thisArg, mergeArg)
    }
  }

  //self可能是ES6的箭头函数,没有prototype,所以就没必要再指向做prototype操作
  if (self.prototype) {
    function Empty() { }
    Empty.prototype = self.prototype;
    bound.prototype = new Empty();
  }

  return bound
}

参考文章

MDN Function.prototype.bind

JavaScript深入之bind的模拟实现

面试官问:能否模拟实现JS的bind方法