new 运算符原理

核心原理

当代码 new Foo(...) 执行时,会发生以下事情:

  1. 创建一个继承自 Foo.prototype 的新对象
  2. 调用构造函数 Foo,并将 this 绑定到新创建的对象。
  3. 如果构造函数返回了一个对象,则该对象会成为 new 表达式的结果。
  4. 否则,如果构造函数没有返回对象(返回原始值或无返回值),则 new 表达式的结果是步骤 1 中创建的对象。

详细步骤解析

我们可以把 new 的过程拆解为如下步骤:

  1. 创建一个空的简单 JavaScript 对象(即 {});
  2. 为步骤 1 新创建的对象添加属性 __proto__,将该属性链接至构造函数的原型对象;
  3. 将步骤 1 新创建的对象作为 this 的上下文;
  4. 如果该函数没有返回对象,则返回 this

手写实现 (Handwriting)

在面试中,手写 new 是一个非常高频的考点。我们可以通过编写一个 myNew 函数来模拟 new 运算符的行为。

/**
 * 模拟 new 操作符
 * @param {Function} Constructor 构造函数
 * @param {...any} args 构造函数的参数
 */
function myNew(Constructor, ...args) {
  // 1. 创建一个新对象,并将其原型指向构造函数的 prototype
  // 方法一:使用 Object.create (推荐)
  // const obj = Object.create(Constructor.prototype);
  
  // 方法二:使用 __proto__ (不推荐,非标准)
  const obj = {};
  obj.__proto__ = Constructor.prototype;

  // 2. 执行构造函数,并将 this 绑定到新对象上
  const result = Constructor.apply(obj, args);

  // 3. 处理返回值
  // 如果构造函数返回的是一个对象(引用类型,且不为 null),则返回该对象
  // 否则返回我们创建的新对象 obj
  // 注意:typeof null === 'object',所以需要排除 null
  const isObject = typeof result === 'object' && result !== null;
  const isFunction = typeof result === 'function';

  return (isObject || isFunction) ? result : obj;
}

测试用例

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHi = function() {
  console.log(`Hi, I am ${this.name}, ${this.age} years old.`);
};

// 正常情况
const p = myNew(Person, 'Tom', 20);
console.log(p.name); // "Tom"
console.log(p.age);  // 20
p.sayHi();           // "Hi, I am Tom, 20 years old."

// 构造函数返回对象的情况
function Conflict(name) {
  this.name = name;
  return { name: 'Conflict Object' };
}

const c = myNew(Conflict, 'Tom');
console.log(c.name); // "Conflict Object"

面试要点

  1. 连接原型链:新对象的 __proto__ 需要指向构造函数的 prototype
  2. 改变 this 指向:使用 applycall 执行构造函数。
  3. 返回值判断:这是容易遗漏的点。构造函数可以显式返回一个对象,如果是这样,new 的结果应该是这个返回的对象,而不是新创建的实例。如果返回原始值(undefined, null, number, boolean, string, symbol),则忽略,仍然返回新实例。