手撸call函数

214 阅读6分钟

说明:本文只关注该函数的核心功能,不会过多处理边界情况(edge case)。

一、 原生 call 函数分析

call函数的基本使用这里不做赘述,想了解可以去看看MDN: Function.prototype.call() - JavaScript | MDN (mozilla.org)

1.1 原生 call 函数的特性

  1. 原生call函数,可以在任何的函数上调用
  2. 能调用对应的函数
  3. 原生call函数语法:function.call(thisArg, arg1, arg2, ...),分析可以得出第一个是需要绑定的this,后边是需要传递的参数
  4. 使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined

二、实现 call 函数

2.1 实现可以在任何函数上调用

对于可以在任何函数上调用,说明是共享的(继承),可以在Object.prototypeFunction.prototype上新增函数即可实现(因为原型链关系,如下图所示 ps:自己理解画的,如有不妥之处还请纠正)。

base.png 这里我们在Function.prototype上新增一个自己的call函数,我命名为_call(你随意~)。

Function.prototype._call = function(){
    console.log('_call')
}
// 测试函数
function foo(){
    console.log('foo执行了~')
}
// 测试一下,看能否调用
foo._call()

输出:

foo执行了~

第一步已经完成,我们已经实现了一个可以在任何函数上调用的_call方法,让我们继续!别停下!

2.2 能调用对应的函数

对于调用对应的函数,只要我们能拿到谁调用了_call,我们就能调用这这个调用者,那么怎么拿到谁调用了_call呢? 答案是:this。请看以下示例:

Function.prototype._call = function(){
  // 看看this是什么?
  console.log('this: ', this)
}
function foo() {
  console.log('foo执行了~')
}
foo._call()

输出:

image.png

因为我们这里写的是普通函数,而普通函数会绑定this,foo._call()的时候this就指向foo了,由此可知,this确实指向了我们的调用者,那么我们只需要this(),即可调用我们的调用者,接下来改造我们的_call

Function.prototype._call = function () {
  const fn = this // 这里的 fn 指向调用者 foo
  fn()
}
function foo() {
  console.log('foo执行了~')
}
foo._call()

输出:

image.png

看!我们已经成功的调用了我们的调用者!

2.3 参数

接收参数的话只需要_call(thisArg,...args)即可。

2.3.1 参数之绑定 this

接收了,怎么改变调用者的this呢?

还记的刚刚我们是怎么拿到调用者的吗?

我们在自己的函数内部 通过this关键字拿到了调用者,那么我们现在是要改变某个函数的this(不通过 call、apply、bind),是不是可以逆推上面的逻辑呢?只要在某个需要绑定的this上调用我们的fn函数,那么这个fn函数的this是不是就指向了我们需要绑定的this呢? 接下来试一试:

Function.prototype._call = function (thisArg, ...args) {
  // 1.获取需要执行的函数
  const fn = this // 这里的 fn 指向调用者 foo

  // 2.通过隐式绑定改变fn函数的this指向
  thisArg.fn = fn
  thisArg.fn()
  delete thisArg.fn // 调用完了之后就可删除了
}
function foo() {
  console.log('foo执行了~', this)
}
// { a: 'foo' } 是绑定的this
foo._call({ a: 'foo' })

输出:

image.png

可以发现打印的this确实指向了我们的{ a: 'foo' }对象,但是发现这个对象上多了一个fn方法?其实我们函数执行完了之后就已经删除了的,只是这里还在显示而已。

隐患:如果传入进来的这个this上边有fn方法的话,那么这里会产生覆盖问题(概率很小,但不排除),我们可以使用Symbol来创建一个独一无二的值,这样的话,就不会产生覆盖问题了。

Symbol改写:

Function.prototype._call = function (thisArg, ...args) {
  // 1.获取需要执行的函数
  const fn = this // 这里的 fn 指向调用者 foo

  // 2.通过隐式绑定改变fn函数的this指向
  // thisArg.fn = fn
  // thisArg.fn()
  // delete thisArg.fn
  const key = Symbol()
  thisArg[key] = fn
  thisArg[key]()
  delete thisArg[key]
}
function foo() {
  console.log('foo执行了~', this)
  
  // 隔一会再打印看看还有没有~
  setTimeout(() => {
    console.log(this)
  }, 1000)
}
// { a: 'foo' } 是绑定的this
foo._call({ a: 'foo' })

输出:

image.png

我们已经实现了改变函数的this指向,接下来看看传递其他参数应该怎么实现。

2.3.2 call方法的其他参数

