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

59 阅读9分钟

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

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

bind的原理

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

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

  • 我们知道传进去的第一个必定是一个对象,剩下几个都是参数,所以这里需要进行一个切割,。把除对象以外的参数截取出来,没有传参数的情况也要考虑到。 在这里我们介绍一个新方法

    Array.prototype.slice.call(arguments, 1);

    首先我们知道函数中自带的arguments是一个类数组,它身上虽然有数组具备的属性,但它如果不解构是无法使用数组身上的api的,而这个方法就是能将具有length属性的对象(key值为数字)转成数组,[]是Array的示例,所以可以直接使用[].slice()方法。

    同时我们不知道用户会在使用Bind的时候传入参数,还是调用返回的函数时传入参数,所以我们需要将bind之后的参数和调用返回的新函数时传入的参数合并。

    Array.prototype.slice.call(arguments, 1).concat(...args)

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

  • 前三点和call和apply的原理差不多,但是呢bind不一样的地方在于,bind改变了this指向以后,并不是帮助我们立刻把改变后的函数立即调用,而是return一个新的函数,用户需要手动调用!

bind的基础功能代码

Function.prototype.myBind = function (context) {
 var args = Array.prototype.slice.call(arguments, 1)||"";
    return (...args2) => {
        args=args.concat(...args2)//合并两个函数的参数
        context["fn"] = this;//获取调用bind的函数
        const res = context["fn"](...args);
        return res;
      };
  }

其实bind在这里基本功能已经完全实现了,我们来试试

  • 第一种bind传参方式

image.png

可以没问题

  • 第二种调用函数传参方式

image.png

可以也没问题

代码完善

看过前面两篇call和apply的小伙伴这里可以跳过直接转向进阶版bind哦!!!

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

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

image.png

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

Function.prototype.myBind = function (context) {
 var args = Array.prototype.slice.call(arguments, 1)||"";
    return (...args2) => {
        args=args.concat(...args2)//合并两个函数的参数
        context["fn"] = this;//获取调用bind的函数
        const res = context["fn"](...args);
        delete context["fn"]
        return res;
      };
  }
  • 不传参会发生什么

image.png

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

image.png

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

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

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

加上以上三个注意点的完整代码

Function.prototype.myBind = function (context) {
 var args = Array.prototype.slice.call(arguments, 1)||"";
    return (...args2) => {
        args=args.concat(...args2)//合并两个函数的参数
        context["fn"] = this;//获取调用bind的函数
        const res = context["fn"](...args);
        delete context["fn"]
        return res;
      };
  }

进阶版bind:new一下bind返回的函数会怎么样?

其实如果不考虑这一点的话我们的bind是 绰绰有余足够用了,但是就是会有人手贱想去new一下新的函数咋办呢?来看看我们原本的bind new一下会发生什么

image.png 我们发现,原来的函数this指向不生效了,输出的内容全部变成了undefined,唯独传入的参数还会执行相加操作。

知识点铺垫:new一个构造函数会发生什么

追根究底一下我们new的时候,函数里面到底发生了什么操作

当我们new一个构造函数时,会发生以下几点

1.编译器新创建一个空对象

var fn = new Object();

2.构造函数的显示原型等于实例对象的隐式原型,实例对象的constructor属性为构造函数的名称

Fn.prototype = fn.__proto__

3.通过调用call、apply方法执行构造函数并改变this对象(绑定到实例对象上)

Fn.call(f)

4.如果没有手动返回其他任何对象或返回值是基本类型(Number、String、Boolean)的值,则会返回 this 指向的新对象,也就是实例,若返回值是引用类型(Object、Array、Function)的值,则实际返回值为这个引用类型。

所以其实在原生的bind中,我们返回的是改变this指向后的foo,本质上还是new了一个foo我们看一下是不是这样

image.png

  • 保存foo的函数原型
    我们需要让bind返回的那个函数,如果被new的话,它的原型一定是foo的原型,所以我们可以在bind函数内部再定义一个,用它的原型指向foo的原型,同时我们也要判断一下foo到底有没有原型,因为构造函数是没有原型的!
 const pro = function () {};
    if (this.prototype) {
    pro.prototype = this.prototype;
  }
  
  • bind的实现基于了apply

大家可能都不知道,bind的内部实现原理是架在了apply上面实现的。有人又要说了,那没有apply怎么办呢?嗯?没有apply我们不知道自己写一个吗?前两篇文章不都教了大家怎么写吗?哈哈哈哈

那apply具体是用在bind里面干嘛的呢?apply除了改变this指向当然就没有其他用处了呀,具体怎么用让我们结合接下来的我们如何得知函数被new了去熟悉。

  • 如何得知我们函数被new了?

这也是最难的地方, 通过前面new构造函数的第二点我们知道构造函数的显示原型等于实例对象的隐式原型,实例对象的constructor属性为构造函数的名称

我们在这里先改变策略,不返回匿名函数了,而是给返回的函数取一个名字,例如bound。将bound的原型继承foo的原型,即new pro之后的实例的隐式原型指向的就是foo的原型。

 const bound = function () {
    …………
  };
 bound.prototype = new pro(); 
 // 继承到了foo的原型,即new bound().__proto__  ===  foo.prototype
 return bound;

同时我们需要判断一下,我们的bound是否被new?那就要通过判断实例对象的构造函数名称来判断bound是否被new,那个方法可用来判断变量的构造函数呢?是不是instance?

this instanceof pro

当我们new了bound之后,此时bound内部的this指向了实例对象,我们的foo该如何拿到呢?只需在bound外面定义一个变量,保存外部this即foo即可

const _this = this;

接下来就是具体说明apply要怎么用了,我们在bound函数内部可以用apply直接就改变foo this的指向,直接把值返回给bound,再把bound返回出去,相当于,bound里面存储了一个改变this指向后foo的值。那这么做有什么意义呢?你先别急,等我一一道来。

我们new特点的第三点:通过调用call、apply方法执行构造函数并改变this对象(绑定到实例对象上)

来一个三元运算符,判断当前this的构造函数的名称,如果是foo的this就绑定实例对象,如果不是就绑定传入的obj

  const _this = this;
  const bound = function () {
    // 判断该函数是否被new
    return _this.apply(
      this instanceof pro ? this : context,
      args.concat(Array.prototype.slice.call(arguments))
    );
  };

  bound.prototype = new pro(); // 继承到了foo的原型
  return bound;

到这里我们可以被new的bind返回函数就大功告成了,看看完整代码

完整代码

Function.prototype.myBind = function (context) {
  if (typeof this !== "function") {//增加的一点完善代码,判断调用的是否为函数,不是就报错
    throw new TypeError("error");
  }
 context = context|| window;

  const args = Array.prototype.slice.call(arguments, 1)||"";//将参数切割
  const _this = this;
  const pro = function () {};

  if (this.prototype) {//判断是否为箭头函数
    pro.prototype = this.prototype;
  }

  const bound = function () {
    // 判断该函数是否被new
    return _this.apply(
      this instanceof pro ? this : context,
      args.concat(Array.prototype.slice.call(arguments))
    );
  };

  bound.prototype = new pro(); // 继承到了foo的原型

  return bound;
};

让我们来试试结果

  • bind直接传参 image.png

  • 返回的函数传参 image.png

  • 不传参

image.png

  • new 返回的函数

image.png

不错!!!功能齐全,很完美

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