new 操作符详解
从 [[Construct]] 到原型绑定,一步步拆解对象创建过程
“new 不是一个语法糖,而是一套完整的对象构造协议。”
当你写下:
js
const obj = new MyClass(arg1, arg2);你可能以为这只是“调用构造函数并返回实例”。
但真相是:
你触发了一套由 ECMAScript 规范定义的、精密的、多阶段的构造协议——
它涉及:
- 内部方法
[[Construct]] - 执行上下文切换
- 原型链初始化
- 构造器返回值的特殊处理
我们来逐行拆解这个过程。
一、前置知识:new 调用的本质是 [[Construct]] 协议
在 ECMAScript 中,函数有两种调用方式:
| 调用形式 | 触发的内部方法 | 行为 |
|---|---|---|
fn() | [[Call]](thisArgument, argumentsList) | 普通函数调用 |
new fn() | [[Construct]](argumentsList, newTarget) | 构造对象 |
new 不是“语法糖”,而是切换了函数的“执行协议”。
这意味着:
- 一个函数能否被
new调用,取决于它是否实现了[[Construct]] - 箭头函数没有
[[Construct]]→ 不能用new class方法默认不可构造 → 不能new
二、new 操作符的四步协议
当执行 new C(...args) 时,引擎会执行以下步骤:
第一步:创建一个空对象(Ordinary Object)
js
let obj = {};但这不是普通的 {},它是:
- 一个“普通对象”(Ordinary Object)
- 内部有
[[Prototype]]、[[Extensible]]等内部槽 - 尚未与构造函数关联
第二步:将该对象的 [[Prototype]] 指向 C.prototype
js
Object.setPrototypeOf(obj, C.prototype);
// 或等价于:obj.__proto__ = C.prototype;这是原型继承的起点。
这意味着:
obj现在可以访问C.prototype上的所有方法obj instanceof C将为true- 原型链正式建立:
obj ---> C.prototype ---> Object.prototype ---> null
第三步:以 obj 作为 this,调用构造函数 C(...args)
js
C.call(obj, ...args);这是最关键的一步:
- 构造函数
C的this被强制绑定到新对象obj - 构造函数中的
this.x = 1实际上是obj.x = 1 - 参数
...args被传入,用于初始化实例状态
此时,执行的是 C 的 [[Call]] 方法,但 this 已被 new 协议劫持。
第四步:返回该对象(除非构造函数显式返回一个非原始值)
js
return typeof result === 'object' && result !== null ? result : obj;这里有个诡异规则:
| 构造函数返回值 | new 表达式的结果 |
|---|---|
| 不返回 / 返回原始值(number, string, boolean) | 返回 obj(新创建的对象) |
| 返回对象({}、[]、function) | 返回该对象,丢弃 obj |
js
function Foo() {
this.name = 'Alice';
return { name: 'Bob' }; // 返回对象
}
const f = new Foo();
console.log(f.name); // 'Bob' —— 原始 obj 被抛弃!这就是“构造函数返回对象会覆盖实例”的真相。
三、图解:new 的完整流程
new MyClass(a, b)
↓
[Step 1] 创建空对象
obj = {}
↓
[Step 2] 设置原型
obj.[[Prototype]] = MyClass.prototype
↓
[Step 3] 调用构造函数(this 绑定到 obj)
MyClass.call(obj, a, b)
→ 执行 this.x = a; this.y = b;
↓
[Step 4] 检查返回值
result = MyClass(a, b)
↓ 是对象?
┌───┴───┐
是 否
↓ ↓
return result return obj四、手写一个 new:模拟引擎行为
我们可以用 JavaScript 模拟 new 的行为:
js
function myNew(Constructor, ...args) {
// Step 1: 创建空对象
const obj = {};
// Step 2: 设置原型
Object.setPrototypeOf(obj, Constructor.prototype);
// 或:obj.__proto__ = Constructor.prototype;
// Step 3: 调用构造函数,绑定 this
const result = Constructor.apply(obj, args);
// Step 4: 处理返回值
const isObject = typeof result === 'object' && result !== null;
const isFunction = typeof result === 'function';
if (isObject || isFunction) {
return result; // 返回构造函数的返回值
}
return obj; // 默认返回新对象
}
// 测试
function Person(name) {
this.name = name;
}
const p = myNew(Person, 'Alice');
console.log(p.name); // 'Alice'
console.log(p instanceof Person); // true这就是 new 的“灵魂实现”。
五、new 与 [[Construct]] 的关系:为什么箭头函数不能 new?
来自规范:§10.2.1 Runtime Semantics: Evaluation
- 普通函数:同时有
[[Call]]和[[Construct]] - 箭头函数:只有
[[Call]],没有[[Construct]] - 类构造器:有
[[Construct]],但必须用new调用
js
const arrow = () => {};
new arrow(); // ❌ TypeError: arrow is not a constructor因为箭头函数没有 [[Construct]] 内部方法,引擎直接拒绝。
六、new.target:检测是否被 new 调用
ES6 引入了 new.target,用于判断函数是否被 new 调用:
js
function Foo() {
if (!new.target) {
throw new Error('Foo must be called with new');
}
this.name = 'Alice';
}
Foo(); // ❌ 报错
new Foo(); // ✅ 正常new.target 的值:
- 被
new调用时:指向构造函数(Foo) - 普通调用时:
undefined
这是语言层面支持“构造器专用函数”的机制。
七、class 中的 new:更严格的构造协议
在 class 中,new 的行为更加严格:
js
class MyClass {
constructor() {
// 必须用 new 调用
// 不能返回原始值(虽然规范允许,但语义错误)
}
}class构造器必须用new调用class构造器不能省略constructorextends触发[[HomeObject]]和super绑定
一句话总结
new 操作符的本质,是触发函数的 [[Construct]] 内部方法,执行一套四步协议:
- 创建空对象
- 设置
[[Prototype]]为Constructor.prototype - 以新对象为
this调用构造函数 - 根据返回值决定最终实例
- 它不是语法糖,而是一套精密的对象构造协议,是 JavaScript 原型继承系统的基石。