在上面我们已经提到,可以通过_call(thisArg,...args)接收剩余参数,那么我们只需要在调用fn函数的时候,传递进去就可以了。

Function.prototype._call = function (thisArg, ...args) {
  // 1.获取需要执行的函数
  const fn = this // 这里的 fn 指向调用者 foo

  // 2.通过隐式绑定改变fn函数的this指向
  // thisArg.fn = fn
  // thisArg.fn()
  // delete thisArg.fn
  const key = Symbol()
  thisArg[key] = fn
  thisArg[key](...args)
  delete thisArg[key]
}
function foo(a, b, c, d) {
  console.log('foo执行了~', this)
  console.log('a, b, c, d: ', a, b, c, d)
}
// { a: 'foo' } 是绑定的this
foo._call({ a: 'foo' }, 1, 2, 3, 4)

输出:

image.png

其他参数的问题也已经解决了!

2.3.3 this参数的特殊处理

原生的call方法,在传入的第一个参数不是object的时候,是有特殊处理的,如下图所示:

原生call特性:

function foo() {
  console.log('this: ', this)
}

foo.call()
foo.call(null)
foo.call(undefined)
foo.call('str')
foo.call(123)
foo.call({})
foo.call([])

输出:

image.png

我们这里也来一一实现一下:

// 封装一个函数,用于判断一个值是否是 object 或 funciton,排除null(因为null不能添加任何属性,但是typeof null === 'object')
function isObject(value) {
  const type = typeof value
  return value !== null && (type === 'object' || type === 'function')
}

Function.prototype._call = function (thisArg, ...args) {
  // 1.获取需要执行的函数
  const fn = this // 这里的 fn 指向调用者 foo

  // 2.判断 this 是否是对象
  if (!isObject(thisArg)) {
    // 不是对象
    const type = typeof thisArg
    // null、undefined、或者为空的时候
    if (thisArg === null || type === 'undefined') {
      thisArg = window
    } else if (type === 'string' || type === 'number') {
      // 小技巧,Object函数,可以将一些基本数据类型包装成对象类型
      // Object(123) => Number {123}, Object('str') => String {'str'}
      thisArg = Object(thisArg)
    }
  }

  // 3.通过隐式绑定改变fn函数的this指向
  // thisArg.fn = fn
  // thisArg.fn()
  // delete thisArg.fn
  const s1 = Symbol()
  thisArg[s1] = fn
  thisArg[s1](...args)
  delete thisArg[s1]
}

function foo() {
  console.log('this: ', this)
}
// 测试
foo._call()
foo._call(null)
foo._call(undefined)
foo._call('str')
foo._call(123)
foo._call({})
foo._call([])

输出:

image.png

对照上图,可以发现,我们已经实现了call函数的核心功能。

2.4 返回值

这个就很简单了,接收一下再return就可以了。

function isObject(value) {
  const type = typeof value
  return value !== null && (type === 'object' || type === 'function')
}

Function.prototype._call = function (thisArg, ...args) {
  // 1.获取需要执行的函数
  const fn = this // 这里的 fn 指向调用者 foo

  // 2.判断 this 是否是对象

  if (!isObject(thisArg)) {
    const type = typeof thisArg
    if (thisArg === null || type === 'undefined') {
      thisArg = window
    } else if (type === 'string' || type === 'number') {
      thisArg = Object(thisArg)
    }
  }

  // 3.通过隐式绑定改变fn函数的this指向
  const s1 = Symbol()
  thisArg[s1] = fn
  const res = thisArg[s1](...args)
  delete thisArg[s1]
  return res
}

function foo(a, b, c, d) {
  console.log('this: ', this)
  console.log('a, b, c, d: ', a, b, c, d)
  return a + b + c + d
}

const result = foo._call({}, 1, 2, 3, 4)
console.log('result: ', result)

输出:

image.png

三、最终实现

整理一下我们函数,附上完整版

// 工具函数
function isObject(value) {
  const type = typeof value
  return value !== null && (type === 'object' || type === 'function')
}

Function.prototype._call = function (thisArg, ...args) {
  // 1.获取需要执行的函数
  const fn = this

  // 2.判断 this 是否是对象
  if (!isObject(thisArg)) {
    const type = typeof thisArg
    if (thisArg === null || type === 'undefined') {
      thisArg = window
    } else if (type === 'string' || type === 'number') {
      thisArg = Object(thisArg)
    }
  }

  // 3.通过隐式绑定改变fn函数的this指向
  const s1 = Symbol()
  thisArg[s1] = fn
  const res = thisArg[s1](...args)
  delete thisArg[s1]
  
  // 4.返回值
  return res
}