探索 JavaScript 中的 new 关键字

119 阅读3分钟

前言

  • 前端工程师应该明白,new 关键字到底做了什么事情。当然这也是一道常见的手撕面试题。

构造函数返回值不同

  • 在手撕 new 关键字前,我们先来看下在构造函数返回值不同时,所创建出来的实例对象也会有所不同。
function Person(name, age) {
  this.name = name;
  this.age = age;

  // 情况一:返回对象
  return {};

  // 情况二: 返回数组
  // return []

  // 情况三: 返回函数
  // return function () {}

  // 情况四: 返回 undefined
  // return undefined

  // 情况五: 返回 null
  // return null;
}
Person.prototype.say = function () {
  console.log("说话了");
};
let p = new Person("peiqi", 22);
console.log(p); // 此处输出的实例对象 p 会受 Person 构造函数的返回值影响
// p.say();
  • 上述代码中, Person 构造函数返回值实例对象 p 的对应关系如下情况:

      1. 返回对象:实例对象 p 为该对象
      1. 返回数组:实例对象 p 为该数组
      1. 返回函数: 实例对象 p 为该函数
      1. 返回 undefined:实例对象 p 不变, 也就是跟 没有 return 语句 的结果一致
      1. 返回 null:实例对象 p 不变, 也就是跟 没有 return 语句 的结果一致

手撕 new 关键字

  • 了解了上面的内容后, 下面我们就开始手写实现 new 关键字, 其实 new 关键字做了这些事情:
      1. 首先创建一个空对象, 这个对象将会作为执行构造函数之后返回的对象实例。
      1. 使上面创建的空对象的原型(即 __proto__) 指向构造函数的 prototype 属性, 这样实例对象就可以调用原型对象上的方法。
      1. 调用构造函数,同时使用 apply() 修改构造函数中的 this 指向新创建的空对象,这样就可以把属性挂载到空对象上。
      1. 最后判断构造函数执行的结果, 如果结果是引用数据类型(如对象、数组或函数), 那么就返回该构造函数的执行结果。否则就返回第一步所创建的空对象。
  • 由于 new 是 JavaScript 的关键字, 因此我们不能直接将其覆盖, 但可以通过实现一个 MockNew() 来进行模拟。我们对 MockNew() 预期的使用方式如下:
function Person(name, age) {
  this.name = name;
  this.age = age;
}

const p = MockNew(Person, "peiqi", 22);
console.log(p); // 预期输出: { name: 'peiqi', age: 22 }

方式一

根据预期的使用方式, 我们可以实现 MockNew, 代码如下:

function MockNew() {
  // 取出 arguments 的第一个参数, 即目标构造函数
  let Constructor = [].shift.call(arguments);

  // 当然你也可以使用如下方式, 其实是等效的, 都是把类数组 arguments 转为数组, 同时取出第一个参数
  // let Constructor = Array.prototype.shift.call(arguments)

  // Object.create() 返回一个空对象 obj, 且会使 obj.__proto__ 指向 Object.create() 的第一个参数
  // 即实现了 obj.__proto__ === Constructor.prototype
  let obj = Object.create(Constructor.prototype);

  // 执行构造函数, 使用 apply() 修改构造函数中 this 指向 obj, 并且得到构造函数返回结果
  let res = Constructor.apply(obj, arguments);

  // 如果构造函数执行后, 返回结果是复杂数据类型(如对象、数组或函数), 则直接将该结果返回, 否则返回 obj 对象
  return res instanceof Object ? res : obj;
}

方式二

除了上面一种实现方式以外, 还有如下的实现方式,但其实本质都是一样:

function MockNew(Constructor, ...args) {
  let obj = {};
  obj.__proto__ = Constructor.prototype;
  let res = Constructor.apply(obj, args);

  // 只要是对象、数组或函数类型,那么都是返回 res
  // 注意该行代码一定要想明白:(res !== 'null' && typeof res === 'object') || typeof res === 'function' 表示就是引用类型
  if (
    (res !== "null" && typeof res === "object") ||
    typeof res === "function"
  ) {
    return res;
  }
  return obj;
}

图解

如果你对上面的代码还是想不明白, 你可以参考如下的图解, 相信结合图解, 你会更加清晰:

1111.png

最后补充

  • 最后补充一些小知识点:
      1. __proto__ 是隐式原型,所以当某些方法定义在原型上的时候,我们直接调用,不用写 __proto__, 内部会帮我们加上。
      1. 当通过 对象.方法 的方式调用对象上的方法时,会先在自身找,找到则直接使用,没有的话则去 __proto__ 上找