重学 JS 系列:聊聊 new 操作符
Category(分类): JavaScript Status: 未知
配图来自原文页面,已下载并转换为本地 WebP;原始授权未核实,发布前请人工确认。
这是重学 JS 系列的第一篇文章,写这个系列的初衷也是为了夯实自己的 JavaScript 基础。既然是重学,肯定不会从零开始介绍一个知识点;下面保留原文的推导过程,并补充现代规范中的边界。
new 的作用
我们先通过例子了解 new 的作用:
function Test(name) {
this.name = name
}
Test.prototype.sayName = function () {
console.log(this.name)
}
const t = new Test('yck')
console.log(t.name) // yck
t.sayName() // yck
console.log(Object.getPrototypeOf(t) === Test.prototype) // true
从上面的例子中可以得出这些结论:
new通过构造函数Test创建出来的实例,可以访问构造函数中写入this的属性。- 实例的
[[Prototype]]通常会指向Test.prototype,因此可以访问构造函数原型上的方法。 Test.prototype和实例的[[Prototype]]是同一个对象引用,但prototype和[[Prototype]]是两个不同的概念。
构造函数返回原始值
当构造函数没有显式返回值时,new 返回新创建的实例。如果构造函数返回原始值,这个值会被忽略:
function ReturnPrimitive(name) {
this.name = name
return 1
}
const value = new ReturnPrimitive('yck')
console.log(value.name) // yck
console.log(value instanceof ReturnPrimitive) // true
这里的“原始值”包括数字、字符串、布尔值、null、undefined、symbol 和 bigint。原始值不会覆盖 new 创建的实例。
构造函数返回对象
如果构造函数返回对象(包括数组、日期、函数以及 Object.create(null) 创建的对象),这个对象会成为整个 new 表达式的结果:
function ReturnObject(name) {
this.name = name
return { age: 26 }
}
const value = new ReturnObject('yck')
console.log(value) // { age: 26 }
console.log(value.name) // undefined
console.log(value instanceof ReturnObject) // false
“对象”这里不能用 result instanceof Object 简单判断,因为 Object.create(null) 没有 Object.prototype,但它仍然是非原始值,应当被 new 采用:
function ReturnNullPrototypeObject() {
return Object.create(null)
}
const value = new ReturnNullPrototypeObject()
console.log(Object.getPrototypeOf(value)) // null
这两个例子告诉我们:普通构造函数通常不需要显式返回值。返回原始值没有覆盖效果,返回对象则会让
new返回另一个对象。
new 大致做了什么
对于普通的、可构造的函数,可以把 new Constructor(...args) 抽象为以下步骤:
- 创建一个新对象。
- 如果
Constructor.prototype是对象,则把新对象的[[Prototype]]设置为它;如果不是对象,则使用当前 realm 的默认对象原型。 - 执行构造函数,并把新对象作为
this,传入调用参数。 - 如果构造函数返回非原始值,就返回该值;否则返回新对象。
规范内部使用 [[Construct]] 等内部方法实现这一过程。这里的伪代码只是教学模型,不能覆盖 Proxy、类、跨 realm 对象和 new.target 的全部细节。
自己实现一个简化版 new
首先回顾几个要点:
- 需要创建一个对象。
- 需要让它访问构造函数原型上的属性。
- 需要把它作为
this传给构造函数。 - 需要忽略构造函数返回的原始值,但保留返回的对象或函数。
下面的实现适合普通函数构造器的教学演示:
function myNew(Constructor, ...args) {
if (typeof Constructor !== 'function') {
throw new TypeError('Constructor must be a function')
}
const prototype = Constructor.prototype
const instance = Object.create(
prototype !== null && (typeof prototype === 'object' || typeof prototype === 'function')
? prototype
: Object.prototype
)
const result = Reflect.apply(Constructor, instance, args)
const isObject = result !== null && (typeof result === 'object' || typeof result === 'function')
return isObject ? result : instance
}
使用它验证行为:
function Test(name, age) {
this.name = name
this.age = age
}
Test.prototype.sayName = function () {
console.log(this.name)
}
const a = myNew(Test, 'yck', 26)
console.log(a.name) // yck
console.log(a.age) // 26
a.sayName() // yck
console.log(a instanceof Test) // true
原文使用 Con.apply(obj, args) 也能表达“绑定 this 并调用”的意图;这里使用标准的 Reflect.apply,并明确区分了“返回函数也是非原始值”的情况。
这个模拟实现的边界
Reflect.apply不能调用 class 构造器,因为 class 只能通过new或Reflect.construct调用。原生new可以构造 class,而上述教学实现不行。- 箭头函数、普通方法和某些内置函数不可构造;它们没有可用的构造行为,原生
new会抛出TypeError。 - Proxy 可能通过
constructtrap 改变构造过程;跨 realm 的默认原型也不能只用Object.prototype完全模拟。 - 生产代码不要为了替代
new而修改Function.prototype。如果只是需要以函数形式调用构造能力,优先使用原生new或Reflect.construct。
生产代码中的 Reflect.construct
Reflect.construct(target, argumentsList, newTarget) 是函数形式的构造操作。前两个参数可以理解为 new target(...argumentsList),第三个参数允许指定 new.target,适合元编程或代理场景:
function Person(name) {
this.name = name
}
const person = Reflect.construct(Person, ['Ada'])
console.log(person.name) // Ada
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
new.target 与“无 new”调用
普通函数可以同时支持直接调用和构造调用,通过 new.target 区分当前调用方式:
function Person(name) {
if (!new.target) {
return { name, calledAsFunction: true }
}
this.name = name
this.calledAsConstructor = true
}
console.log(Person('Ada')) // { name: 'Ada', calledAsFunction: true }
console.log(new Person('Ada')) // Person { name: 'Ada', calledAsConstructor: true }
new.target 在直接调用时是 undefined,在构造调用中是构造目标(在绑定函数和部分 Reflect.construct 场景中还要结合规范理解)。class 不能直接调用:
class User {
constructor(name) {
this.name = name
}
}
try {
User('Ada')
} catch (error) {
console.log(error instanceof TypeError) // true
}
最后
new 不只是“调用函数并返回对象”,它同时建立实例与原型之间的关系、准备构造调用的 this,并处理构造函数的返回值。阅读旧代码时,还要注意它可能使用 __proto__、Function.prototype.apply 或手写的 new 模拟;现代代码应优先使用 Object.getPrototypeOf、Object.create、Reflect.apply 和 Reflect.construct 等标准 API。

参考资料
作者:yck
链接:https://juejin.cn/post/6844903789070123021
来源:稀土掘金。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。