Appearance
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; // truenew.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看起来原始类型也有原型?不是。这里发生的是自动装箱:
- JS 引擎遇到
a.__proto__(.是属性访问运算符) - 属性访问运算符要求操作数是对象
- 引擎临时将
a装箱为new Number(1) - 访问
new Number(1).__proto__→ 返回Number.prototype - 装箱对象立即被 GC
这个过程在字节码层面由 ToObject 抽象操作完成。undefined 和 null 没有对应的包装类型,所以 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():规范明确禁止(Symbol和BigInt不是构造函数)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。
