手写 new 操作符:从零推导 JavaScript 构造机制

39 阅读3分钟

在 JavaScript 中,new 是创建对象实例的关键操作符。它看似简单,背后却涉及对象创建、原型链、函数调用和返回值处理等多个核心机制。本文将不依赖任何现成答案,仅根据 new 的规范行为,一步步推导出如何手写它,并最终给出完整实现。


new 操作符的四个重要步骤

要正确模拟 new 的行为,必须严格遵循以下四个步骤:

  1. 创建一个新对象(obj)
  2. 将新对象的原型指向构造函数的 prototype
  3. 调用构造函数,并将 this 绑定到新对象上
  4. 返回新对象(除非构造函数显式返回一个非 null 的对象)

接下来,我们逐条分析每一步的必要性与实现方式。


第一步:创建一个新对象(obj)

new 的首要任务是生成一个全新的空对象,作为后续属性和方法的载体。

这个对象必须是一个普通对象,尚未绑定任何原型或属性。最直接的方式是:

const obj = new Object();

虽然 {} 在效果上等价,但使用 new Object() 更清晰地表达了“创建一个基础对象”的意图,便于理解后续操作。

目标达成:获得一个干净的实例容器。


第二步:将新对象的原型指向构造函数的 prototype

JavaScript 的继承机制基于原型链。所有通过构造函数创建的实例,都应能访问其 prototype 上定义的方法。

为了让 obj 具备这种能力,必须将其内部原型([[Prototype]])指向 constructor.prototype

这个我很容易想到直接将 constructor.prototype拷贝到obj.__proto__上不就好了

obj.__proto__ = constructor.prototype;

目标达成:建立原型链,使 obj 能继承构造函数的原型方法。


第三步:调用构造函数,并将 this 绑定到新对象上

现在对象已创建,原型链也已连接。下一步是执行构造函数,完成实例初始化。

关键在于:构造函数内部的 this 必须指向我们刚创建的 obj

这可以通过 Function.prototype.apply 实现:

const result = constructor.apply(obj, args);
  • apply 调用 constructor
  • obj 作为 this 传入;
  • 并传递用户提供的参数 args

这样,this.name = name 等语句就会作用于 obj

目标达成:执行构造逻辑,为实例赋值。


第四步:返回新对象(除非构造函数显式返回一个非 null 的对象)

这是最容易被误解的规则,但也是 new 行为的关键。

根据 ECMAScript 规范:

  • 若构造函数 返回一个对象(如 {}, [], new Date() 等),则 new 返回该对象;
  • 若返回 原始值(如 1"str"true)、nullundefined,或没有 return,则 new 忽略返回值,返回最初创建的 obj

因此,必须判断返回值是否为“有效的对象”:

if (result !== null && typeof result === 'object') {
    return result;
} else {
    return obj;
}

特别注意:typeof null === 'object',所以需额外排除 null

目标达成:正确处理构造函数的返回值,符合原生 new 行为。


完整代码实现

将上述四步整合,得到最终的手写 new 函数:

function newhand(constructor, ...args) {
    const obj = new Object(); // 步骤1:创建新对象
    obj.__proto__ = constructor.prototype; // 步骤2:设置原型链
    const result = constructor.apply(obj, args); // 步骤3:执行构造函数
    return (result !== null && typeof result === 'object') ? result : obj; // 步骤4:返回结果
}

验证示例

// 示例1:正常构造
function User(name) {
    this.name = name;
}
console.log(newhand(User, 'Tom').name); // "Tom"

// 示例2:返回原始值 → 忽略
function Box() {
    this.value = 100;
    return 42;
}
console.log(newhand(Box).value); // 100

// 示例3:返回对象 → 使用返回值
function Factory() {
    this.type = 'default';
    return { type: 'custom' };
}
console.log(newhand(Factory).type); // "custom"

所有结果均与原生 new 一致。


总结

通过严格遵循 new 的四个核心步骤,我们从零推导出一个功能完整的实现。这不仅是一道面试题,更是深入理解 JavaScript 面向对象机制的钥匙。

掌握这四步,你就真正明白了:

  • 对象是如何被创建的;
  • 原型链是如何建立的;
  • this 是如何被绑定的;
  • 构造函数的返回值为何有特殊规则。

这才是手写 new 的真正价值。