重新认识构造函数、原型和原型链
Category(分类): JavaScript Status: 未知
配图来自原文页面,已下载并转换为本地 WebP;原始授权未核实,发布前请人工确认。
引言
前端进阶系列已经到第 5 期。本篇重点介绍构造函数、原型和原型链,并回答几个常见问题:Symbol 是否可以作为构造函数、constructor 如何产生以及是否可修改、prototype、[[Prototype]] 和 __proto__ 有什么区别,以及属性如何沿着原型链查找。
原文中的思维导图和配图保留为本地 WebP;文字和代码按现代 ECMAScript 语义重新整理。

构造函数
什么是构造函数
constructor 是一个属性名,通常引用创建对象的构造函数;它的值是函数本身,而不是函数名称字符串。但它不是对象身份的可靠标签,因为它通常来自原型链,而且可以被覆盖。
构造函数本身就是函数。函数是否在某次调用中作为构造函数使用,取决于调用方式:使用 new 或 Reflect.construct 是构造调用,直接调用则是普通调用。首字母大写只是社区约定,不是语言强制规则。
function Parent(age) {
this.age = age
}
const parent = new Parent(50)
console.log(parent.constructor === Parent) // true
console.log(parent.constructor === Object) // false
console.log(Object.hasOwn(parent, 'constructor')) // false
这里的 constructor 通常来自 Parent.prototype,而不是实例自己的属性:
console.log(Object.hasOwn(Parent.prototype, 'constructor')) // true
console.log(Parent.prototype.constructor === Parent) // true
普通函数直接调用不一定会创建实例:
function parent2(age) {
this.age = age
}
function parent3(age) {
return { age }
}
// 在 classic script 的非严格模式中,parent2() 的 this 可能是 globalThis;
// 在 ES module 或严格模式中,直接调用会因 this 为 undefined 而抛错。
const p2 = parent2(50)
console.log(p2) // undefined(非严格 classic script)
const p3 = parent3(50)
console.log(p3.constructor === Object) // true
新代码应避免依赖普通函数直接调用时的隐式 this。如果函数既支持工厂调用又支持构造调用,建议显式检查 new.target,或直接拆成工厂函数和 class。
Symbol 是构造函数吗
Symbol 是一个函数,但不能使用 new Symbol() 构造:
try {
new Symbol(123)
} catch (error) {
console.log(error instanceof TypeError) // true
}
const sym = Symbol(123)
console.log(typeof sym) // symbol
console.log(sym.constructor === Symbol) // true
console.log(Symbol.prototype.constructor === Symbol) // true
Symbol(123) 返回的是原始值。访问 sym.constructor 时,语言会临时对原始值进行包装,并沿着 Symbol.prototype 找到 constructor;这不意味着 sym 是一个普通对象实例,也不意味着 constructor 可以作为可靠的类型判断依据。
类似地,BigInt() 可以调用但不能使用 new BigInt();Map、Set、Proxy 等则需要构造调用。一个函数能否构造,应以实际语言语义和 Reflect.construct 行为为准,而不是只看 typeof value === 'function'。
constructor 值可以修改吗
对于普通对象,constructor 通常是原型上的可写属性,可以被覆盖;对于原始值,属性访问会临时装箱,不能把属性持久写回原始值。null 和 undefined 不能进行属性访问。
function Foo() {}
Foo.prototype = {
method() {}
}
console.log(Foo.prototype.constructor === Object) // true
// 重新赋值 prototype 后,默认的 constructor 属性来自 Object.prototype。
// 如果希望保留约定,需要显式修正,并按需设置属性描述符。
Foo.prototype.constructor = Foo
console.log(Foo.prototype.constructor === Foo) // true
把 prototype 直接替换成对象时,最好明确恢复 constructor:
function Bar() {}
Bar.prototype = Object.assign(Object.create(Object.prototype), {
constructor: {
value: Bar,
writable: true,
configurable: true,
enumerable: false
},
method() {
return 'bar'
}
})
console.log(new Bar().constructor === Bar) // true
下面的例子说明原始值的 constructor 不是一个可以写入的实例属性:
function Type() {}
const values = [1, 'muyiy', true, Symbol(123)]
for (const value of values) {
console.log(value.constructor.name, value instanceof Type)
}
// Number false
// String false
// Boolean false
// Symbol false
在严格模式下,对原始值属性执行赋值可能抛出 TypeError;在非严格模式下通常会被忽略。不要用这种行为修改所谓的“基本类型 constructor”。

