趣谈 call / apply / bind

519 阅读6分钟

趣谈 call / apply / bind

call : 召唤,打电话

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

故事
let a = {};
a.push("text"); // TypeError: a.push is not a function

“又错了,又错了,怎么回事?” ,远远的听到对象的抱怨声。仔细了解才知道是对象在处理任务时,发现键都是数字,觉得自己每次都用对象[数字]赋值有点麻烦,就想尝试使用和数组老弟一样的push方法,结果老是报错!于是找到了数组老弟

对象: 数组老弟呀!好久不见

数组:大哥,好久不见,找小弟有什么事吗?

对象:大哥最近处理事情的时候,的确遇到了一点麻烦!!!

数组:大哥随便说,小弟能帮的,一定义不容辞

对象:我最近有一个任务,用的是数字做 key,麻烦死,如果我有一个 push 方法就好了

数组:大哥,这还不简单,下次有这样的方法直接找我就是了,我随叫随到

于是对象下次一旦有这样的任务就召唤数组老弟帮忙

let a = {};

// 打电话给数组老弟,我要用你的push方法
Array.prototype.push.call(a, "测试数据");

console.log(a);
插曲

其实在对象数组来处理任务的时候也发生过一段小插曲,那就是对象发现自己好不容易设置上去的值不见了

let a = {
      0: "没有push前的值",
};

Array.prototype.push.call(a, "这个新值");

console.log(a); // {0: '这个新值', length: 1}

这个是因为push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。

上面的 push 方法是一个隐藏的方法,我们了解到了,我们使用对象调用了数组的方法,接下来再举个例子,看看到底是怎么做到的

韩梅梅借计算器

有一天,韩梅梅看到李雷买了一个 sum 计算器计算自己的薪资,感觉很方便,但是感觉自己买又太贵了,于是决定找李雷借用一下,计算自己的薪资

let lilei = {
      name: "李雷",
      sum: function (base, num1, num2, num3) {
            console.log(this.name + "得到的薪资是:" + (base + num1 + num2));
      },
};

lilei.sum(1000, 200, 100, 200); // 李雷得到的薪资是:1500

let hanmeimei = {
      name: "韩梅梅",
};

// 李雷借给了韩梅梅计算器
lilei.sum.call(hanmeimei, 2000, 300, 100, 230); // 韩梅梅得到的薪资是:2630

我们可以看到韩梅梅并没有 sum 计算器,但是依然可以使用,这都应为 call 在前面帮忙,以上案例我们可以看到 call 做了两件事

  • 绑定函数 this 指向 - 在李雷借计算器给韩梅梅时,输出的 this.name 不在是李雷了,而是韩梅梅
  • 执行函数 - 因为控制台打印了韩梅梅得到的薪资是:2630

apply :申请

apply() 方法调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数。

有一天,公司说,系统升级了,说可以导出数据了,但是导出的数据是一个数组。韩梅梅就想呀!好不容易导出数据了,不用自己一个一个的输入了,我能不能换一种方式借用李雷的 sum 计算器呢? 于是想起了另一个方法apply

let lilei = {
      name: "李雷",
      sum: function (base, num1, num2, num3) {
            console.log(
                  this.name + "得到的薪资是:" + (base + num1 + num2 + num3)
            );
      },
};

lilei.sum(1000, 200, 100, 200); // 李雷得到的薪资是:1500

let hanmeimei = {
      name: "韩梅梅",
};

lilei.sum.apply(hanmeimei, [2000, 300, 100, 100]); // 韩梅梅得到的薪资是:2500

这次财务系统的升级很明显给没有买计算器的韩梅梅带来了更多的便利,也让我们看到 apply 和 call 之间的差别

  • 绑定函数 this 指向 - 在李雷借计算器给韩梅梅时,输出的 this.name 不在是李雷了,而是韩梅梅
  • 将数组参数解构到方法参数上 - 传递了一个参数,但是实际上 sum 接收多个参数
  • 执行函数 - 因为控制台打印了 韩梅梅得到的薪资是:2500

bind : 捆绑

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

这天, 公司推出了克隆技术,于是韩梅梅就想,我每天借李雷的计算器不是很好,要不我去克隆一个给自己,这样我就不用再借了呀!

let lilei = {
      name: "李雷",
      sum: function (base, num1, num2, num3) {
            console.log(
                  this.name + "得到的薪资是:" + (base + num1 + num2 + num3)
            );
      },
};

lilei.sum(1000, 200, 100, 200); // 李雷得到的薪资是:1500

let hanmeimei = {
      name: "韩梅梅",
};

let newSum = lilei.sum.bind(hanmeimei);
newSum(2000, 300, 400, 290); // 韩梅梅得到的薪资是:2990

克隆完成了,但是韩梅梅想呀!我每次的底薪都是 2000,我是不是可以让克隆方法进化为一个不用输入底薪的方法呢?

let lilei = {
      name: "李雷",
      sum: function (base, num1, num2, num3) {
            console.log(
                  this.name + "得到的薪资是:" + (base + num1 + num2 + num3)
            );
      },
};

let hanmeimei = {
      name: "韩梅梅",
};

let newSum = lilei.sum.bind(hanmeimei, 2000);
newSum(300, 400, 290);

很明显克隆技术很容易就将李雷的计算器扒了过来,并且还能进化为更独特的方法,我们来看看 bind 到底做了哪些事

  • 绑定函数 this 指向 - 李雷的计算器克隆给韩梅梅后,我们计算输出的 this.name 不在是李雷了,而是韩梅梅
  • 将第二个后面的参数,直接绑定到源方法的参数中 - 这里指的是2000 和 2000 以后的参数
  • 返回一个新函数
  • 执行函数 - 再 newSum 赋值的时候,并没有执行,需要调用函数才会执行,所以并不执行函数

总结一下

  • call 和 apply 都可以绑定 this 值,并且都是立即调用,只是参数不同

    • call 可以有多个参数,第一个指定的 this,后面参数为调用方法(sum 方法)的其他参数
    • apply 只有两个参数,第一个指定的 this,第二个是一个数组或者伪数组【对应调用函数(sum 方法)的参数列表】
  • bind 也有绑定 this 的功能,但是不会立即调用,拥有多个参数,

    • 第一个参数指定 this
    • 后面的参数会固定调用函数(sum 方法)的形参,返回一个新的函数
    • 【新函数的参数值个数(3) = 原函数的参数值个数(4) - (bind 参数值个数(2) - 1)】

面试题

使用 call 或者 apply 方法实现 bind

题目分析:

  • bind需要返回一个新函数
  • bind可以先保存一部分原函数的参数(闭包保存)
  • 调用原函数,并传入参数
Function.prototype.bind = function (obj) {
      // 存一下当前函数方法
      var self = this;
      // 拿一下参数传入的其他参数
      var arrOuter = Array.prototype.slice.call(arguments, 1);
      // 返回一个新function
      return function () {
            // 拿下返回函数的参数
            var arrInner = Array.prototype.slice.call(arguments);
            // 内外参数合并
            var args = arrInner.concat(arrOuter);
            // 调用函数
            self.apply(obj, args);
      };
};