先记住这个答案
当函数被 new 调用时,会经历四步:创建空对象;将该对象的 [[Prototype]] 指向构造函数的 prototype 属性;以该对象作为 this 执行函数体;若函数返回对象则返回该对象,否则返回之前创建的对象。this 在步骤中被固定为新对象,因此构造函数内可直接给 this 添加属性。
new会创建新对象作为this- 原型关联到构造函数的
prototype - 构造函数返回对象会覆盖实例
四步流程的完整推导
第一步,创建空白对象。此对象是普通对象,其内部 [[Prototype]] 暂未设置。第二步,将该对象的 [[Prototype]] 指向构造函数当前的 prototype 属性。注意如果构造函数的 prototype 不是对象,则新对象保持继承 Object.prototype,这对内置类型如 Number 的构造不常用。
第三步,以该新对象为 this 调用函数,函数体内所有 this 赋值都会写入此对象。第四步,根据返回值分流:若函数返回对象或函数,则整个 new 表达式结果就是该返回值;否则返回首次创建的对象。这解释了为何构造函数极少写返回语句,但若刻意返回对象可覆盖默认实例。
统一实例工厂的构造实现
场景:需要设计一个可配置的日志器构造工厂,允许外部传入多种存储适配器。通过 new Logger(type) 创建实例时,内部根据 type 决定使用内存或本地存储。为了兼容直接调用,代码用 new.target 检测:若未用 new,则自动递归 new 自身,确保 this 绑定正确。
实现时,构造器内 if (!new.target) return new Logger(type),并基于 type 给 this.store 赋值方法。这种设计让调用者不必区分调用形式。关键点在于显示返回对象不会造成问题,因为整个表达式始终返回实际实例,但注意避免在递归时重复检测造成死循环。
失效条件与处理代价
边界一:当构造函数的 prototype 被重新赋值为原始类型时,新对象的原型会退化为 Object.prototype,导致原有方法无法访问。这常见于代码动态修改 prototype 后未及时还原,出现实例丢失原型方法的现象,且难以察觉。
处理方案:在构造函数内部用 Object.setPrototypeOf 强制修正或检测原型类型。但代价是每次构造都要额外代码,且影响性能。更稳妥的是,保持构造函数 prototype 始终为对象,避免重新赋值为非对象。若必须动态修改,建议使用 Object.create 替代 new 模式。
容易答错的地方
- 认为构造函数不能返回对象
- 很多人以为构造器内写
return没意义,实际上显式返回对象会替换默认实例。例如返回{ custom: true }时,即使用new,结果也不是this生成的对象。这是合法覆盖机制,不是错误。 - 忽略 `new.target` 区分调用方式
- 普通函数调用与
new调用时this完全不同,但不少开发者直接用this全局对象。用new.target可判断是否通过new调用,从而避免误污染全局。类若无new会直接抛错,但普通函数不会。
面试官还会怎么问?
箭头函数能否用 `new` 调用?为什么?
不能。箭头函数没有 [[Construct]] 内部方法,也没有 prototype 属性,所以用 new 调用会抛出 TypeError。箭头函数中的 this 来自词法环境,不受 new 影响。
`new.target` 在继承场景中如何表现?
在构造函数中,new.target 指向实际被 new 调用的函数,而非继承链上的父类。若子类构造内调用 super(),new.target 保持为子类,父类可使用它判断实例来源。
`Reflect.construct` 与 `new` 有什么区别?
Reflect.construct 允许指定新对象的原型,可模拟某些特殊继承;它不依赖函数自身的调用形式,且可通过 newTarget 参数控制 new.target 的值。但内部步骤相同,均遵循创建、原链、this、返回值。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。