构造器返回值的诡异行为
--当 return {} 或 return 1 时,new 到底返回什么?
“new 操作符的返回值,并不由构造函数完全决定——它是一场 this 实例与 return 值之间的‘协议博弈’。”
当你写下:
function MyClass() {
this.name = 'Alice';
return something;
}
const obj = new MyClass();你可能以为 obj 一定是 MyClass 的实例。
但真相是:new 操作符会检查构造函数的返回值,如果它是一个对象,就会“抛弃”原本创建的实例,转而返回这个对象。
这背后,是一套被大多数人忽视的 “构造协议优先级规则”。
我们来从 规范定义、执行流程、边界案例、设计哲学 四个维度,彻底拆解这场“返回值的叛逃”。
一、核心规则:ECMAScript 的“对象优先”原则
来自 ECMA-262 §13.3.7 NewExpression:
当 new C(...args) 执行完毕后,引擎会检查构造函数的返回值:
- 如果返回值是 对象(Object 或 Function) → 返回该对象
- 如果返回值是 原始值(number, string, boolean, null, undefined)或无返回 → 返回新创建的实例
🔥 new 不是无条件返回 this,而是“有条件地尊重返回值”。
二、实战对比:不同返回值的行为差异
案例 1:无返回 / 返回原始值 → 返回新实例
function Foo() {
this.name = 'Alice';
return 1; // 原始值
}
const f = new Foo();
console.log(f); // Foo { name: 'Alice' }
console.log(f instanceof Foo); // truereturn 1 被忽略,new 返回新对象。
案例 2:返回对象 → 返回该对象,抛弃 this
function Bar() {
this.name = 'Alice';
return { name: 'Bob' }; // 对象
}
const b = new Bar();
console.log(b); // { name: 'Bob' }
console.log(b instanceof Bar); // false原始实例被完全抛弃,b 不再是 Bar 的实例。
案例 3:返回函数 → 同样被接受
function Baz() {
this.name = 'Alice';
return function() {}; // 函数也是对象
}
const z = new Baz();
console.log(typeof z); // 'function'
console.log(z instanceof Baz); // false函数是对象,所以 new 返回函数,抛弃实例。
案例 4:返回数组 → 同样“叛逃”
function Qux() {
this.name = 'Alice';
return [1, 2, 3];
}
const q = new Qux();
console.log(q); // [1, 2, 3]
console.log(q instanceof Qux); // false数组是对象,所以 new 返回数组。
三、图解:new 如何“审判”返回值
new MyClass()
↓
[1] 创建空对象:obj = {}
↓
[2] 设置原型:obj.[[Prototype]] = MyClass.prototype
↓
[3] 执行构造函数:result = MyClass.call(obj)
↓
[4] 检查 result 类型
┌─────────────┐
│ 是对象? │
└─────┬───────┘
│
是 │ 否
↓ ↓
return result return obj
(抛弃 obj) (正常返回实例)🔥 这就是“构造器返回对象会覆盖实例”的协议级原因。
四、边界案例:null 和 undefined 会被忽略吗?
function A() {
this.name = 'A';
return undefined;
}
function B() {
this.name = 'B';
return null;
}
const a = new A(); // A { name: 'A' }
const b = new B(); // B { name: 'B' }null 和 undefined 属于“原始值处理路径”,
所以 new 忽略它们,返回新实例。
规范中“对象”指的是 typeof x === 'object' && x !== null 或 typeof x === 'function'。
五、class 构造器中的行为:更严格的“默认返回”
在 class 中,如果你不写 return,默认返回 this。
class MyClass {
constructor() {
this.name = 'Alice';
// 隐式 return this;
}
}
const obj = new MyClass(); // 正常返回实例但如果你显式 return 一个对象:
class MyOtherClass {
constructor() {
this.name = 'Alice';
return { name: 'Bob' };
}
}
const o = new MyOtherClass();
console.log(o); // { name: 'Bob' }即使是 class,也无法阻止“对象优先”规则。
class 只是语法糖,底层仍遵循 [[Construct]] 协议。
六、手写 new:如何实现这一“叛逃逻辑”
function myNew(Constructor, ...args) {
// 1. 创建空对象
const obj = {};
// 2. 设置原型
Object.setPrototypeOf(obj, Constructor.prototype);
// 3. 调用构造函数,获取返回值
const result = Constructor.apply(obj, args);
// 4. 判断返回值类型
const isObject = typeof result === 'object' && result !== null;
const isFunction = typeof result === 'function';
if (isObject || isFunction) {
return result; // “叛逃”:返回构造函数的返回值
}
return obj; // 正常:返回新实例
}这就是 new 的“灵魂判官”逻辑。
七、为什么这样设计?背后的“灵活性”哲学
你可能觉得这很“诡异”,但这种设计有其历史和实用价值:
1. 实现“单例模式”的早期方案
let instance = null;
function Singleton() {
if (instance) return instance;
this.name = 'Only One';
instance = this;
return instance;
}虽然现代用 Symbol 或模块私有变量更好,但这是早期 JS 的常见模式。
2. 允许“降级为普通对象”
function Config() {
if (someCondition) {
return { mode: 'safe', debug: false }; // 直接返回配置对象
}
this.mode = 'normal';
}3. 与 Promise 构造器的兼容性
new Promise((resolve, reject) => {})Promise 构造器内部会返回一个包装对象,但这是由引擎控制的,不是用户 return 的。
八、最佳实践:避免显式 return 对象
尽管语言允许,但显式 return 对象会破坏 instanceof 检查,导致类型系统混乱。
推荐做法:
// ❌ 避免
function Bad() {
return { x: 1 };
}
// ✅ 正确
function Good() {
this.x = 1;
// 不要 return 对象
}如果需要返回特定对象,应使用工厂函数:
function createSpecialObject() {
return { type: 'special' };
}一句话总结
new 操作符的返回值遵循“对象优先”原则:
- 构造函数返回 对象(包括函数、数组) →
new返回该对象,抛弃新实例 - 返回 原始值或
null/undefined→new忽略返回值,返回新创建的实例 - 这不是 bug,而是 ECMAScript 构造协议的一部分,体现了语言的灵活性,但也带来了类型不安全的风险。