先记住这个答案
普通构造函数执行时,new 先准备一个实例,让函数里的 this 指向它。函数结束后,如果显式返回的是对象或函数,new 就交出这个返回值;没有返回值,或返回数字、字符串、null 等原始值,就交出原先的实例。类也能返回对象,但派生类对原始返回值有额外限制,不能照搬普通函数的规则。
- 返回对象或函数会替换
new创建的实例 - 普通构造函数返回原始值会被忽略
null是原始值,不会替换实例- 派生类显式返回其他原始值会抛错
换个返回值,结果就变了
最容易记错的是 null。对它使用 typeof 虽然得到 'object',它仍是原始值,不会替换实例。函数则相反,typeof 是 'function',但它属于对象,可以替换实例。
function Create(kind) {
this.fromThis = true
if (kind === 'object') return { fromReturn: true }
if (kind === 'function') return function returned() {}
return null // 原始值,被忽略
}
const a = new Create('object')
const b = new Create('function')
const c = new Create('null')
console.log(a.fromThis, a.fromReturn)
console.log(typeof b, b instanceof Create)
console.log(c.fromThis, c instanceof Create)查看输出与解释
undefined true
function false
true truea 是函数返回的对象,原先写在 this 上的 fromThis 没跟过去。b 是函数,原型链也不是 Create.prototype 那条,所以这里的 instanceof 是 false。c 返回了 null,实际拿到的仍是默认实例。把 null 换成 0、'hello' 或 false,普通构造函数的结果也一样。
返回已有对象,调用方会看到什么
有人会利用这条规则做单例:第二次调用构造函数时直接返回上一次创建的对象。它确实能工作,但调用方看到的是缓存对象,这一次传入的参数可能根本不会生效。
let cached
function Config(options) {
if (cached) return cached // 替换这次 new 创建的实例
this.options = options
cached = this
}
const first = new Config({ theme: 'dark' })
const second = new Config({ theme: 'light' })
console.log(first === second)
console.log(second.options.theme)查看输出与解释
true
dark第二次 new 已经准备了临时实例,但构造函数返回 cached 后,它就被丢弃了。第二次的选项没有覆盖第一次的配置。更要小心的是,返回值可以是任意对象;如果它不是由这个构造函数创建的,instanceof Config 可能是 false。业务代码里若需要复用对象,明确写一个工厂函数或模块级缓存,通常比让 new 暗中复用更容易读懂。
派生类为什么要单独记
普通 class 的基类构造器可以返回对象来替换实例,返回原始值则被忽略。派生类不同:显式返回对象是允许的,甚至能跳过 super();但是得到的只是那个对象,不会自动带上父类和子类的实例字段。显式返回数字、字符串、null 等非对象且非 undefined 的值会抛 TypeError。返回 undefined 不算替换实例,仍须先调用 super() 才能正常结束。
比如 class Child extends Parent { constructor() { return { x: 1 } } } 能构造,但返回的是普通对象。语法里写了 extends,不代表结果一定满足 instanceof Child。如果在返回对象之前读取 this,却没有调用 super(),又会触发初始化错误。面试问到类时,把「能不能替换返回值」和「什么时候能访问 this」分开解释。
容易答错的地方
- 把
null当成对象返回值 typeof null的历史结果是'object',但语言规范把null归在原始值里。普通构造函数返回它,new得到的仍是默认实例。判断是否会替换实例,要看返回值是否为非null的对象或函数。- 以为返回对象后,
this上的属性也会保留 - 两个对象不会自动合并。返回
{ id: 1 }后,之前写到this.name的值只留在被丢弃的实例上。只有返回的对象本身就是那个this,属性才会保留。 - 把派生类和普通函数混为一谈
- 普通函数返回
0会被忽略;派生类构造器显式返回0会抛TypeError。派生类没有写构造器时,引擎会处理好super(),不要因此误以为所有子类都得手写构造器。
面试官还会怎么问?
返回函数时,new 的结果能调用吗?
能。函数也是对象,所以会替换默认实例。拿到的值就是那个可调用函数,除非另行设置原型链,否则它不是原构造函数的实例。
箭头函数能用 new 吗?
不能。箭头函数没有构造能力,new (() => {}) 会抛 TypeError。返回对象的规则只在目标本身能够作为构造函数时才有机会执行。
new.target 能判断结果有没有被替换吗?
不能。new.target 表示这次构造由哪个构造器发起,不会因为最终返回了别的对象就改变。要确认结果的属性和原型,得直接检查返回对象本身。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。