手写JS原生方法bind的过程分析

114 阅读4分钟

bind 的使用

在实现bind方法之前,首先了解完整的bind 方法是怎么工作的。
bind : 创建一个新的函数,在bind被调用时,这个新函数的this被指定bind的第一个参数而其余的参数将作为新函数的参数

var food = {
  value: 1,
  name: "banana",
}

function bar () {
  console.log('this@', this);
  console.log('vale,name:@', this.value, this.name);
}

// 用bind创建一个新的函数
var bindFood = bar.bind(food)
bindFood()

//ouput:
//this@ {value: 1, name: 'banana'}
//vale,name:@ 1 banana

实现步骤分析

版本1

1.获取调用bind 的上下文this
2.考虑到调用bind的函数可能有返回值,加上return

Function.prototype.myBind = function(context) {
  // 1.获取调用函数
  var self = this

  // 2.调用myBind函数可能有返回值
  return function() {

    // 3.将调用新函数的this,指向对应的参数
    return self.apply(context)
  }
}

测试 : mybind test

var food = {
  value: 1,
  name: 'banana',
};

function bar (name, age) {
      console.log('this.value@', this.value);
      console.log('name@', name);
      console.log('age@', age);
}

var bindFood = bar.myBind(food, '大明')
bindFood('23')

//ouput:
//this.value@ 1
//name@ undefined
//age@ undefined

通过对myBind测试得出,传进去的参数没得到过滤(处理),所有无值返回

版本2

为了解决版本1的问题,提出解决方法:
1.对传入参数 arguments 进行过滤
2.生成新函数被调用时,并传入参数时,也要进行过滤

Function.prototype.myBind2 = function(context) {
  // 1.获取调用myBind2的函数
  var self = this

  // 2.过滤调用myBind2时传入的参数
  var args = Array.prototype.slice.call(arguments, 1)

  // 3.调用myBind2的函数可能有返回值
  return function() {
    // 4.第二次过滤,对生成新函数被调用时,并传入的参数将进行过滤
    var bindArgs = Array.prototype.slice.call(arguments)

    // 5.合并第一,第二次过滤的arguments ,并通过apply改变this指向(让生成的新函数也可以调用传入参数的属性)
    return self.apply(context, args.concat(bindArgs))
  }
}

测试:mtBind2 test

var food2 = {
  value: 1,
  name: 'banana',
};

function bar2 (name, age) {
  console.log('this.value@', this.value);
  console.log('name@', name);
  console.log('age@', age);
}
var bindFood2 = bar2.myBind2(food2, '大明')
bindFood2('23') // 1 ,大明,23

//output:
//this.value@ 1
//name@ 大明
//age@ 23

虽然版本2的问题已得到解决,但当把bindFood2当成原函数, 通过 new 构造一个函数时,this的指向会出现失效问题。
例子:

bar2.prototype.color = 'RED'
var bindFood2 = bar2.bind(food2, '大明')
const obj = new bindFood2('18') // undefined , 大明 ,18
console.log('new构造的新函数@', obj.color); //undefined

通过例子不难发现, bar2.prototype.color 赋值后,new 构造并读取 color 属性时为undefined,说明使用new构造的新没有被返回,为了解决这个问题看下个版本。

版本3

解决版本2问题的方式:
判断是否有对myBind2生成的新函数使用 new ,有需要将实例赋值给返回函数,没有侧返回原实例。

Function.prototype.myBind3 = function() {
// 1.获取调用myBind3的函数
var self = this

// 2.过滤第一次,arguments参数
var args = Array.prototype.slice.call(arguments, 1)

// 3.返回函数对this过滤
var fBound = function() {
  // 4.第二次过滤,arguments参数(新函数调用时转递的参数)
  var bindArgs = Array.prototype.slice.call(arguments)

  // 5.对myBind3生成的新函数,使用 new 构造函数时,this已经指向调用myBind3的实例;
  // 所有:使用new构造一个myBind3生成的函数时,要将调用myBind3的实例this赋值给返回函数
  // 是否使用new构造 ? 是将this给它 : 不是侧将原上下文context给它(当作为普通函数时,this 指向 window)
  return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs))

}

// 6.修改返回函数的原型 prototype ,将获取到的this赋值给新返回函数的原型,使实例可以继承原函数中的属性
fBound.prototype = this.prototype
  // 7.返回最终函数
  return fBound
}

测试:mtBind3 test

 var food3 = {
  value: 3,
  name: 'banana',
};

function bar3 (name, age) {
  console.log('this.value@', this.value);
  console.log('name@', name);
  console.log('age@', age);
}
bar3.prototype.color = 'GREEN'
var bindFood3 = bar3.myBind3(food3, '果果')
const obj3 = new bindFood3('23') 
console.log('new构造的新函数@', obj3.color); 

//ouput:
//this.value@ undefined
//name@ 果果
//age@ 23
//new构造的新函数@ GREEN

通过3版本的优化,解决了版本二使用new 无效的问题。 但是通过测试结果来看,this.value 返回的是 undefined 这是什么原因呢?为了解决这个问题看下个版本

版本4

经过分析发现: 直接将 fBound.prototype = this.prototype 在修改 fBound.prototype 时可能会修改到绑定函数的 prototype
解决方式:通过第三个函数将,实例转接到新的函数

Function.prototype.myBind4 = function(context) {
  // 1.获取调用myBind4的实例
  var self = this

  // 2.过滤调用myBind4时传进来的参数
  var args = Array.prototype.slice.call(arguments, 1)

  // 4.定义转接this的第三者函数
  var fNOP = function() { }

  // 3.定义返回的新函数
  var fBound = function() {
    // 3.1 过滤调用新函数时传进来的参数
    var bindArgs = Array.prototype.slice.call(arguments)

    // 3.2 判断是否对新函数使用 new 构造
    return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs))
  }


  // 5.将实例原型赋值给第三者函数原型
  fNOP.prototype = this.prototype

  // 6.通关new将第三者得到的原型,赋值给新函数
  fBound.prototype = new fNOP()

  return fBound
}

测试:mtBind4 test

var food4 = {
  value: 4,
  name: 'banana',
};

function bar4 (name, age) {
  console.log('this.value@', this.value);
  console.log('name@', name);
  console.log('age@', age);
}
bar4.prototype.color = 'yellow'

// var bindFood4 = bar4.bind(food4, '东东')
// bindFood4('23')
//ouput:
//this.value@ 4
//name@ 东东
//age@ 23

var bindFood4 = bar4.myBind4(food4, '东东')
bindFood5('23')
//ouput:
//this.value@ 4
//name@ 东东
//age@ 23

//var bindFood4 = bar4.myBind4(food4, '东东')
// const obj4 = new bindFood5('23')
// console.log('new构造的新函数@', obj4.color);
//ouput:
//this.value@ undefined
//name@ 东东
//age@ 23
//new构造的新函数@ yellow

经过测试,原生的bind 和myBind4 对比以实现相似功能效果。

最终简洁版

Function.prototype.myBindFN = function(context) {
  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 fBound ? this : context, args.concat(bindArgs))
  }

  fNOP.prototype = this.prototype
  fBound.prototype = new fNOP()

  return fBound
}

总结

本文主要通过对bind使用功能分析的方式,一步步实现手写bind时可能遇到的问题。