手写apply、call、bind

1,242 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情

前言

手写apply、call、bind是面试的经典题,在项目中也会经常遇到。所以这篇文章通过自己实现的方式加深对方法的理解和使用。

call()

  • call函数

    • 接收三个参数,需要执行的函数、函数运行时this指向的对象、函数运行时参数(可以是多个)

    • 功能的实现:如何去改变this的指向然后指向函数呢?并不容易但是可以变相实现效果:

      1. obj对象添加临时方法 obj.temp = Fn 此时obj身上有fn一样的方法,就可以不需要调用fn而调用obj身上的方法,达到调用fn一样的效果,并且temp方法在执行时this指向是obj的,所以就变相实现了this指向obj的效果
      2. 调用temp方法 let result = obj.temp(...args) 这里还需要把参数放到temp
      3. 此时我们的obj身上还要temp这个临时方法,需要做一个复原。删除temp方法,通过delete关键字 delete obj.temp
      4. 返回执行结果 return result
      5. 观察一下js中的call方法,我们需要和它更相似,就需要判断一下是否指向全局。如果obj为undefinenullthis指向全局对象。注意点:这里的全局对象指向谁呢?window,但是在node中就不是window了而是global。此时的解决方法是使用globalThis,这是es11新推出来的新特性,用它指向全局对象。if(obj===undefined || obj===null){obj=globalThis}
    • call函数测试

      目的:执行add,并且改变运行时addthis指向为obj

      1. 新建html文件,引入call.js,添加script标签
      2. 声明一个函数,add(a,b){console.log(this);return a+b+this.c;}
      3. 声明一个对象,obj={c:2}
      4. 添加全局属性,window.c=3
      5. 指向call函数。call(add,obj,10,20)// 结果为 30+2=32

    call.js

    function call(Fn, obj, ...args) {
      // 判断是否指向全局
      if (obj === undefined || obj === null) {
        obj = globalThis
      }
      // 为obj添加临时方法
      obj.temp = Fn
      // 调用obj临时方法
      let result = obj.temp(...args)
      // 删除临时方法
      delete obj.temp
      // 返回执行结果
      return result
    }
    

    call.html

    <!DOCTYPE html>
    <html lang="en"><head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
      <script src="./call.js"></script>
    </head><body>
      <script>
        // 声明一个函数
        function add(a, b) {
          console.log(this)
          return a + b + this.c
        }
        // 声明一个对象
        let obj = {
          c: 2
        }
        // 添加全局属性
        window.c = 3
        // 执行call函数
        console.log(call(add, obj, 10, 20)) // 32 此时函数add中this的指向是obj
        console.log(call(add, null, 10, 20)) // 33 此时函数add中this的指向是window
      </script>
    </body></html>
    

2 apply()

与call函数一样。区别:函数运行时的参数,applay是以数组形式传递参数

实现:

  • 接收参数,第一个函数、第二个为this指向的对象、第三个函数运行的实参(不需要...args了)

  • 功能实现:

    1. 为obj添加临时方法
    2. 执行方法。参数需要调整,通过...args,因为传过来是数组,所以需要扩展运算符展开
    3. 删除临时属性
    4. 返回结果
    5. 判断是否指向全局和 call一样
  • 总结:几乎和call一样,除了接收的参数不同

测试:

  • 测试方法和call一样,就是传入的时候是一个数组

apply.js

function apply(Fn, obj, args) {
  // 判断是否指向全局
  if (obj === undefined || obj === null) {
    obj = globalThis
  }
  // 创建临时方法
  obj.temp = Fn
  // 调用临时方法
  let result = obj.temp(...args)
  // 删除临时方法
  delete obj.temp
  // 返回执行结果
  return result
}

apply.html

<!DOCTYPE html>
<html lang="en"><head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <script src="./apply.js"></script>
</head><body>
  <script>
    function add(a, b) {
      console.log(this)
      return a + b + this.c
    }
    let obj = {
      c: 2
    }
    window.c = 3
    console.log(apply(add, obj, [10, 20]))
    console.log(apply(add, null, [10, 20]))
  </script>
</body></html>

3 bind()

与call很像,call会执行函数,bind不会执行函数。接收参数都是一样的

实现

  • 结构:第一参数函数、第二参数对象、第三...args

  • 功能实现:需要实现两个形式:一种和call类似、一种执行函数时传递参数

    1. 返回一个新的函数。这个函数的作用是调用目标函数,并且改变this的指向。这里的效果其实就是与call一样的,可以直接指向call函数。return function(){return call(Fn,obj,...args)},这里需要引入call文件。到这里只能实现第一种形式。如需实现第二种形式需要继续传参
    2. 返回一个新的函数时,传递...args2参数。在调用call函数时,将...arg2拼接到最后面,因为在实际使用bind时,调用函数传递新参数都会再最后面拼接。return function(){return call(Fn,obj,...args,...args2)}

测试

说明:执行add,并且改变运行时addthis指向为obj,这里有两种实现方式。

  1. 新建html文件,引入call.js,添加script标签

  2. 声明一个函数,add(a,b){console.log(this);return a+b+this.c;}

  3. 声明一个对象,obj={c:2}

  4. 添加全局属性,window.c=3

  5. 执行函数:

    • let fn = bind(add,obj,10,20); console.log(fn());
    • let fn2 = bind(add,obj); console.log(fn2(10,20));
    • let fn3 = bind(add,obj,10,20); console.log(fn(30,40));

bind.js

function bind(Fn, obj, ...args) {
  // 返回一个新的函数
  return function (...args2) {
    // 执行call函数
    return call(Fn, obj, ...args, ...args2)
  }
}

bind.html

<!DOCTYPE html>
<html lang="en"><head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <script src="./call.js"></script>
  <script src="./bind.js"></script>
</head><body>
  <script>
    // 声明一个函数
    function add(a, b) {
      console.log(this)
      return a + b + this.c
    }
    // 声明一个对象
    let obj = {
      c: 2
    }
    // 添加全局属性
    window.c = 3
    // 定义bind函数
    let fn1 = bind(add, obj, 10, 20)
    let fn2 = bind(add, obj)
    let fn3 = add.bind(obj, 10, 20)
    // 执行bind函数
    console.log(fn1()) // 32 
    console.log(fn2(30, 40)) // 72 
    console.log(fn3(50, 60)) // 32 此时运算时只是跟前面的实参有关系与这里的无关。
  </script>
</body></html>