Skip to content

构造器返回值的诡异行为

--当 return {}return 1 时,new 到底返回什么?

new 操作符的返回值,并不由构造函数完全决定——它是一场 this 实例与 return 值之间的‘协议博弈’。”

当你写下:

js
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:无返回 / 返回原始值 → 返回新实例

js
function Foo() {
  this.name = 'Alice';
  return 1; // 原始值
}
const f = new Foo();
console.log(f);           // Foo { name: 'Alice' }
console.log(f instanceof Foo); // true

return 1 被忽略,new 返回新对象。

案例 2:返回对象 → 返回该对象,抛弃 this

js
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:返回函数 → 同样被接受

js
function Baz() {
  this.name = 'Alice';
  return function() {}; // 函数也是对象
}
const z = new Baz();
console.log(typeof z);        // 'function'
console.log(z instanceof Baz); // false

函数是对象,所以 new 返回函数,抛弃实例。

案例 4:返回数组 → 同样“叛逃”

js
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)        (正常返回实例)

🔥 这就是“构造器返回对象会覆盖实例”的协议级原因

四、边界案例:nullundefined 会被忽略吗?

js
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' }

nullundefined 属于“原始值处理路径”
所以 new 忽略它们,返回新实例。

规范中“对象”指的是 typeof x === 'object' && x !== nulltypeof x === 'function'

五、class 构造器中的行为:更严格的“默认返回”

class 中,如果你不写 return默认返回 this

js
class MyClass {
  constructor() {
    this.name = 'Alice';
    // 隐式 return this;
  }
}
const obj = new MyClass(); // 正常返回实例

但如果你显式 return 一个对象:

js
class MyOtherClass {
  constructor() {
    this.name = 'Alice';
    return { name: 'Bob' };
  }
}
const o = new MyOtherClass();
console.log(o); // { name: 'Bob' }

即使是 class,也无法阻止“对象优先”规则。

class 只是语法糖,底层仍遵循 [[Construct]] 协议。

六、手写 new:如何实现这一“叛逃逻辑”

js
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. 实现“单例模式”的早期方案

js
let instance = null;
function Singleton() {
  if (instance) return instance;
  this.name = 'Only One';
  instance = this;
  return instance;
}

虽然现代用 Symbol 或模块私有变量更好,但这是早期 JS 的常见模式。

2. 允许“降级为普通对象”

js
function Config() {
  if (someCondition) {
    return { mode: 'safe', debug: false }; // 直接返回配置对象
  }
  this.mode = 'normal';
}

3. 与 Promise 构造器的兼容性

js
new Promise((resolve, reject) => {})

Promise 构造器内部会返回一个包装对象,但这是由引擎控制的,不是用户 return 的。

八、最佳实践:避免显式 return 对象

尽管语言允许,但显式 return 对象会破坏 instanceof 检查,导致类型系统混乱

推荐做法:

js

// ❌ 避免
function Bad() {
  return { x: 1 };
}

// ✅ 正确
function Good() {
  this.x = 1;
  // 不要 return 对象
}

如果需要返回特定对象,应使用工厂函数

js
function createSpecialObject() {
  return { type: 'special' };
}

一句话总结

new 操作符的返回值遵循“对象优先”原则:

  • 构造函数返回 对象(包括函数、数组)new 返回该对象,抛弃新实例
  • 返回 原始值或 null/undefinednew 忽略返回值,返回新创建的实例
  • 这不是 bug,而是 ECMAScript 构造协议的一部分,体现了语言的灵活性,但也带来了类型不安全的风险。