面试官:手写一个call、apply、bind我瞧瞧之call(二)

169 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

通过上一篇文章我们知道了this的指向,以及call、apply、bind的作用,简单回顾一下

//定义一个对象,对象中挂载一些属性
let obj = {
  name: "夹心啊",
  age: 18,
  type: "美女",
};

var name = "Alice";

function foo(num1, num2) {
  console.log(this.name);
  console.log(this.age);
  console.log(this.type);
  console.log(num1 + num2);
}

foo.call(obj,1,2)

foo.apply(obj,[1,2])

var newFoo=foo.bind(obj)
newFoo(1,2)

看看结果

image.png

image.png

image.png

call、apply、bind的区别

  • 执行方法:从上面可以看出call和apply都是使用后直接执行,但是唯独bind不会立即给你执行,而是返回一个新的函数,需要你去手动调用

  • 传参方式:call传参采用一个一个参数列举,apply则采用数组传参方式,将使用的参数包裹在一个数组内。bind有两种方式,你可以和call一样在调用bind的时候直接传参,也可以等call返回一个新函数后,调用新函数时向新函数内传参。

call实现原理

前期铺垫做完了,我们已然发现了call、apply、bind的作用和区别,那我们先来实现一下call吧

  • 首先我们知道,我们的函数身上并没有call、apply、bind这三种方法,所以他们必定是挂载在函数原型上的。Function.prototype.myCall

  • 同时我们知道他们传进去的第一个必定是一个对象,剩下几个都是参数,但是我们并不知道它会传多少个参数,所以需要将参数做一个切割,将对象和其他参数分别切出来,没有传参数的情况也要考虑到var args = [...arguments].slice(1)||"";

  • 最重要的一点来了,实际上,我们为了能让foo中的this指向obj,我们还是采用了隐式绑定规则,即让foo挂载在obj内部,让obj进行调用!这就是call能改变this绑定的最终奥义!

call的代码

Function.prototype.myCall = function (context) {
    //参数用slice切割,将对象后面传入的参数拿出来
   var args = [...arguments].slice(1);
   //注意必须将arguments结构一下,它是类数组,不解构无法使用数组身上的api哦
   ……
 };

接下来我们要思考了,也是最大的难题,我们要如何拿到foo,让它挂载在obj上呢?仔细想想,我们上一篇文章里面讲到了什么? this!!我们要使用call是不是需要用函数去调用?那么函数中的this自然会指向foo上下文咯!!所以this就相当于我们的foo函数了

//context是传入的obj
Function.prototype.myCall = function (context) {
    //参数用slice切割,将obj拿出来
   var args = [...arguments].slice(1);
   //注意必须将arguments结构一下,它是类数组,不解构无法使用数组身上的api哦
   context['fn'] = this;//this指向调用mycall的函数,将这个函数挂载到对象
 };

到这一步,其实call的原理已经差不多了,但call还帮我们多做了一步,就是调用改变this指向后的函数,返回它的返回结果。

Function.prototype.myCall = function (context) {
  var args = [...arguments].slice(1); //参数用slice切割,将obj拿出来
  context['fn'] = this;//this指向调用mycall的函数,将这个函数挂载到对象
  const res = context['fn'](...args);//调用改变this指向后的函数
  return res;//返回它的返回结果。
};

这样一个call方法基本就完成了,让我来试试好不好用

image.png

Yes!!我们成功啦!!

代码完善

  • delete删除obj上多出来的函数

但这样的代码并不完美,为什么呢?让我们来看看obj上多了点什么

image.png

你会看到,我们的obj上多了一个名叫foo的函数,是我们在myCall内部给obj挂载上去的,但是我们原本的call是不会这样的,所以我们还需要多做一步,就是在函数调用完毕后,将刚刚挂载到obj的函数删除 delete context['fn']

Function.prototype.myCall = function (context) {
  var args = [...arguments].slice(1); //参数用slice切割,将obj拿出来
  context['fn'] = this;//this指向调用mycall的函数,将这个函数挂载到对象
  const res = context['fn'](...args);//调用改变this指向后的函数
  delete context['fn']//将刚刚挂载到obj的函数删除
  return res;//返回它的返回结果。
};
  • 不传参会发生什么

image.png

我们发现不传参this就指回全局,但是我们的myCall并没有处理这一步,会报错

image.png

所以我们需要在使用传入context(obj)前处理一下,如果没传入obj就让它指向window var context = context || "window";

Function.prototype.myCall = function (context) {
  var context = context || "window";//如果没传入obj就让它指向window
   ……
};
  • 挂载一个独一无二的"fn"

思考一下万一obj里面存在一个与fn重名了的函数怎么办呢,我们myCall应该去调用执行哪个呢?我们想一想ES6里面新增了一个什么数据类型可以帮我们解决这个问题?是不是Symbol。可以声明一个不被改变的独一无二的fn。const fn = Symbol("fn") 注意这样的话context["fn"]里面的引号就要去掉了,因为此时的fn是声明的一个变量了,而不是一个名称。

想到这三点处理,我们的没有myCall就完美做成了

以下是myCall完整代码

Function.prototype.myCall = function (context) {
  var context = context || "window";
  var args = [...arguments].slice(1)||"";
  const fn = Symbol("fn");
  context[fn] = this;
  const res = context[fn](...args);
  delete context[fn];
  return res;
};

试一试效果

image.png

不错不错,真完美~~

上一篇:面试官:手写一个call、apply、bind之this铺垫(一)

下一篇:面试官:手写一个call、apply、bind我瞧瞧之apply(三)