call、apply、bind 实现

126 阅读4分钟

call、apply、bind 实现

1. call 实现

首先我们要知道系统的 call 方法主要实现了什么

  • 执行了函数
  • 改变了 this 的指向

先看系统的call方法

function foo() {
  console.log("foo函数被执行", this);
}

function sum(num1, num2) {
  console.log("sum函数被执行", this);
  return num1 + num2
}

// 1. 传入对象
foo.call({}) 
// 2. 传入null/undefined
foo.call(null) // this自动替换为指向全局对象
// 3. 传入其他,字符串,数字,布尔值等等
foo.call("abc") // this 指向相应的对象

let res = sum.call("123", 10,20)
console.log(res) // 30

img0328_01.png

接下来我们开始实现自己的call(主要是实现思路,没有把所有的边缘条件考虑完全,但是基本都有)

1.1 让函数执行起来

Function.prototype.mycall = function() {
    let fn = this // this就是我们想要执行的函数 foo,把它保存到 fn
    // 接着让函数执行起来
    fn() // 但是这里是独立函数调用,this指向window
}

foo.mycall() // 这里相当于mycall隐式绑定了foo,因此 mycall中的this就是foo

2.2 显式绑定this

现在我们要绑定我们指定的this

  • 先看第一种:foo.mycall({name: 'hello'}),绑定一个对象
Function.prototype.mycall = function(thisArg) {
  let fn = this // this就是我们想要执行的函数 foo,把它保存到 fn
  // 接着调用这个函数
  // 我们的目的是让函数执行的时候绑定thisArg这个对象执行即thisArg.fn()(隐式绑定)
  // 但是现在 thisArg没有 fn 这个属性呀,怎么调用呢
  // 所以我们可以给thisArg添加一个属性 fn, 值就是我们想要执行的函数
  thisArg.fn = fn
  // 然后再调用
  thisArg.fn()
  // 但是这样我们不就让thisArg多出来一个属性fn了吗?
  // 没关系,函数执行完 删掉就好
  delete thisArg.fn
}
  • 如果我们传入的不是对象呢?那就不能给thisArg添加属性啦

    foo.mycall("123")

Function.prototype.mycall = function(thisArg) {
  let fn = this 
  // 所以要把 thisArg 转成对象类型(Object构造函数可以将给定的值包装为一个新对象)
  thisArg = Object(thisArg)
  thisArg.fn = fn
  thisArg.fn()
  delete thisArg.fn
}
  • 如果传入的是 null / undefined 呢
Function.prototype.mycall = function(thisArg) {
  let fn = this 
  // 如果thisArg传入的是 undefined / null, 应该让它指向全局对象
  // 所以我们要做一个判断
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
  thisArg.fn = fn
  thisArg.fn()
  delete thisArg.fn
}

2.3 接下来要考虑参数了

// rest运算符
// ...args 会把我们传入的参数列表组合到一个数组 [num1,num2]
// ...args 也可以展开数组, 相当于对数组的一个遍历
Function.prototype.mycall = function(thisArg, ...args) {
  let fn = this 
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
  thisArg.fn = fn
  // 把参数传入函数,并用 result 接收函数执行完的结果(例子中就是sum函数执行完的结果)
  let result = thisArg.fn(...args) // ...[num1, num2] --> num1, num2
  delete thisArg.fn
  // 最后把结果返回出去
  return result
}

到此,基本的call就已经实现了

检验一下叭

foo.mycall({name: 'hello'})
foo.mycall("123")
foo.mycall(undefined)
foo.mycall(null)

sum.mycall({name: "hello"}, 10, 20)
let res1 = sum.mycall(123, 10, 20)
console.log(res1);

image0328_02.png style="zoom:70%;"

2. apply 实现

跟 call 类似,只不过参数的处理有不同

Function.prototype.myapply = function(thisArg, argsArray) {
  let fn = this

  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window

  thisArg.fn = fn
  // 没有传argArray,那么argsArray默认为 undefined,那么就不能使用展开运算符了
  // 所以如果没有传入参数, 那么把它初始化为 [], 空数组也是可以展开的 ...[]
  argsArray = argsArray || []
  let result = thisArg.fn(...argsArray)

  delete thisArg.fn
  return result
}

3. bind 实现

bind 需要我们返回一个新的函数,并且调用 bind 的时候不需要执行函数

Function.prototype.mybind = function(thisArg, ...args) {

  var fn = this

  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window

  function newFn() {
    thisArg.fn = fn
    let result = thisArg.fn(...args)
    delete thisArg.fn
    // 返回结果
    return result
  }

  // bind 需要返回一个新的函数 你也可以直接 return function() {...}
  return newFn
}

大体上也差不多,但我们可以就下面这种情况改进一下

function sum2(num1, num2, num3, num4) {
  console.log("sum2函数被执行", this);
  return num1 + num2 + num3 + num4
}

let newSum2 = sum2.mybind("abc", 10,20) // 绑定的时候传入了两个参数
console.log(newSum2(30,40)); // 使用新返回的函数的时候再传入剩余的参数

这种情况,我们就需要把两次传入的参数合并起来,再调用

Function.prototype.mybind = function(thisArg, ...args) {
  var fn = this

  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window

  function newFn(...newArgs) {
    // 合并传入的参数
    let allArgs = [...args, ...newArgs]

    thisArg.fn = fn
    let result = thisArg.fn(...allArgs)
    delete thisArg.fn
    // 返回结果
    return result
  }
  return newFn
}

到此,基本的bind也实现了

最后说明一下,实现的思路是这样,但是方法不唯一的,可能还有一些边边角角没有考虑到的话,可以自己添加进去