前端面试手写代码-bind

603 阅读3分钟

前言

大家一定要摆脱舒适圈,只有用时间才能换来你想要的,还是那句话,光羡慕有什么用,咱们得行动起来。

call和apply

如果我们想要改变函数内部的this指向,有三种方法,其中callapply类似

const res = myFn.call(context, ...args);
const res = myFn.apply(context, args);

两者都是传入函数上下文context和其他参数后直接调用,获取返回值,只是其他参数的传递形式不同罢了。

因此实现这两种方法比较简单:

  • 声明一个myCall/myApply函数
  • 在内部通过arguments获取到传入的上下文context,和其他剩余的参数args
  • 将调用该方法的函数fn(fn = this)挂载到context
  • 通过context.fn的方法来调用即可
  • 将挂载的临时属性删除,并返回结果
myCall(context, ...args) {
  // 判断调用者是否为函数,this就是调用的函数
  if (typeof this !== "function") {
    throw new Error("need function");
  }
  // 借鉴别人的思路,生成唯一值用于挂载,后续删除
  const fnKey = Symbol();
  // **异常判断**如果传入的context为null undefined等
  if (!context) {
    context = window;
  }
  // 挂载,调用
  context[fnKey] = this;
  const res = context[fnKey](...args);
  delete context[fnKey];
  return res;
}

bind

bind在此比较特殊,因为它不仅改变了上下文this,还返回了一个全新的函数(并不是直接调用),我们有以下几个需要注意的点:

  • bind时传递的额外参数会排列在后续调用时传递的实际参数之前
  • 返回的函数可能被new操作符调用

new

没有。new一个吧

new的过程到底发生了什么呢?

  • 新建一个对象obj

  • 将新对象obj的原型指向构造函数的原型对象,即obj.__proto__ = Cons.prototype或者Object.setPrototypeOf(obj, Cons.prototype)

  • 将构造函数的this指向obj,并调用,可以这样实现Cons.call(obj, ...args)

  • 根据构造函数的返回值来判断new该返回什么

    • 如果构造函数返回基础类型(没有return语句相当于返回undefined),则new操作会返回新建的obj对象
    • 如果构造函数返回的时引用类型,则会忽略new操作符,直接返回构造函数的return值

手写new

function New(fn, ...args) {
  const obj = {};
  Object.setPrototypeOf(obj, fn); 
  // 等价于obj.__proto__ = fn.prototype或者
  // const obj = Object.create(fn.prototype)
  const res = fn.call(obj, ...args);
  // 返回值需要做特殊判断处理
  return res instanceof Object ? res : obj;
}

实现bind

myBind(context, ...preArgs) {
  const fn = this;
  if (typeof fn !== "function") {
    throw new Error("need function");
  }
  function newFn(...args) {
    // 判断外部是否是通过new操作符调用
    // 是的话,先前传入的context直接作废,this指向新创建的obj
    // new的过程会对构造函数进行call(obj)的操作,所以当前上下文this就是obj
    const isByNew = this instanceof fn ? this : context;
    // !!重点!!
    const newContext = isByNew ? this : context;
    return fn.call(newContext, ...preArgs, ...args);
  }
  // 拷贝函数就得完整,同时也要判断箭头函数没有prototype的情况
  // 如果此处不对原型进行拷贝,那上面就无法准确判断出是否通过new调用
  if (fn.prototype) {
    // 不可以直接引用赋值
    newFn.prototype = Object.create(fn.prototype);
  }
  return newFn;
}

// 补充Object.create原理
// Object.create = function (prototype) {
//   const F = function () {};
//   F.prototype = prototype;
//   return new F();
// }

这里的逻辑稍微有点绕,但只要明白new的过程会发生Cons.call(obj)就比较好理解判断isByNew的过程了。

扩展instanceof操作符

不要睡,咱们还能扩展,最后谈谈instanceof

MDN解释instanceof操作符用于判断该对象的原型链上__proto__属性) 是否存在对构造函数的原型对象prototype)的引用

所以执行完setPrototype(obj, Cons.prototype)后得到obj instanceof Cons === true

灵魂拷问

如果对箭头函数进行一个bind操作会怎么样?

我们不妨试试

const arrow = () => {
    console.log(this.a)
};
arrow();  // undefined
a = 1;
arrow();  // 1

const bindArrow = arrow.bind({a: 2});

bindArrow(); // 1

可以看到,我们对箭头函数进行绑定时,传入的context,其实直接被忽略了。