Skip to content

new 关键字的四步执行模型

new 常被概括为“创建对象、绑定 this、返回实例”,但更准确的理解方式是把它拆回规范中的执行步骤和边界条件。


规范层面的四步分解

ECMAScript §10.2.1 定义了 [[Construct]] 内部方法的行为。简化后:

text
new Constructor(...args) 的执行:
1. 创建一个新的普通对象 obj
2. 将 obj.[[Prototype]] 设为 Constructor.prototype
3. 以 obj 为 this 调用 Constructor.[[Call]](obj, ...args) → result
4. 如果 result 是对象(包括函数)→ 返回 result
   否则(result 是原始值或 Constructor 没有 return)→ 返回 obj

第 4 步是理解的关键:构造函数的显式返回值只在使用 return <引用类型> 时生效,return <原始值> 被忽略。

js
function A() { this.x = 1; return 2; }
function B() { this.x = 1; return { x: 2 }; }

new A().x; // 1 —— return 2(原始值)被忽略,返回新对象
new B().x; // 2 —— return { x: 2 }(对象)覆盖了默认返回值

这个设计的原因:面向对象的一致性。 new 的语义是"创建该类型的实例"。如果 return 123 生效,new Foo() 返回一个 number,那它就不是 Foo 类型的实例了,instanceof、原型方法调用全部失效。引擎选择忽略原始值返回值来保证 new 始终返回对象。


手写 new 的实现与边界

js
function myNew(constructor, ...args) {
  // 步骤 1+2:创建对象并设置原型
  const obj = Object.create(constructor.prototype);
  // 步骤 3:执行构造函数
  const result = constructor.apply(obj, args);
  // 步骤 4:处理返回值
  return (typeof result === 'object' && result !== null) || typeof result === 'function'
    ? result
    : obj;
}

几个值得讨论的边界:

typeof result === 'function' 也要返回。 规范对"对象"的定义包括函数(ECMAScript 中函数是对象的一种)。如果构造函数返回一个函数,new 会返回这个函数而不是默认对象。这种写法的实用场景:返回一个绑定了私有状态的函数(工厂模式混用构造器)。

constructor.prototype 可能不是对象。 如果构造函数的 prototype 属性被设置为 null 或原始值(如 Foo.prototype = 42),Object.create(constructor.prototype) 会抛 TypeError。但真正的 new 不会——它会用 Object.prototype 作为新对象的原型。这是 myNew 和原生 new 的一个行为差异:

js
function Foo() { this.x = 1; }
Foo.prototype = 42;
const obj = new Foo(); // obj 的原型是 Object.prototype,不是 42
Object.getPrototypeOf(obj) === Object.prototype; // true

new.target:引擎如何知道是通过 new 调用的

new.target 是一个只能在函数内部访问的元属性。通过 new 调用时,它指向被调用的构造函数;普通调用时,它是 undefined

引擎实现:每次函数调用,JS 引擎创建一个新的 Execution Context,其中包含一个 [[NewTarget]] 字段。new 调用路径中,这个字段被设为当前构造函数;普通调用路径([[Call]])中,它保持为 undefined

在 V8 中:

text
new Foo()
  → Builtins::kConstruct
  → 创建 ExecutionContext,设置 new_target = Foo
  → 调用 Foo.[[Construct]]

Foo()
  → Builtins::kCall
  → 创建 ExecutionContext,new_target = undefined
  → 调用 Foo.[[Call]]

Reflect.construct(Foo, args, Bar) 可以覆盖 new.target

js
function Foo() { console.log(new.target); }
function Bar() {}

new Foo();                // [Function: Foo]
Reflect.construct(Foo, [], Bar); // [Function: Bar]

这在创建继承链但是不直接使用 class extends 时有用——比如从内置类(Array、Error)正确继承时需要 Reflect.construct 配合 new.target


原始类型的自动装箱

js
const a = 1;
a.__proto__; // 不报错,返回 Number.prototype

看起来原始类型也有原型?不是。这里发生的是自动装箱

  1. JS 引擎遇到 a.__proto__. 是属性访问运算符)
  2. 属性访问运算符要求操作数是对象
  3. 引擎临时将 a 装箱为 new Number(1)
  4. 访问 new Number(1).__proto__ → 返回 Number.prototype
  5. 装箱对象立即被 GC

这个过程在字节码层面由 ToObject 抽象操作完成。undefinednull 没有对应的包装类型,所以 undefined.__proto__TypeError,而 1..__proto__(两个点,第一个是小数点)不报错。

new 返回的包装对象 vs 字面量原始值:

js
const a = 1;              // typeof: 'number',原始值
const b = new Number(1);   // typeof: 'object',Number 实例

a === 1;        // true
b === 1;        // false —— b 是对象,不是原始值
+a === +b;      // true —— 通过隐式转换比较数值

new 不能用于哪些函数

  • 箭头函数:没有 [[Construct]] 内部方法,没有 prototype 属性
  • Symbol() / BigInt():规范明确禁止(SymbolBigInt 不是构造函数)
  • Proxy(无 construct trap 时):如果 Proxy 的 target 不可构造,new Proxy(target, {})TypeError
  • class 的方法:类方法(包括 static)没有 [[Construct]]

检查一个函数是否可以用 new 调用:

js
function isConstructable(fn) {
  try {
    Reflect.construct(Object, [], fn);
    return true;
  } catch {
    return false;
  }
}

Reflect.construct(Object, [], fn) 中的 fn 作为 new.target 参数。如果 fn 不可构造,抛 TypeError