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

87 阅读5分钟

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

通过前两篇文章我们知道了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返回一个新函数后,调用新函数时向新函数内传参。

apply的原理

上一篇中我们实现了call方法,那接下来我们再来实现一下apply,其实和call没什么太大的区别,主要区别就在于传参方式,apply的参数,除了第一个对象,就是数组形式。

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

  • 同时我们知道他们传进去的第一个必定是一个对象,剩下几个都是参数,并且传入的参数都是以数组形式包裹的,所以 arguments自然只有两个参数,下标为0的是对象,为1的是数组参数,我们只需要按下标把参数拿出来就可以了,没有传参数的情况也要考虑到。var args = arguments[1]||"";

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

apply的代码

Function.prototype.myApply= function (context) {
    //将对象后面传入的参数拿出来
   var args = arguments[1]||"";
   ……
 };

同样,通过上一篇我们知道了this其实就是指向调用myApply函数的上下文,那么函数中的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的函数,将这个函数挂载到对象
 };

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

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

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

image.png

可以!差不多实现了

代码完善

这一步和我上一篇实现call是一样的,看过的小伙伴可以不要再看了。

  • 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就指回全局,但是我们的myApply并没有处理这一步,会报错

image.png

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

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

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

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

以下是myApply完整代码

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

试一试效果 image.png

完美结束

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

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