面试官问:能否模拟实现 JS 的 new 操作符
Category(分类): JavaScript Status: 未知
配图来自原文页面,已下载并转换为本地 WebP;原始授权未核实,发布前请人工确认。
前言
你好,我是若川。这是“面试官问”系列的第一篇,旨在帮助读者提升 JavaScript 基础知识,涉及 new、call、apply、this 和继承。
面试官问系列还包括:
- 面试官问:能否模拟实现 JS 的 new 操作符
- 面试官问:能否模拟实现 JS 的 bind 方法
- 面试官问:能否模拟实现 JS 的 call 和 apply 方法
- 面试官问:JS 的 this 指向
- 面试官问:JS 的继承
Vue 2 时代常见的入口是 new Vue(options):
// Vue 2 的历史写法
const vm = new Vue({
el: '#app',
mounted() {}
})
Vue 3 使用应用工厂 createApp,不再要求通过 new Vue 创建根应用:
// Vue 3 的现代写法
const app = createApp(App)
app.mount('#app')
面试官可能会继续追问:new 到底做了什么,能否用 JavaScript 模拟实现?
new 做了什么
例子 1:创建新对象
function Student() {}
const student = new Student()
console.log(typeof student) // object
console.log(Object.prototype.toString.call(student)) // [object Object]
console.log(Object.getPrototypeOf(student) === Student.prototype) // true
console.log(student.constructor === Student) // true
console.log(typeof Student) // function
console.log(typeof Object) // function
一个函数使用 new 调用后,会产生一个新对象。Student 和内置的 Object 都是函数,但 Student 是用户定义的构造器,Object 是内置构造器。
new Object() 和 Object() 在很多常见输入下都会产生对象,但两者仍是不同的调用形式;对于原始值参数,Object(value) 会返回对应的包装对象,不能把它简单理解成“总是创建空对象”。日常代码通常使用对象字面量 {},而不是 new Object()。

在 new Student() 创建的对象上,原型关系可以这样观察:
console.log(Object.getPrototypeOf(student) === Student.prototype) // true
console.log(Student.prototype.constructor === Student) // true
console.log(Object.getPrototypeOf(Student.prototype) === Object.prototype) // true
console.log(Object.getPrototypeOf(Object.prototype)) // null
原文使用 __proto__ 查看原型。它是为了 Web 兼容而保留的访问器属性,不建议作为新代码的主要 API;读取原型优先使用 Object.getPrototypeOf。
小结 1
从这个简单例子可以观察到:
new创建一个全新的对象。- 新对象的
[[Prototype]]通常会指向构造函数的prototype属性。
例子 2:构造函数中的 this
function Student(name) {
console.log('赋值前 this', this)
this.name = name
console.log('赋值后 this', this)
}
const student = new Student('若川')
console.log(student.name) // 若川
console.log(Object.getPrototypeOf(student) === Student.prototype) // true
构造函数中的 this 指向 new Student() 创建的新对象。构造函数执行前,原型连接和 this 绑定已经由构造调用准备好。
小结 2
- 构造函数执行时,其
this指向new创建的新对象。
例子 3:实例共享原型方法
function Student(name) {
this.name = name
}
Student.prototype.doSth = function () {
console.log(this.name)
}
const student1 = new Student('若')
const student2 = new Student('川')
student1.doSth() // 若
student2.doSth() // 川
console.log(student1.doSth === student2.doSth) // true
console.log(Object.getPrototypeOf(student1) === Student.prototype) // true
console.log(Object.getPrototypeOf(student2) === Student.prototype) // true
doSth 只存储在 Student.prototype 上,两个实例沿原型链找到同一个函数。调用 student1.doSth() 时,函数中的 this 仍然是 student1,而不是 Student.prototype。


