• call\apply\bind详解


    call\apply\bind

    call,apply,bind都是可以改变普通函数的this指向(不能改变箭头函数的this指向)的方法,这三个函数实际上都是绑定在Function构造函数的prototype上,而每一个函数都是Function的实例,因此每一个函数都可以直接调用call\apply\bind

    call\apply\bind区别

    1. call的第一个参数是this要指向的对象,并且第二个参数开始,可以接收多个参数作为改造后的函数参数,使用call方法,改造后的函数会立即执行。call的返回值是使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。

      const obj = {
      	name: '张三',
      	age: 20}
      function fn(params1, params2) {
      	console.log(this.name, this.age, params1, params2);
      }
      const result = fn.call(obj, '参数1', '参数2'); // 打印:张三, 20, 参数1, 参数2
      console.log(result); // 打印undefined
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    2. apply接收两个参数,第一个参数是this要指向的对象,第二个参数是一个数组(或一个类数组对象),数组或者类数组对象中包裹的是要传递给改造后的函数的参数,使用apply方法,改造后的函数会立即执行。apply的返回值是使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。

      const obj = {
      	name: '张三',
      	age: 20}
      function fn(params1, params2) {
      	console.log(this.name, this.age, params1, params2);
      }
      const result = fn.apply(obj, ['参数1', '参数2']); // 打印:张三, 20, 参数1, 参数2
      console.log(result); // 打印undefined
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    3. bind的第一个参数是this要指向的对象,并且第二个参数开始,可以接收多个参数作为改造后的函数参数,使用bind方法,改造后的函数并不会立即执行,而是作为bind方法的返回值return出来,因此需要额外调用。bind的返回值就是改造后的函数

      const obj = {
      	name: '张三',
      	age: 20}
      function fn(params1, params2) {
      	console.log(this.name, this.age, params1, params2);
      }
      const resultFun = fn.bind(obj, '参数1', '参数2');
      resultFun(); // 打印:张三, 20, 参数1, 参数2
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

    手写call\apply\bind

    1. 实现call
      首先我们要分析call的主要特性:
      ①call挂载在Function构造函数的原型链上
      ②call的调用者必须是函数
      ③接收的参数数量不固定,因此我们需要获取传入call的实参,即通过argument实现
      ④argument是类数组对象,我们需要将类数组对象转成数组,有三种方法:Array.prototype.slice.call、Array.from、循环遍历
      ⑤接收的第一个参数是this指定的对象(要保证指定的this指向(即context)必须是一个复杂类型的数据结构(如object\function),不能是简单类型的数据(如boolean\number\string\symbol)。如果context是简单类型的数据,需要转成对应类型的对象;如果context是复杂类型的数据,则保持不变;如果是undefined、null,则默认指向window;)(判断数据类型最推荐的办法是通过Object.prototype.toString.call()来判断,但是我们需要手动实现call\apply\bind,那么这里就不通过这种方式来判断数据类型,而是通过其他的方法来判断数据类型)
      ⑥接收的参数,从第二个算起,是传给改造之后的函数的,作为改造之后的函数的参数
      ⑦call是立即执行函数
      ⑧call返回的是改造后的函数所返回的值(原函数返回啥,call就返回啥)

      从上述主要特性,那么我们可以着手实现call:

      // 实现call
      Function.prototype.myCall = function () { // call挂载在Function构造函数的原型链上,因此myCall也要挂载在Function的prototype上
        // 先获取调用myCall的函数
        const fn = this;
        
        // myCall的调用者数据类型必须是函数,否则抛出错误
        if(typeof fn !== 'function') {
          throw TypeError('Not a Function');
        }
        const args = []; // 创建一个数组,用于接收myCall的实参
        /*
          arguments是原生js的一个对象,可以获取传入myCall函数的实参,是一个类数组对象,类数组对象拥有数组的特性,即length,但是不能使用Array的方法
          我们需要将arguments类数组对象转成数组:
            1.正常情况下可以通过Array.prototype.slice.call()的方式,即Array.prototype.slice.call(arguments)转成数组,但是我们还没实现call,所以此方法暂时不用
            2.es6中的新方法Array.from()也可以将类数组对象转成数组,即Array.from(arguemtns)
            3.我们使用最原始的方法,以循环的形式,将类数组对象转成数组,获取myCall的实参数组
        */
        for (let i = 0; i < arguments.length; i++) {
          args.push(arguments[i]);
        }
        // 获取指定的this指向,即实参数组中的第一个值,并将指定的this指向从参数数组中剔除
        let context = args.shift() ;
        /*
          容错机制:要保证指定的this指向(即context)必须是一个复杂类型的数据结构(如object\function),不能是简单类型的数据(如boolean\number\string\symbol)。
          如果context是简单类型的数据,需要转成对应类型的对象;
          如果context是复杂类型的数据,则保持不变;
          如果是undefined、null,则默认指向window;
          备注:其实判断数据类型最推荐的办法是通过Object.prototype.toString.call()来判断,但是我们需要手动实现call\apply\bind,那么这里就不通过这种方式来判断数据类型,而是通过其他的方法来判断数据类型
        */
        if (typeof context === 'undefined') {
          context = window;
        } else if (typeof context === 'boolean') {
          context = new Boolean(context);
        } else if (typeof context === 'number') {
          context = new Number(context);
        } else if (typeof context === 'string') {
          context = new String(context);
        } else if (typeof context === 'symbol') {
          context = new Symbol(context);
        } else if (typeof context === 'function') {
          context = context;
        } else if (typeof context === 'object') {
          // context = context ? context : window;
          context = context ? context : window;
        }
      
        let randomAttributeName = Symbol(); // 创建一个随机属性名,Symbol()始终返回唯一值
        context[randomAttributeName] = fn; // 将fn临时作为context这个对象的属性存储起来,去调用fn的时候this也就指向context
        let result = context[randomAttributeName](...args); // 用context这个对象调用函数fn
        delete context[randomAttributeName]; // 删除临时绑定在context上的属性,让context回归原本的样子
        return result; // call返回的是改造后的函数调用后所返回的值(原函数返回啥,call就返回啥)
      }
      
      
      
      // 测试
      function fn(params1, params2) {
        console.log(this, params1, params2)
      }
      
      const obj = {
        name: '张三',
        age: 20,
      }
      
      fn.myCall(obj, '参数1', '参数2'); // 打印obj、参数1、参数2
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
    2. 实现apply (类似call的实现,只是参数传递不同)
      首先我们要分析apply的主要特性:
      ①apply挂载在Function构造函数的原型链上
      ②apply的调用者必须是函数
      ③接收两个参数,第一个参数是指定的this指向,第二个参数是数组,数组中是要传给改造后的函数作为参数
      ④接收的第一个参数是this指定的对象(要保证指定的this指向(即context)必须是一个复杂类型的数据结构(如object\function),不能是简单类型的数据(如boolean\number\string\symbol)。如果context是简单类型的数据,需要转成对应类型的对象;如果context是复杂类型的数据,则保持不变;如果是undefined、null,则默认指向window;)(判断数据类型最推荐的办法是通过Object.prototype.toString.call()来判断,但是我们需要手动实现call\apply\bind,那么这里就不通过这种方式来判断数据类型,而是通过其他的方法来判断数据类型)
      ⑤apply是立即执行函数
      ⑥apply返回的是改造后的函数所返回的值(原函数返回啥,call就返回啥)

      从上述主要特性,那么我们可以着手实现apply:

      // 实现apply
      Function.prototype.myApply = function (context, args = []) { // apply挂载在Function构造函数的原型链上,因此myApply也要挂载在Function的prototype上
                                                              // 第一个参数是指定的this指向
                                                              // 第二个参数是一个数组,数组中是要传给改造后的函数作为参数
        // 先获取调用myApply的函数
        const fn = this;
        
        // myApply的调用者数据类型必须是函数,否则抛出错误
        if(typeof fn !== 'function') {
          throw TypeError('Not a Function');
        }
      
        /*
          容错机制:要保证指定的this指向(即context)必须是一个复杂类型的数据结构(如object\function),不能是简单类型的数据(如boolean\number\string\symbol)。
          如果context是简单类型的数据,需要转成对应类型的对象;
          如果context是复杂类型的数据,则保持不变;
          如果是undefined、null,则默认指向window;
          备注:其实判断数据类型最推荐的办法是通过Object.prototype.toString.call()来判断,但是我们需要手动实现call\apply\bind,那么这里就不通过这种方式来判断数据类型,而是通过其他的方法来判断数据类型
        */
        if (typeof context === 'undefined') {
          context = window;
        } else if (typeof context === 'boolean') {
          context = new Boolean(context);
        } else if (typeof context === 'number') {
          context = new Number(context);
        } else if (typeof context === 'string') {
          context = new String(context);
        } else if (typeof context === 'symbol') {
          context = new Symbol(context);
        } else if (typeof context === 'function') {
          context = context;
        } else if (typeof context === 'object') {
          // context = context ? context : window;
          context = context ? context : window;
        }
      
        let randomAttributeName = Symbol(); // 创建一个随机属性名,Symbol()始终返回唯一值
        context[randomAttributeName] = fn; // 将fn临时作为context这个对象的属性存储起来,去调用fn的时候this也就指向context
        let result = context[randomAttributeName](...args); // 用context这个对象调用函数fn
        delete context[randomAttributeName]; // 删除临时绑定在context上的属性,让context回归原本的样子
        return result; // myApply返回的是改造后的函数调用后所返回的值(原函数返回啥,myApply就返回啥)
      }
      
      
      // 测试
      function fn(params1, params2) {
        console.log(this, params1, params2)
      }
      
      const obj = {
        name: '张三',
        age: 20,
      }
      
      fn.myApply(obj, ['参数1', '参数2']); // 打印obj、参数1、参数2
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
    3. 实现bind(类似call的实现,只是bind返回的是函数)
      首先我们要分析bind的主要特性:
      ①bind挂载在Function构造函数的原型链上
      ②bind的调用者必须是函数
      ③接收的参数数量不固定,因此我们需要获取传入bind的实参,即通过argument实现
      ④argument是类数组对象,我们需要将类数组对象转成数组,有三种方法:Array.prototype.slice.call、Array.from、循环遍历
      ⑤接收的第一个参数是this指定的对象(要保证指定的this指向(即context)必须是一个复杂类型的数据结构(如object\function),不能是简单类型的数据(如boolean\number\string\symbol)。如果context是简单类型的数据,需要转成对应类型的对象;如果context是复杂类型的数据,则保持不变;如果是undefined、null,则默认指向window;)(判断数据类型最推荐的办法是通过Object.prototype.toString.call()来判断,但是我们需要手动实现call\apply\bind,那么这里就不通过这种方式来判断数据类型,而是通过其他的方法来判断数据类型)
      ⑥接收的参数,从第二个算起,是传给改造之后的函数的,作为改造之后的函数的参数
      ⑦bind返回值的是一个函数,即改造之后的函数,不是立即执行的,需要手动调用
      ⑧因为bind返回的是函数,而不是立即执行的,所以我们需要优化这个return出来的函数,让其支持柯里化

      从上述主要特性,那么我们可以着手实现bind:

      // 实现bind
      Function.prototype.myBind = function () { // bind挂载在Function构造函数的原型链上,因此myBind也要挂载在Function的prototype上
        // 先获取调用myBind的函数
        const fn = this;
        
        // myBind的调用者数据类型必须是函数,否则抛出错误
        if(typeof fn !== 'function') {
          throw TypeError('Not a Function');
        }
        const args = []; // 创建一个数组,用于接收myBind的实参
        /*
          arguments是原生js的一个对象,可以获取传入myBind函数的实参,是一个类数组对象,类数组对象拥有数组的特性,即length,但是不能使用Array的方法
          我们需要将arguments类数组对象转成数组:
            1.正常情况下可以通过Array.prototype.slice.call()的方式,即Array.prototype.slice.call(arguments)转成数组,但是我们还没实现call,所以此方法暂时不用
            2.es6中的新方法Array.from()也可以将类数组对象转成数组,即Array.from(arguemtns)
            3.我们使用最原始的方法,以循环的形式,将类数组对象转成数组,获取myBind的实参数组
        */
        for (let i = 0; i < arguments.length; i++) {
          args.push(arguments[i]);
        }
        // 获取指定的this指向,即实参数组中的第一个值,并将指定的this指向从参数数组中剔除
        let context = args.shift() ;
        /*
          容错机制:要保证指定的this指向(即context)必须是一个复杂类型的数据结构(如object\function),不能是简单类型的数据(如boolean\number\string\symbol)。
          如果context是简单类型的数据,需要转成对应类型的对象;
          如果context是复杂类型的数据,则保持不变;
          如果是undefined、null,则默认指向window;
          备注:其实判断数据类型最推荐的办法是通过Object.prototype.toString.call()来判断,但是我们需要手动实现call\apply\bind,那么这里就不通过这种方式来判断数据类型,而是通过其他的方法来判断数据类型
        */
        if (typeof context === 'undefined') {
          context = window;
        } else if (typeof context === 'boolean') {
          context = new Boolean(context);
        } else if (typeof context === 'number') {
          context = new Number(context);
        } else if (typeof context === 'string') {
          context = new String(context);
        } else if (typeof context === 'symbol') {
          context = new Symbol(context);
        } else if (typeof context === 'function') {
          context = context;
        } else if (typeof context === 'object') {
          // context = context ? context : window;
          context = context ? context : window;
        }
      
        // bind方法返回的是一个函数,所以myBind也应该返回一个函数,而且这个函数的this得是指定的this指向,即context
        return function F(...rest) { // 除了通过arguments可以获取F的实参外,还能通过剩余参数...rest的形式来获取F的实参。这边是为了区分myBind和F的实参,所以F不用arguments而是...rest
          
          if (this instanceof F) { // 处理myBind return出去的函数被作为构造函数使用的情况,即通过new运算符来调用return出去的函数实例( 如 new func.bind() ) (很少有这样的用法,一般都是return出去之后直接调用的)
            return new fn(...args, ...rest);
          }
      
          /*
            以下一段完全可以用
              return fn.apply(context, args.concat(...rest))
            代替。
            只是我们假装没有apply方法,故以下面的写法来实现
          */
          let randomAttributeName = Symbol(); // 创建一个随机属性名,Symbol()始终返回唯一值
          context[randomAttributeName] = fn; // 将fn临时作为context这个对象的属性存储起来,去调用fn的时候this也就指向context
          let paramsArr = args.concat(...rest); // args.concat(...rest)是为了支持函数柯里化
          let result = context[randomAttributeName](...paramsArr); // 用context这个对象调用函数fn
          delete context[randomAttributeName]; // 删除临时绑定在context上的属性,让context回归原本的样子
          return result;
        }
      }
      
      // 测试
      function fn(params1, params2) {
        console.log(this, params1, params2)
      }
      
      const obj = {
        name: '张三',
        age: 20,
      }
      
      fn.myBind(obj, '参数1', '参数2')(); // 打印obj、参数1、参数2
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79

    如何选用call\apply\bind

    如果不需要关心具体有多少参数被传入函数,选用apply();
    如果确定函数可接收多少个参数,并且想一目了然表达形参和实参的对应关系,用call();
    如果我们想要将来再调用方法,不需立即得到函数返回结果,则使用bind();

    函数柯里化

    手动实现bind函数的底层原理就是函数柯里化的应用,关于函数柯里化,可以参考我的文章函数柯里化详解

  • 相关阅读:
    《论文阅读27》SuperGlue: Learning Feature Matching with Graph Neural Networks
    文件分卷压缩和压缩的区别是什么
    HTML总结
    自动语音识别(ASR)研究综述
    RabbitMQ消息的重复消费问题
    春风吹又生的开源项目「GitHub 热点速览」
    时间复杂度和空间复杂度
    阿里云网络
    2.FastRunner定时任务Celery+RabbitMQ
    浅入浅出linux中断子系统
  • 原文地址:https://blog.csdn.net/Boale_H/article/details/126430132