面试官:来手写个call、apply和bind吧

94 阅读5分钟

最近正值秋招,面试时难免会问到this指向的问题,于是就有了下面这样的对话~

面试官:你说下this吧

我:开始吟唱,疯狂背八股~

面试官:ok,那你知道call、apply和bind的作用以及他们之间的区别吗?

我:常规八股,继续吟唱~

面试官:ok,那你来手写一个call和bind吧

我:啊?

写半天没写出来。。。

面试官:好的,我们今天的面试就到这里,谢谢你的时间

写在开头

本文不讨论this指向的各种问题,相信大家也了解的滚瓜烂熟了,本文旨在带大家熟悉callapplybind的区别、用法以及手写出来。

call、apply和bind区别

众所周知,通过 callapplybind 我们可以修改函数绑定的 this,使其成为我们指定的对象。通过这些方法的第一个参数我们可以显式地绑定 this

function foo(name, price) {
    this.name = name
    this.price = price
}

function Food(category, name, price) {
    foo.call(this, name, price)       // call 方式调用
    // foo.apply(this, [name, price])    // apply 方式调用
    this.category = category
}

new Food('食品', '汉堡', '5块钱')

// 浏览器中输出: {name: "汉堡", price: "5块钱", category: "食品"}
call 和 apply 的区别是 call 方法接受的是参数列表,而 apply 方法接受的是一个参数数组。

func.call(thisArg, arg1, arg2, ...)        // call 用法
func.apply(thisArg, [arg1, arg2, ...])     // apply 用法

而 bind 方法是设置 this 为给定的值,并返回一个新的函数,且在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。

func.bind(thisArg[, arg1[, arg2[, ...]]])    // bind 用法

ES6 方式用了一些 ES6 的知识比如 rest 参数、数组解构

注意:  如果你把 null 或 undefined 作为 this 的绑定对象传入 callapplybind,这些值在调用时会被忽略,实际应用的是默认绑定规则。

var a = 'hello'

function foo() {
    console.log(this.a)
}

foo.call(null)         // 浏览器中输出: "hello"

总的来说:他们两个区别

  • 第一个是传参方式不同: callbind 是列表传参,apply 是数组或伪数组传参
  • 第二个是执行机制不同:callapply 是立即执行,bind 不会立即执行而是生成一个修改 this 之后的新函数

手写call、apply

Function.prototype._call = function (context = globalThis, ...args) {
  if (typeof context !== "object") context = new Object(context);
  let fnKey = Symbol();
  context[fnKey] = this;
  let res = context[fnKey](...args);
  delete context[fnKey];
  return res;
};

代码逐行解释:

Function.prototype._call = function (context = globalThis, ...args)

此处将 _call 方法添加到 Function 的原型对象上,使所有函数对象都可以使用该方法。_call 方法接受两个参数:context(默认为全局对象 globalThis)表示要绑定给函数的上下文对象,...args 表示传递给函数的参数列表。

if (typeof context !== "object") context = new Object(context);

在调用函数之前,首先判断 context 的类型是否为对象。如果不是对象,将使用 Object 构造函数创建一个新的对象作为上下文对象。

let fnKey = Symbol(); // 相当于将函数绑定到上下文对象并改变函数执行时的 this 指向。 context[fnKey] = this;

创建一个唯一的符号键 fnKey,并将当前函数对象(调用 _call 方法的函数)绑定到上下文对象的 fnKey 键上。这相当于将函数绑定到上下文对象并改变函数执行时的 this 指向。

let res = context[fnKey](...args);

通过调用上下文对象的 fnKey 属性(即绑定的函数),并传递参数列表 args,执行函数。

delete context[fnKey];

在函数执行后,删除上下文对象的 fnKey 属性,以便清除对函数的引用。

同样的,apply与call基本一致

// 这里传参和call传参不一样
Function.prototype._apply = function (context = globalThis, args) {
  if (typeof context !== "object") context = new Object(context); 

  // args 传递过来的参数
  // this 表示调用call的函数
  // context 是apply传入的this

  // 在context上加一个唯一值,不会出现属性名称的覆盖
  let fnKey = Symbol();
  // this 就是当前的函数
  context[fnKey] = this;
  // 绑定了this
  let res = context[fnKey](...args);
  delete context[fnKey];
  return res;
};

这里我就不做解释了

手写bind

Function.prototype._bind = function (context = globalThis, ...args) {
  let self = this;
  let fBound = function (...innerArgs) {
    return self.apply(
      this instanceof fBound ? this : context,
      args.concat(innerArgs)
    );
  };
  fBound.prototype = Object.create(this.prototype);
  return fBound;
};

代码逐行解释:

Function.prototype._bind = function (context = globalThis, ...args) {

此处将 _bind 方法添加到 Function 的原型对象上,使所有函数对象都可以使用该方法。_bind 方法接受两个参数:context(默认为全局对象 globalThis)表示要绑定给函数的上下文对象,...args 表示预设的参数列表。

let self = this;

在进行绑定操作之前,保存原始函数的引用。

let fBound = function (...innerArgs) {
  return self.apply(
    this instanceof fBound ? this : context,
    args.concat(innerArgs)
  );
};

创建一个新的函数 fBound,当调用这个新函数时,会执行原始函数,并将原始函数的 this 值绑定到指定的上下文 context。在新函数内部,通过 self.apply() 调用原始函数,将新函数的执行上下文当做 this 值传递给原始函数。this instanceof fBound 检查当前函数是否通过 new 关键字调用,如果是,则将当前的 this 值作为执行上下文;否则,使用指定的上下文对象 context

fBound.prototype = Object.create(this.prototype);

将新函数 fBound 的原型对象设置为原始函数的原型对象,以确保通过 new 关键字创建的实例能够继承原始函数的原型链上的属性和方法。

return fBound;

返回绑定了指定上下文的新函数 fBound

后话

在现在的环境下,校招面试只会越来越难,单纯去背八股文已经很难适应面试官的要求了。同时,大厂和一些中厂对于面试时手写也都有比较高的要求,手写题没写出来,往往那场面试就会挂。所以我们一定要掌握一些重点手写题来应对。