模拟实现 new
对于普通函数构造器,教学版实现可以写成:
function create(Constructor, ...args) {
if (typeof Constructor !== 'function') {
throw new TypeError('Constructor must be a function')
}
const prototype = Constructor.prototype
const object = Object.create(
prototype !== null && (typeof prototype === 'object' || typeof prototype === 'function')
? prototype
: Object.prototype
)
const result = Reflect.apply(Constructor, object, args)
const isObject = result !== null && (typeof result === 'object' || typeof result === 'function')
return isObject ? result : object
}
这个实现表达了创建对象、连接原型、绑定 this 和处理返回值四个步骤,但它不是原生 new 的完全替代品:class 不能被 Reflect.apply 调用,Proxy 的构造捕获、跨 realm 默认原型和 new.target 也需要使用 Reflect.construct 等机制处理。
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function () {
return this.name
}
const person = create(Person, 'Ada', 26)
console.log(person.name) // Ada
console.log(person.age) // 26
console.log(person.sayName()) // Ada
console.log(person instanceof Person) // true
如果目标是执行真正的构造调用,生产代码应直接使用 new,或在元编程场景使用标准 API:
function User(name) {
this.name = name
}
const user = Reflect.construct(User, ['Ada'])
console.log(user.name) // Ada
原型 prototype
JavaScript 是基于原型的语言。函数对象可以有一个名为 prototype 的自有属性,它通常是一个对象;当这个函数作为构造函数被 new 调用时,新实例的 [[Prototype]] 会指向该对象。
需要区分三件事:
Parent.prototype:构造函数Parent的一个普通对象属性,供构造实例使用。Object.getPrototypeOf(parent):读取实例parent的内部[[Prototype]],通常等于Parent.prototype。Object.getPrototypeOf(Parent):读取构造函数本身的[[Prototype]],通常等于Function.prototype,与Parent.prototype不是一回事。
function Parent() {}
const parent = new Parent()
console.log(Object.getPrototypeOf(parent) === Parent.prototype) // true
console.log(Object.getPrototypeOf(Parent) === Function.prototype) // true
console.log(Parent.prototype.constructor === Parent) // true
并不是所有函数都有可用的 prototype 属性。箭头函数、对象方法和绑定函数通常不可构造或没有自己的默认 prototype:
const arrow = () => {}
const object = {
method() {}
}
const bound = Parent.bind(null)
console.log(arrow.prototype) // undefined
console.log(object.method.prototype) // undefined
console.log(bound.prototype) // undefined

[[Prototype]] 与 __proto__
[[Prototype]] 是对象的内部插槽,值可以是对象或 null。它不是一个普通的字符串属性,外部代码应使用以下标准 API 读取或设置:
const parent = { kind: 'parent' }
const child = Object.create(parent)
console.log(Object.getPrototypeOf(child) === parent) // true
console.log(Reflect.getPrototypeOf(child) === parent) // true
const another = { kind: 'another' }
Object.setPrototypeOf(child, another)
console.log(child.kind) // another
console.log(Reflect.setPrototypeOf(child, parent)) // true
Object.prototype.__proto__ 是为了 Web 兼容而保留的访问器属性,不是每个实例自有的普通属性;不建议在新代码中使用 obj.__proto__ = value。对象字面量中的 { __proto__: value } 是另一种标准语法,作用是在创建对象时设置其原型,不能和访问器混为一谈:
function Parent() {}
const parent = new Parent()
console.log(parent.__proto__ === Parent.prototype) // true(访问器读取)
console.log(Object.hasOwn(parent, '__proto__')) // false
const child = { __proto__: parent, name: 'child' }
console.log(Object.getPrototypeOf(child) === parent) // true
动态修改已经存在对象的原型可能使 JavaScript 引擎放弃原有优化;如果原型关系是已知的,优先在对象创建时使用 class extends、构造函数、对象字面量或 Object.create 设置。

