• 分不清bind、apply、call?手写实现一下就明白了


    bind、call和apply都是Function原型链上面的方法,因此不管是使用function声明的函数,还是箭头函数都可以直接调用。这三个函数在使用时都可以改变this指向,本文就带你看看如何实现bind、call和apply。

    bind、call和apply的用法

    bind

    bind()方法可以被函数对象调用,并返回一个新创建的函数。

    语法:

    function.bind(thisArg[, arg1[, arg2[, ...]]]) 
    
    • 1

    bind()会将第一个参数作为新函数的this,如果未传入参数列表,或者第一个参数是nullundefined,那么新函数的this将会是该函数执行作用域的this。使用bind()应注意以下事项:

    • 返回一个新的函数,但是不会立即执行该函数
    • 根据传入的参数列表绑定this指向,如果未传入thisArg,那么需要明确this的指向
    • 如果是箭头函数,无法改变this,只能改变参数,这一点我们在这些情况下不建议你使用箭头函数也讲到过

    举个例子:

    正常使用

    function fn(a) {console.log(this, a)
    }
    const fn1 = fn.bind({x: 100}); // fn1是一个函数,但是并没有立即执行
    fn1(); // {x:100} 100
    console.log(fn === fn1); // false,bind返回的是一个新的函数 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    箭头函数

    const fn = (a) => {console.log(this, a);
    }
    const fn1 = fn.bind({x: 100}, 100); // 返回一个新的函数fn1,不会执行
    fn1(); // window,100 箭头函数通过bind返回的函数无法修改其this指向 
    
    • 1
    • 2
    • 3
    • 4

    未绑定this,或绑定到null、undefined

    const fn = (a) => {console.log(this, a);
    }
    const fn1 = fn.bind(); // 未绑定
    const fn2 = fn.bind(null); // 绑定null
    const fn3 = fn.bind(undefined); // 绑定undefined
    fn1(); // 绑定到执行作用域,默认为window
    fn2(); // 绑定到执行作用域,默认为window
    fn3(); // 绑定到执行作用域,默认为window 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    call&apply

    bind不同,callapply都是用来执行函数的,可以解决执行的函数的this指向问题。

    语法

    function.call(thisArg, arg1, arg2, ...)
    function.apply(thisArg, argsArray) 
    
    • 1
    • 2

    call的参数列表是可选的,如果传入的thisArgnull或者undefined,那么会自动替换为全局对象;如果是传入的原始值,则会替换为原始值对应的包装类型。apply的用法和call类似,不同点在于其额外传入的参数是一个数组或类数组对象,而call的额外参数是不确定参数。

    举个栗子:

    function fn(a, b) {console.log(this, a, b);
    }
    fn.call({x: 100}, 10, 20); // {x: 100} 10 20
    fn.apply({x: 100}, [10, 20]); // {x: 100} 10 20 
    
    • 1
    • 2
    • 3
    • 4

    callapply无法修改箭头函数的this指向:

    const fn = (a, b) => {console.log(this, a, b);
    }
    fn.call({x: 100}, 10, 20); // Window 10 20
    fn.apply({x: 100}, [10, 20]); // Window 10 20 
    
    • 1
    • 2
    • 3
    • 4

    简单回顾了以下bind、call、apply的使用,接下来就看看应该如何来实现。

    实现bind

    根据我们刚刚使用的bind(),在设计时需要如下考虑:

    • 最终返回的是一个新的函数,可通过function来声明
    • 需要绑定新函数的this
    • 需要绑定运行时的参数,可通过apply或call来实现

    实现代码

    // 通过原型链注册方法
    // context:传递的上下文this;bindArgs表示需要绑定的额外参数
    Function.prototype.newBind = function (context, ...bindArgs) {
      const self = this; // 当前调用bind的函数对象
    
      // 返回的函数本身也是可以再传入参数的
      return function (...args) {
          // 拼接参数
          const newArgs = bindArgs.concat(args);
          return self.apply(context, newArgs)
      }
    }
    function fn(a,b) {console.log(this, a, b);
    }
    const fn1 = fn.newBind({x: 100}, 10);
    fn1(20); // {x: 100} 10 20 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    bind()返回的是一个新函数,执行新函数就相当于是通过callapply来调用原函数,并传入this和参数。

    实现call和apply

    在实现bind的过程中,我们使用了apply来完成this的绑定,那么要实现apply又应该用什么来绑定this呢?可能会有小机灵鬼发现,好像在apply中使用call,在call中使用apply也可以完成this绑定。这不就形成了嵌套嘛,不是我们最终想要的。

    我们先来

    call和apply的应用:

    • bind返回一个新的函数,并不会执行;call和apply会立即执行函数
    • 绑定this
    • 传入执行参数

    举个栗子:

    function fn(a, b) {console.log(this, a, b);
    }
    fn.call({x: 100}, 10, 20); // {x: 100} 10 20
    fn.apply({x: 100}, [10, 20]); // {x: 100} 10 20 
    
    • 1
    • 2
    • 3
    • 4

    call和apply的实现效果是一样的,都是立即执行函数,不同的是call需要传入单个或者多个参数,apply可以传入一个参数数组。

    如何在函数执行时绑定this:

    • const obj = {x: 100, fn() {this.x}}
    • 执行obj.fn(),此时fn()内部的this指向的就是obj
    • 可以借此实现函数绑定this

    使用过Vue的朋友都知道,Vue实例其实就是一个对象,其里面的方法在调用时,this就会指向当前对象。举个栗子:

    let obj = {
      key: 'key',
      getKey: () => {
          return this.key;
      },
      getKey2() {
          return this.key;
      }
    };
    obj.getKey(); // this指向window,返回值取决于window中是否有对应的属性
    obj.getKey2(); // this指向obj,返回 'key' 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个例子在这些情况下不建议你使用箭头函数也是有提及的,感兴趣的朋友可以去看看。根据此原理,我们就可以来尝试给函数绑定this了:某函数调用apply,那么我们就将这个函数添加到传入的this对象中(如果未传入则this为全局对象,如果传入的是原始值,则使用其包装类型),然后使用()来执行函数,这个时候函数的this指向的就是我们传入的this了。

    实现代码:

    Function.prototype.newCall = function(context, ...args) {
      if (context == null) context = globalThis; // 如果传入的上下文是null或者undefined,则使用全局globalThis,一般指向的就是window
      if (typeof context !== 'object') context = new Object(context); // 如果是原始类型(数字、字符串、布尔值等),则使用其包装类型
    
      const fnKey = Symbol(); // 使用Symbol可确保key值不会重复,避免属性覆盖
      context[fnKey] = this; // this指向的是当前调用newCall的函数
    
      console.log(context[fnKey]); // 打印当前函数以及上下文this
      console.log(context);
    
      const res = context[fnKey](...args); // 执行函数,函数的this指向为context
      delete context[fnKey]; // 删除fn,防止污染
    
      return res; // 返回结果
    }
    fn.newCall({x: 100}, 10, 20); // {x: 100} 10 20
    function fn(a,b) {console.log(this, a, b);
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这样我们就实现了call,那么apply实现类似,只不过传入的额外参数要变成数组或类数组的方式

    Function.prototype.newCall = function(context, args) {
      if (context == null) context = globalThis; // 如果传入的上下文是null或者undefined,则使用全局globalThis,一般指向的就是window
      if (typeof context !== 'object') context = new Object(context); // 如果是原始类型(数字、字符串、布尔值等),则使用其包装类型
    
      const fnKey = Symbol(); // 使用Symbol可确保key值不会重复,避免属性覆盖
      context[fnKey] = this; // this指向的是当前调用newCall的函数
    
      console.log(context[fnKey]); // 打印当前函数以及上下文this
      console.log(context);
    
      const res = context[fnKey](...args); // 执行函数,函数的this指向为context
      delete context[fnKey]; // 删除fn,防止污染
    
      return res; // 返回结果
    }
    fn.newCall({x: 100}, 10, 20); // {x: 100} 10 20
    function fn(a,b) {console.log(this, a, b);
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    注意打印的当前函数以及上下文:

    实现callapplybind有很大的不同就是如何来处理this绑定。

    总结

    学会了如何实现bind、call和apply,对于理解如何使用,以及如何避免潜在的错误有很大的帮助。特别是callapply,我们在实现的时候借助于对象内部的非箭头函数,其this指向对象自身这一基础知识,实现了this绑定。如果还未搞清楚的朋友,可以将代码运行起来看看,也许能帮助你更好的理解。

  • 相关阅读:
    人工智能、深度学习、机器学习常见面试题301~320
    分类问题常用算法之决策树、随机森林及python实现
    图解LeetCode——1224. 最大相等频率(难度:困难)
    kubernetes 通过HostAliases属性配置域名解析
    东方博宜OJ——1004 - 【入门】编程求1*2*3*...*n
    JavaScript 函数详解
    2、TypeScript常见数据类型
    Windows平台下C++五子棋项目实战开发
    Java14新增特性
    如何同时打开两个pycharm文件
  • 原文地址:https://blog.csdn.net/qq_53225741/article/details/126561981