js call、apply、bind的实现

2,831 阅读5分钟

三个函数其实是老生常谈了,网上也有太多关于他们的实现。开始只是理解别人的实现,其实是似懂非懂。只有自己实现出来,并且逐行理解,才是真的懂了。 文章从三个函数的使用入手,结合使用场景逐步实现。

call的使用

window.name = 'window'
var obj = {
 name: 'obj'
}
function getName(p1, p2) {
 console.log(p1, p2)
 console.log(this.name)
}
getName('str1', 'str2') 
getName.call(obj, 'str1', 'str2') 

// 函数运行结果可以思考一下。

怎么记住call呢,其实就是getName这个方法执行了,不是window去执行,是call括号内的参数去执行。

可以联想到一个很典型的使用场景 [].prototype.shift.call(arguments),起初很不理解这种写法,后来一想,其实就是 arguments 不是数组,它没有shift方法可以直接来用,就把数组的shift方法拿来用。

call的实现

1 明确是谁调用call,答案,是函数。

2 call接收的参数是什么?第一个参数是要改变的this指针,也就是上面说到了,是谁去执行这个函数。若无指定,默认为window

3 call接收的第二个,第三个,等等,参数,是用来做什么的?答,就是作为调用call的那个函数所需的参数。

function myCall(context) {
  // 1
  if (typeof this !== 'function'){
	throw new TypeError('error')
  }
  // 2
  context = context || window
  // 3
  context.fn = this
  // 4
  const args = [...arguments].slice(1)
  // 5
  const result = context.fn(...args)
  // 6
  delete context.fn
  return result
}
Function.prototype.myCall = myCall

getName.myCall(obj, 'str1', 'str2')
  • 1 在myCall方法实现体中的this是什么? 回想一下,谁调用函数,this就是谁。那谁来调用call方法呢,是函数,所以这个this,就是调用call方法的函数,对于本例子来说,就是getName。如果不是函数,直接报错。
  • 2 myCall方法如果没有参数,那么默认为window
  • 3 本例中的context是传进来的obj对象,给这个对象添加一个方法,这个方法,就是this,也就是getName
  • 4 获取参数 ,在本例中,对应的是 'str1', 'str2'
  • 5 结果很明确了,就是 fn是方法,是getName。context是传进来的obj对象,那么就是实现了,obj调用getName方法,参数为'str1', 'str2'。(谁调用方法,this就指向谁)
  • 6 删除对象上函数,返回结果

apply的使用

apply使用与call大体一致,只是接受参数的方法不同。call可以接收多个参数。apply接收的第一个参数是this,第二个参数是 所需参数所组成的数组

window.name = 'window'
var obj = {
 name: 'obj'
}
function getName(p1, p2) {
  console.log(p1, p2)
  console.log(this.name)
}
getName('str1', 'str2') 
getName.apply(obj, ['str1', 'str2'])

apply的实现

Function.prototype.myApply = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  var result
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}

和上述的call实现基本类似,就参数处理有些不同,不再赘述。

bind的使用

为什么有call apply后还要有个bind ? 当我们需要绑定一个点击事件的时候,就改变回调函数的this,怎么破?因为 call apply都是立即执行了,所以bind登场。看一下下面这个例子吧。

var obj = {
  name: 'obj'
}

document.addEventListener('click',myClick.bind(obj,'p1','p2'),false);

function myClick(p1,p2){
  console.log(this.name, p1, p2)
}

MDN的解释是:bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

注意:bind方法的返回值是函数

bind的实现

bind()最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的this值。

因为bind返回值是函数,那么函数除了直接运行之外,还可以作为构造函数放在new 操作符之后,所以bind的实现就要把这种情况考虑进去。

前置知识点 new

任何函数都可以作为构造函数,放在 new 之后使用,那么new的过程是怎么样的呢?大体分为以下几步,具体不深究。

  1. 生成空对象
  2. 空对象的原型属性指向构造函数的原型对象
  3. 给这个空对象添加属性
  4. 返回这个对象。

上面所说的那个空对象就是构造函数内部的this,并且 对于 new 的情况来说,不会被任何方式改变 this

window.name = 'window'
var obj = {
  name: 'obj'
}
function Fun(p1,p2){
  console.log(this)
  console.log(this.__proto__ === Fun.prototype)
  console.log(this.name)
  this.a = p1
  this.b = p2
  console.log(this)
}
var c = new Fun('str1', 'str2')
console.log(c)

运行结果如下:

再来看一下,直接执行Fun的返回结果,代码不做修改,直接执行 Fun('str1', 'str2')

两次结果的不同,本文不展开叙述。那么举这个例子是想说明什么呢? 答 : 函数充当构造函数和普通函数,运行时内部的this指向不同。 接下来看bind函数的模拟实现

Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
const _this = this
const args = [...arguments].slice(1)
// 返回函数
return function F() {
  // 1 判断是否用作构造函数
  if (this instanceof F) {
    return new _this(...args, ...arguments)
  }
  // 2 用作普通函数
  return _this.apply(context, args.concat(...arguments))
 }
}
// 还是用上述举例子
window.name = 'window'
var obj = {
  name: 'obj'
}
function Fun(p1, p2){
  this.a = p1
  this.b = p2
  console.log(this.name)
  console.log(p1, p2)
}
var f1 = Fun.bind(obj, 'str1')
f1('str2')

运行结果如下,可见 改变了fn1函数的this指向

再来看一下 ,去掉 f1('str2') ,换成如下语句的运行结果

// f1('str2')
var b = new f1('str2')
console.log(b)

没有逐行的说明,不过对于内部实现,有new的前置介绍和注释,相信开始的call实现代码都看懂了的话,这个bind方法也会一目了然