在 JavaScript 中,new 是创建对象实例的关键操作符。它看似简单,背后却涉及对象创建、原型链、函数调用和返回值处理等多个核心机制。本文将不依赖任何现成答案,仅根据 new 的规范行为,一步步推导出如何手写它,并最终给出完整实现。
new 操作符的四个重要步骤
要正确模拟 new 的行为,必须严格遵循以下四个步骤:
- 创建一个新对象(obj)
- 将新对象的原型指向构造函数的 prototype
- 调用构造函数,并将 this 绑定到新对象上
- 返回新对象(除非构造函数显式返回一个非 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)、null、undefined,或没有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 的真正价值。