小结 3
通过 new Student() 创建的每个对象,会把自己的 [[Prototype]] 连接到当时的 Student.prototype 对象。注意,如果之后重新赋值 Student.prototype,已经创建的实例仍然指向旧对象,新实例才会指向新对象。
构造函数的返回值
如果构造函数没有显式返回值,或返回原始值,new 会返回新创建的对象:
function ReturnPrimitive(name) {
this.name = name
return name
}
const primitiveResult = new ReturnPrimitive('若川')
console.log(primitiveResult.name) // 若川
console.log(primitiveResult instanceof ReturnPrimitive) // true
下面这些值都是原始值,返回时会被忽略:
function ReturnNull() {
this.kind = 'instance'
return null
}
function ReturnSymbol() {
this.kind = 'instance'
return Symbol('ignored')
}
console.log(new ReturnNull().kind) // instance
console.log(new ReturnSymbol().kind) // instance
如果构造函数返回非原始值,则该值成为 new 表达式的结果。对象不仅包括普通对象,也包括函数、数组、日期、正则表达式、错误对象和 null 原型对象:
function ReturnObject() {
this.kind = 'instance'
return { kind: 'returned' }
}
function ReturnFunction() {
return function returnedFunction() {}
}
function ReturnNullPrototypeObject() {
return Object.create(null)
}
const objectResult = new ReturnObject()
const functionResult = new ReturnFunction()
const nullPrototypeResult = new ReturnNullPrototypeObject()
console.log(objectResult.kind) // returned
console.log(typeof functionResult) // function
console.log(Object.getPrototypeOf(nullPrototypeResult)) // null
小结 4
- 构造函数没有返回值或返回原始值时,
new返回新对象。 - 构造函数返回对象或函数时,
new返回该非原始值。 - 判断返回值不能只写
result instanceof Object,因为Object.create(null)是对象但不继承Object.prototype。
模拟实现 new
把上面的行为整理起来,普通函数构造器的教学实现可以写成:
function newOperator(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
}
逐步说明:
- 检查第一个参数是否是函数。
- 创建对象,并在可能时把它的原型设置为
Constructor.prototype。 - 使用
Reflect.apply把对象作为this,执行构造函数。 - 返回构造函数产生的对象或函数,否则返回最初创建的实例。
function Student(name, age) {
this.name = name
this.age = age
}
Student.prototype.doSth = function () {
return `${this.name}:${this.age}`
}
const student1 = newOperator(Student, '若', 18)
const student2 = newOperator(Student, '川', 18)
console.log(student1.doSth()) // 若:18
console.log(student2.doSth()) // 川:18
console.log(student1 instanceof Student) // true
console.log(Object.getPrototypeOf(student1) === Student.prototype) // true
原文曾写 newOperator.target = ctor,这不是 new.target:new.target 是 JavaScript 运行时提供的元属性,只能在函数或 class 的调用上下文中读取,不能通过给普通函数增加 target 属性来模拟。上面的实现也不是原生 new 的完全替代品:
- class 不能用
Reflect.apply调用,只能通过new或Reflect.construct构造; - Proxy 可以通过
constructtrap 改变构造行为; - 跨 realm 对象的默认原型不能简单固定为当前 realm 的
Object.prototype; - 内置对象可能需要特殊内部槽,手写函数不能任意模拟
Map、Date等全部构造语义。
生产代码直接使用 new;元编程场景使用标准的 Reflect.construct:
function User(name) {
this.name = name
}
const user = Reflect.construct(User, ['Ada'])
console.log(user.name) // Ada
Object.create() 用法举例
Object.create(proto, propertiesObject) 创建一个新对象,并把 proto 作为新对象的 [[Prototype]]。第二个参数是可选的属性描述符对象:
const anotherObject = {
name: '若川'
}
const myObject = Object.create(anotherObject, {
age: {
value: 18,
enumerable: true,
writable: true,
configurable: true
}
})
console.log(Object.getPrototypeOf(anotherObject) === Object.prototype) // true
console.log(Object.getPrototypeOf(myObject) === anotherObject) // true
console.log(Object.hasOwn(myObject, 'name')) // false
console.log(Object.hasOwn(myObject, 'age')) // true
console.log(myObject.name) // 若川
console.log(myObject.age) // 18
原文中的 value:18 使用了中文冒号,会造成语法错误;属性描述符的键值应使用英文冒号。属性描述符中的 enumerable、writable、configurable 如果省略,默认值为 false,这也是 Object.create 示例中经常被忽略的细节。
历史上的 Object.create polyfill
现代浏览器和 Node.js 都已经支持 Object.create,不需要再添加 polyfill。下面保留一个 ES5 时代的简化思路,但它不支持 null 原型和第二个属性描述符参数,不能作为完整标准实现:
function createLegacy(proto) {
if (proto !== null && typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object or null')
}
function Empty() {}
Empty.prototype = proto
return new Empty()
}
const value = createLegacy({ kind: 'legacy' })
console.log(value.kind) // legacy
真正的兼容层还需要处理 null 原型、属性描述符、不可扩展对象和各类错误;不要覆盖现代运行时已有的标准实现。
现代补充:class、new.target 与 Reflect.construct
class 语法仍然建立在原型机制之上,但 class 不能像普通函数一样直接调用:
class Person {
constructor(name) {
this.name = name
}
}
const person = new Person('Ada')
console.log(person.name) // Ada
try {
Person('Ada')
} catch (error) {
console.log(error instanceof TypeError) // true
}
普通函数可以使用 new.target 区分直接调用和构造调用:
function Factory(name) {
if (!new.target) {
return { name, fromFactory: true }
}
this.name = name
this.fromConstructor = true
}
console.log(Factory('Ada')) // { name: 'Ada', fromFactory: true }
console.log(new Factory('Ada')) // Factory { name: 'Ada', fromConstructor: true }
最后总结
new创建新对象,并在适用时把新对象的[[Prototype]]指向构造函数的prototype。- 构造函数执行时,
this指向新对象。 - 构造函数返回原始值时,原始值会被忽略;返回对象或函数时,该非原始值会成为表达式结果。
Object.getPrototypeOf、Object.create、Reflect.apply和Reflect.construct是理解或实现相关机制时更清晰的标准 API。- 手写
new适合面试和学习,不要声称它可以替代所有内置构造器、class、Proxy 和跨 realm 语义。
参考资料
作者:若川
链接:https://juejin.cn/post/6844903704663949325
来源:稀土掘金。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。