Object.create()
Object.create(proto) 创建一个新对象,并把它的 [[Prototype]] 设置为 proto。传入 null 可以创建没有 Object.prototype 的字典对象:
const parent = { age: 50 }
const child = Object.create(parent)
console.log(child.age) // 50,继承属性
console.log(Object.hasOwn(child, 'age')) // false
console.log(Object.getPrototypeOf(child) === parent) // true
const dictionary = Object.create(null)
console.log(Object.getPrototypeOf(dictionary)) // null
console.log(dictionary.hasOwnProperty) // undefined
原文中 function Parent() { age: 50 } 里的 age: 50 只是一个标签语句,并不会给对象添加 age 属性;如果希望构造函数实例拥有属性,应写成 this.age = 50。
优化实现 new
使用 Object.create 比手动给 __proto__ 赋值更清晰:
function createWithObjectCreate(Constructor, ...args) {
const object = Object.create(Constructor.prototype)
const result = Reflect.apply(Constructor, object, args)
const isObject = result !== null && (typeof result === 'object' || typeof result === 'function')
return isObject ? result : object
}
这是面试题中的教学实现,不建议在业务代码中用它替换原生 new。原生操作还会处理可构造性、new.target、Proxy 和内置对象的特殊内部槽。

原型链
每个普通对象都可以通过 [[Prototype]] 指向另一个对象,另一个对象也可以有自己的原型,最终通常到达 Object.prototype,再到 null。访问属性时,先查找对象自身;找不到再沿原型链向上查找,直到找到属性或遇到 null。
function Parent(age) {
this.age = age
}
const parent = new Parent(50)
console.log(Object.hasOwn(parent, 'constructor')) // false
console.log(parent.constructor === Parent) // true
console.log(Object.getPrototypeOf(parent) === Parent.prototype) // true
console.log(Object.getPrototypeOf(Parent.prototype) === Object.prototype) // true
console.log(Object.getPrototypeOf(Object.prototype)) // null
实例 parent 本身没有 constructor,但可以从 Parent.prototype 继承到它。in 会检查整条原型链,Object.hasOwn 只检查对象自身:
console.log('constructor' in parent) // true
console.log(Object.hasOwn(parent, 'constructor')) // false
console.log(Object.hasOwn(Parent.prototype, 'constructor')) // true

现代继承写法
class 语法仍然建立在原型机制之上,但能更清晰地表达构造、继承、静态成员和私有字段:
class ParentClass {
constructor(age) {
this.age = age
}
sayAge() {
return this.age
}
}
class ChildClass extends ParentClass {
#kind = 'child'
constructor(age, name) {
super(age)
this.name = name
}
describe() {
return `${this.name}:${this.sayAge()}:${this.#kind}`
}
}
const child = new ChildClass(18, 'Ada')
console.log(child.describe()) // Ada:18:child
console.log(child instanceof ChildClass) // true
console.log(child instanceof ParentClass) // true
extends 会建立 ChildClass.prototype 到 ParentClass.prototype 的原型链,并同时建立构造函数本身的继承关系。class 方法默认不可枚举,class 仍然不能脱离 new 直接调用。
也可以使用 Object.setPrototypeOf 构建继承关系,但应尽量在定义阶段完成,不要在高频路径中反复修改对象原型:
function Base() {}
function Derived() {}
Object.setPrototypeOf(Derived.prototype, Base.prototype)
const value = new Derived()
console.log(value instanceof Derived) // true
console.log(value instanceof Base) // true
小结
Symbol可以调用但不能使用new,Symbol.prototype.constructor仍然引用Symbol。constructor通常是原型上的可写属性,不是可靠的类型标签;替换prototype后要留意它是否仍指向原构造函数。prototype是构造函数的属性,实例的[[Prototype]]是对象内部插槽,二者相关但不相同。__proto__访问器不是每个实例的自有属性;读取或设置原型优先使用Object.getPrototypeOf、Object.setPrototypeOf或Reflect对应 API。- 原型链以对象为节点,属性查找会从实例逐级向上直到
null。 - 现代代码优先使用 class、
Object.create和明确的组合方式;只有在确实需要元编程时才动态修改原型。


参考资料
作者:木易杨
来源:原文整理自稀土掘金文章,著作权归原作者所有。