一道面试题引发的思考:理解 new 运算符
Category(分类): JavaScript Status: 已整理
本文保留原文从一道面试题理解
new的思路,并按 ECMAScript 当前术语修正“构造函数一定返回实例”等容易误导的说法。原文配图已下载到同目录images文件夹,图中文字属于历史示意。
一、原题:为什么不是 'Tom'
function Person(name) {
this.name = name
return name
}
const person = new Person('Tom')
console.log(person) // Person { name: 'Tom' }
console.log(person instanceof Person) // true
Person 明确返回了字符串 'Tom',但 new Person('Tom') 的结果仍然是新创建的实例。因为使用 new 调用构造函数时,只有对象或函数返回值会替代默认实例;原始值(包括字符串、数字、布尔值、undefined、null 和 symbol 等)会被忽略。

如果不使用 new,那就是一次普通函数调用。普通调用的 this 由调用形式和严格模式决定,返回值则直接由 return 决定:
function Person(name) {
this.name = name
return name
}
const receiver = {}
const result = Person.call(receiver, 'Tom')
console.log(result) // 'Tom'
console.log(receiver.name) // 'Tom'
原文直接写 Person('Tom') 在非严格的浏览器经典脚本中还可能把 this 指向全局对象,在严格模式、ES 模块或 Node.js 模块中则会得到不同结果,甚至抛出异常。因此不要用“直接调用构造函数”的结果推断 new 的行为。
二、new 到底做了什么
对一个可构造的函数执行 new Constructor(arg1, arg2),可以用下面的近似过程理解:
- 创建一个新对象。
- 将新对象的
[[Prototype]]设置为构造函数的prototype属性(如果该属性不是对象,则按构造函数所属 realm 的规则回退到对应的%Object.prototype%;普通同 realm 示例中就是Object.prototype)。 - 以新对象作为
this调用构造函数,并传入参数。 - 如果构造函数返回对象或函数,就把该返回值作为
new表达式的结果;否则返回第 1 步创建的对象。
这是一种便于学习的模型,规范中的正式过程由 [[Construct]]、GetPrototypeFromConstructor 和 OrdinaryCreateFromConstructor 等内部操作共同完成。
function Person(name) {
this.name = name
}
const person = new Person('Fe')
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
console.log(person.name) // Fe
console.log(person.constructor === Person) // true(默认 prototype 未被替换时)
返回对象、原始值和 null
function NoReturn(name) {
this.name = name
}
function ReturnPrimitive(name) {
this.name = name
return name
}
function ReturnObject(name) {
this.name = name
return { kind: 'replacement' }
}
function ReturnNull(name) {
this.name = name
return null
}
const p1 = new NoReturn('aa')
const p2 = new ReturnPrimitive('bb')
const p3 = new ReturnObject('cc')
const p4 = new ReturnNull('dd')
console.log(p1.name) // aa
console.log(p2.name) // bb:字符串返回值被忽略
console.log(p3) // { kind: 'replacement' }
console.log(p4.name) // dd:null 也不会替代默认实例
这里“对象”应理解为规范中的 object value,函数也属于对象的一种。return function () {} 同样会替代默认实例。
function ReturnFunction() {
return function replacement() {}
}
const value = new ReturnFunction()
console.log(typeof value) // 'function'
三、原文模拟实现的改进版
原文中的实现使用了 _proto_、arguments 和 cons.apply,可以帮助理解早期 JavaScript 的写法,但存在几个问题:
- 规范属性名是
[[Prototype]],__proto__只是 Web 兼容遗留访问器,不应作为首选 API。 typeof ret === 'object'会把null错误地当成可返回对象。- 没有检查第一个参数是否可构造。
cons.prototype不是对象时,不能直接把它交给原型设置操作。apply版本不能完整模拟原生new的所有内部行为。
现代 JavaScript 中,可以用 Reflect.apply 写一个更接近规范模型的教学实现:
function isObject(value) {
return (typeof value === 'object' && value !== null) || typeof value === 'function'
}
function newLike(Constructor, ...args) {
if (typeof Constructor !== 'function') {
throw new TypeError('Constructor must be callable')
}
const prototype = isObject(Constructor.prototype)
? Constructor.prototype
: Object.prototype
const instance = Object.create(prototype)
const result = Reflect.apply(Constructor, instance, args)
return isObject(result) ? result : instance
}
function Person(name) {
this.name = name
return { replacement: true }
}
const person = newLike(Person, 'Fe')
console.log(person) // { replacement: true }
newLike 主要是教学模型,不是对引擎 [[Construct]] 的完整替代。它只检查了“可调用”,没有实现规范的 IsConstructor;因此传入箭头函数时会普通调用并错误地返回实例,而真实的 new Arrow() 必须抛出 TypeError。类构造器、Map/Set 等内置构造器、new.target、代理和跨 realm 构造器也不能用 Reflect.apply 完整模拟。
const Arrow = () => {}
try {
new Arrow()
} catch (error) {
console.log(error instanceof TypeError) // true
}
class User {
constructor(name) {
this.name = name
}
}
console.log(new User('Ada').name) // Ada
try {
User('Ada')
} catch (error) {
console.log(error instanceof TypeError) // true
}
四、prototype、[[Prototype]] 与 constructor
构造函数的 prototype 是一个普通属性。用 new 创建对象时,构造函数的 prototype 如果是对象,新对象通常会以它为原型;对象自身的内部 [[Prototype]] 可以通过 Object.getPrototypeOf 观察:
function Person(name) {
this.name = name
}
const person = new Person('Ada')
console.log(Person.prototype === Object.getPrototypeOf(person)) // true
console.log(Person.prototype.constructor === Person) // true
如果整个 prototype 被替换,需要手动决定是否保留 constructor 信息;constructor 只是原型上的普通属性,不是引擎识别实例类型的可靠依据:
function Person() {}
Person.prototype = { kind: 'person' }
const person = new Person()
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
console.log(person.constructor === Object) // true:constructor 从 Object.prototype 继承而来
Object.defineProperty(Person.prototype, 'constructor', {
value: Person,
writable: true,
configurable: true,
enumerable: false,
})
console.log(person.constructor === Person) // true
判断原型关系时,优先使用 instanceof、Object.getPrototypeOf 或明确的品牌检查,而不要只依赖可被覆盖的 constructor 属性。
五、new.target 和构造函数设计
new.target 可以判断函数是否通过构造方式调用,也可用于阻止基类直接实例化:
function AbstractModel() {
if (new.target === AbstractModel) {
throw new TypeError('AbstractModel cannot be constructed directly')
}
}
function User(name) {
AbstractModel.call(this)
this.name = name
}
Object.setPrototypeOf(User.prototype, AbstractModel.prototype)
const user = new User('Ada')
console.log(user.name) // Ada
在实际项目中,优先使用 class、明确的工厂函数或普通对象组合,而不是为了模拟其他语言的类层次结构而堆叠多层构造函数。new 只解决“创建并初始化对象”的一种调用形式,不会自动解决状态共享、继承设计或资源释放问题。
六、容易混淆的细节
1. 箭头函数不能作为构造器
箭头函数没有自己的 prototype,也没有 [[Construct]],所以不能使用 new。
2. 类构造器必须使用 new
类的构造器在没有 new 的情况下调用会抛出 TypeError。这与传统普通函数可以被直接调用不同。
3. new 的返回值不是“永远是实例”
如果构造函数返回一个对象或函数,返回值会替代实例。若构造函数返回原始值或 null,才会保留默认实例。
4. prototype 不是实例的原型本身
Person.prototype 是构造函数的一个属性;实例的内部原型是 Object.getPrototypeOf(instance)。两者经常相等,但概念不同。
总结
原文面试题的答案不是 'Tom',而是 Person 实例,因为字符串是原始值,不能替代 new 创建的对象。记住下面的判断即可:
const result = new Constructor(...args)
// Constructor 返回对象或函数:result 是返回值
// Constructor 返回原始值、null 或没有返回:result 是新实例
参考资料:
作者:fe。原文链接:一道面试题引发的思考:理解 new 运算符。