Skip to content

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);

这是最关键的一步:

  • 构造函数 Cthis强制绑定到新对象 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 构造器不能省略 constructor
  • extends 触发 [[HomeObject]]super 绑定

一句话总结

new 操作符的本质,是触发函数的 [[Construct]] 内部方法,执行一套四步协议:

  1. 创建空对象
  2. 设置 [[Prototype]]Constructor.prototype
  3. 以新对象为 this 调用构造函数
  4. 根据返回值决定最终实例
  5. 它不是语法糖,而是一套精密的对象构造协议,是 JavaScript 原型继承系统的基石。