原型链与继承
prototype、__proto__ 与 constructor 的关系
JavaScript 的原型系统由三个核心属性构成:prototype、__proto__(即 [[Prototype]])和 constructor。理解它们之间的三角关系是掌握原型链的基石。
三者的定义
prototype:只有函数才拥有的属性,指向一个对象,称为该函数的"原型对象"。当函数作为构造函数使用时,通过new创建的实例会继承该原型对象上的属性和方法。__proto__:每个对象(包括函数)都拥有的隐式属性,指向创建该对象时所用构造函数的prototype。它是Object.getPrototypeOf()的非标准别名,构成了原型链的实际链接。constructor:原型对象上的属性,默认指回拥有该prototype的函数本身。
关系图解
构造函数 Person
┌──────────────────┐
│ Person │
│ │─────── Person.prototype ──────▶ ┌─────────────────────────┐
│ [[Prototype]] │ │ Person.prototype │
└────────┬─────────┘ │ │
│ ┌────────────│ constructor ───▶ Person│
│ │ │ sayHello: fn() │
▼ │ │ [[Prototype]] │
Function.prototype │ └────────┬────────────────┘
│ │
│ ▼
实例 person │ Object.prototype
┌──────────────────┐ │ ┌────────────────────────┐
│ person │ │ │ constructor ──▶ Object│
│ │ │ │ toString() │
│ [[Prototype]] ──┼────────────────────┘ │ hasOwnProperty() │
│ name: 'Alice' │ │ [[Prototype]]: null │
└──────────────────┘ └────────────────────────┘代码验证
function Person(name) {
this.name = name
}
Person.prototype.sayHello = function () {
return `Hello, I'm ${this.name}`
}
const person = new Person('Alice')
person.__proto__ === Person.prototype
Person.prototype.constructor === Person
person.constructor === Person
Object.getPrototypeOf(person) === Person.prototype
Person.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null深度分析
constructor 属性并非不可变。当直接替换 prototype 对象时,constructor 的指向会丢失:
function Animal() {}
Animal.prototype = {
eat() {}
}
const a = new Animal()
a.constructor === Animal
a.constructor === Object这是因为新的字面量对象的 constructor 指向 Object。正确的做法是手动修复:
Animal.prototype = {
constructor: Animal,
eat() {}
}或者使用 Object.defineProperty 使其不可枚举,与原生行为一致:
Object.defineProperty(Animal.prototype, 'constructor', {
value: Animal,
enumerable: false,
writable: true,
configurable: true
})原型链查找机制
当访问一个对象的属性时,JavaScript 引擎会沿着原型链逐级向上查找,直到找到该属性或到达原型链的顶端 null。
查找流程
访问 person.toString()
person 自身属性 → 没有 toString
↓
person.__proto__ (Person.prototype) → 没有 toString
↓
Person.prototype.__proto__ (Object.prototype) → 找到 toString ✓
↓ (如果还没找到)
Object.prototype.__proto__ → null → 返回 undefined属性遮蔽(Property Shadowing)
当原型链上多个层级存在同名属性时,最近层级的属性会遮蔽上层的属性:
function Foo() {}
Foo.prototype.value = 1
const obj = new Foo()
obj.value
obj.value = 2
obj.value
delete obj.value
obj.value性能考量
原型链越长,查找开销越大。对于性能敏感的场景,应使用 hasOwnProperty 或 Object.hasOwn(ES2022)检查属性是否为自身属性:
const obj = Object.create({ inherited: true })
obj.own = true
obj.hasOwnProperty('own')
obj.hasOwnProperty('inherited')
Object.hasOwn(obj, 'own')
Object.hasOwn(obj, 'inherited')for...in 会遍历原型链上的可枚举属性,这也是推荐使用 Object.keys() 或 Object.hasOwn 进行过滤的原因。
属性设置的陷阱
给对象设置属性时,并不总是简单地在对象上创建新属性。如果原型链上已经存在同名属性且该属性是 setter 或被标记为不可写,行为会有所不同:
const proto = {}
Object.defineProperty(proto, 'readOnly', {
value: 42,
writable: false
})
const child = Object.create(proto)
child.readOnly = 100
child.readOnly
const withSetter = {}
let _val = 0
Object.defineProperty(withSetter, 'val', {
get() { return _val },
set(v) { _val = v * 2 }
})
const child2 = Object.create(withSetter)
child2.val = 5
child2.val
child2.hasOwnProperty('val')Object.create 原理与手写实现
原理
Object.create(proto, propertiesObject) 创建一个新对象,以 proto 作为新对象的 [[Prototype]],可选地通过 propertiesObject 定义新对象的属性。
它是实现纯粹原型继承的核心方法,与 new 操作符不同,它不涉及构造函数的调用。
const personProto = {
greet() {
return `Hi, I'm ${this.name}`
}
}
const person = Object.create(personProto)
person.name = 'Bob'
person.greet()
Object.getPrototypeOf(person) === personProtoObject.create(null) 创建一个没有任何原型的"纯净对象",常用于创建安全的字典对象:
const dict = Object.create(null)
dict.toString
dict.__proto__
dict['key'] = 'value'手写实现
function objectCreate(proto, propertiesObject) {
if (typeof proto !== 'object' && typeof proto !== 'function' && proto !== null) {
throw new TypeError('Object prototype may only be an Object or null')
}
function F() {}
F.prototype = proto
const obj = new F()
if (proto === null) {
Object.setPrototypeOf(obj, null)
}
if (propertiesObject !== undefined) {
Object.defineProperties(obj, propertiesObject)
}
return obj
}核心思路是利用一个空的中间构造函数 F,将其 prototype 指向目标原型,然后通过 new F() 创建实例。这样新对象的 __proto__ 就指向了目标原型,而不会执行任何构造逻辑。
验证
const proto = { type: 'animal' }
const cat = objectCreate(proto, {
name: {
value: 'Tom',
writable: true,
enumerable: true,
configurable: true
}
})
Object.getPrototypeOf(cat) === proto
cat.name
cat.typenew 操作符的完整过程与手写实现
完整执行过程
当执行 new Foo(args) 时,JavaScript 引擎执行以下四个步骤:
- 创建新对象:创建一个空的普通对象
{} - 链接原型:将新对象的
[[Prototype]]设置为Foo.prototype - 绑定 this 并执行:以新对象为
this上下文执行构造函数Foo,传入参数 - 判断返回值:如果构造函数显式返回了一个对象类型(非原始值),则使用该返回值;否则返回新创建的对象
返回值规则的深入理解
function ReturnObject() {
this.a = 1
return { b: 2 }
}
const obj1 = new ReturnObject()
obj1.a
obj1.b
function ReturnPrimitive() {
this.a = 1
return 42
}
const obj2 = new ReturnPrimitive()
obj2.a
obj2 instanceof ReturnPrimitive
function ReturnNull() {
this.a = 1
return null
}
const obj3 = new ReturnNull()
obj3.a注意 null 虽然 typeof null === 'object',但 new 操作符将其视为非对象返回值,仍然返回新创建的实例。
手写实现
function myNew(Constructor, ...args) {
if (typeof Constructor !== 'function') {
throw new TypeError(`${Constructor} is not a constructor`)
}
const obj = Object.create(Constructor.prototype)
const result = Constructor.apply(obj, args)
return result !== null && (typeof result === 'object' || typeof result === 'function')
? result
: obj
}验证
function Car(brand, price) {
this.brand = brand
this.price = price
}
Car.prototype.info = function () {
return `${this.brand}: ¥${this.price}`
}
const car = myNew(Car, 'Tesla', 250000)
car.brand
car.info()
car instanceof Car与 Reflect.construct 的比较
ES6 提供了 Reflect.construct,它等价于 new 操作符但可以指定不同的 new.target:
function Base() {
this.createdBy = new.target.name
}
function Derived() {}
Derived.prototype = Object.create(Base.prototype)
const obj = Reflect.construct(Base, [], Derived)
obj instanceof Derived
obj.createdByinstanceof 原理与手写实现
原理
a instanceof B 的判定规则是:沿着 a 的原型链(__proto__ 链)向上查找,检查是否有某个节点等于 B.prototype。如果找到则返回 true,到达 null 则返回 false。
function Animal() {}
function Dog() {}
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog
const dog = new Dog()
dog instanceof Dog
dog instanceof Animal
dog instanceof ObjectSymbol.hasInstance
ES6 允许通过 Symbol.hasInstance 自定义 instanceof 的行为:
class EvenNumber {
static [Symbol.hasInstance](instance) {
return typeof instance === 'number' && instance % 2 === 0
}
}
4 instanceof EvenNumber
3 instanceof EvenNumber手写实现
function myInstanceof(obj, Constructor) {
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
return false
}
if (typeof Constructor !== 'function') {
throw new TypeError('Right-hand side of instanceof is not callable')
}
if (typeof Constructor[Symbol.hasInstance] === 'function') {
return Constructor[Symbol.hasInstance](obj)
}
let proto = Object.getPrototypeOf(obj)
while (proto !== null) {
if (proto === Constructor.prototype) {
return true
}
proto = Object.getPrototypeOf(proto)
}
return false
}验证
function A() {}
function B() {}
B.prototype = Object.create(A.prototype)
const b = new B()
myInstanceof(b, B)
myInstanceof(b, A)
myInstanceof(b, Object)
myInstanceof(42, Number)
myInstanceof('str', String)instanceof 的局限性
instanceof 依赖原型链,因此存在以下局限:
- 跨 iframe / realm 失效:不同 iframe 中的
Array构造函数不同,[] instanceof Array可能为false - 原型被修改后失效:动态修改
prototype会导致之前创建的实例判断失败 - 原始值无法判断:
42 instanceof Number返回false
function Foo() {}
const foo = new Foo()
foo instanceof Foo
Foo.prototype = {}
foo instanceof Foo六种继承方式
1. 原型链继承
将子类的 prototype 指向父类的一个实例,利用原型链实现继承。
function Parent() {
this.colors = ['red', 'blue']
}
Parent.prototype.getColors = function () {
return this.colors
}
function Child() {}
Child.prototype = new Parent()
Child.prototype.constructor = Child
const c1 = new Child()
const c2 = new Child()
c1.colors.push('green')
c2.colors优点:
- 实现简单
- 父类原型上的方法可以复用
缺点:
- 引用类型属性被所有实例共享:所有子类实例共享同一个父类实例,修改引用类型属性(如数组)会互相影响
- 创建子类实例时无法向父类构造函数传参
- 无法实现多继承
2. 构造函数继承(借用构造函数/经典继承)
在子类构造函数中调用父类构造函数,通过 call / apply 改变 this 指向。
function Parent(name) {
this.name = name
this.colors = ['red', 'blue']
}
Parent.prototype.getName = function () {
return this.name
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
const c1 = new Child('Alice', 20)
const c2 = new Child('Bob', 22)
c1.colors.push('green')
c1.colors
c2.colors
c1.getName优点:
- 避免了引用类型属性的共享问题,每个实例拥有独立的副本
- 可以向父类构造函数传参
- 可以通过多次
call实现多继承
缺点:
- 无法继承父类原型上的方法,
c1.getName为undefined - 方法都在构造函数中定义,每次创建实例都会创建新的函数对象,无法复用
3. 组合继承(伪经典继承)
结合原型链继承和构造函数继承,取两者之长。这是 ES6 之前最常用的继承方式。
function Parent(name) {
this.name = name
this.colors = ['red', 'blue']
}
Parent.prototype.getName = function () {
return this.name
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
Child.prototype.getAge = function () {
return this.age
}
const c1 = new Child('Alice', 20)
const c2 = new Child('Bob', 22)
c1.colors.push('green')
c1.colors
c2.colors
c1.getName()
c1 instanceof Parent优点:
- 结合了两种继承的优点
- 既能继承原型方法,又能传参,引用类型不共享
instanceof和isPrototypeOf都正常工作
缺点:
- 父类构造函数被调用两次:一次在
new Parent()(设置原型时),一次在Parent.call(this)(创建实例时)。导致Child.prototype上存在多余的父类实例属性,虽然被实例自身属性遮蔽,但仍然浪费内存
4. 原型式继承
不使用构造函数,直接基于已有对象创建新对象。道格拉斯·克罗克福德在 2006 年提出,后被标准化为 Object.create。
function object(proto) {
function F() {}
F.prototype = proto
return new F()
}
const person = {
name: 'Default',
hobbies: ['reading']
}
const p1 = object(person)
const p2 = object(person)
p1.name = 'Alice'
p1.hobbies.push('coding')
p2.name
p2.hobbies优点:
- 不需要定义构造函数,适合从现有对象派生新对象
- 实现简洁,ES5 已标准化为
Object.create
缺点:
- 引用类型属性同样被共享,与原型链继承有相同的问题
- 无法传递初始化参数
5. 寄生式继承
在原型式继承的基础上,通过一个工厂函数增强对象,添加额外的方法或属性。
function createPerson(original) {
const clone = Object.create(original)
clone.sayHi = function () {
return `Hi, I'm ${this.name}`
}
return clone
}
const person = {
name: 'Default',
hobbies: ['reading']
}
const p1 = createPerson(person)
p1.name = 'Alice'
p1.sayHi()优点:
- 灵活,可以在工厂函数中自由增强对象
缺点:
- 方法无法复用,每次调用工厂函数都会创建新的函数对象,效率低
- 引用类型属性共享问题依旧存在
6. 寄生组合式继承 ✅
这是公认的最理想的继承方式,也是 ES6 class extends 在底层采用的方案。它解决了组合继承中父类构造函数被调用两次的问题。
核心思想:用 Object.create 代替 new Parent() 来建立原型链,避免执行父类构造函数中的实例化逻辑。
function inheritPrototype(Child, Parent) {
const prototype = Object.create(Parent.prototype)
prototype.constructor = Child
Child.prototype = prototype
}
function Parent(name) {
this.name = name
this.colors = ['red', 'blue']
}
Parent.prototype.getName = function () {
return this.name
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
inheritPrototype(Child, Parent)
Child.prototype.getAge = function () {
return this.age
}
const c1 = new Child('Alice', 20)
const c2 = new Child('Bob', 22)
c1.colors.push('green')
c1.colors
c2.colors
c1.getName()
c1 instanceof Parent
c1 instanceof Child优点:
- 父类构造函数只调用一次
- 原型链保持正确,
instanceof正常工作 - 不会在
Child.prototype上留下多余的父类实例属性 - 引用类型不共享,可以传参
缺点:
- 实现相对复杂(但 ES6 的
class已经封装了这一模式)
六种继承方式对比
| 方式 | 父类构造函数调用 | 原型方法复用 | 引用类型独立 | 可传参 | 缺陷 |
|---|---|---|---|---|---|
| 原型链继承 | 1 次 | ✅ | ❌ | ❌ | 引用共享 |
| 构造函数继承 | 每次实例化 | ❌ | ✅ | ✅ | 无法继承原型方法 |
| 组合继承 | 2 次 | ✅ | ✅ | ✅ | 父构造调用两次 |
| 原型式继承 | 0 次 | ✅ | ❌ | ❌ | 引用共享 |
| 寄生式继承 | 0 次 | ❌ | ❌ | ❌ | 方法不复用 |
| 寄生组合式继承 | 1 次 | ✅ | ✅ | ✅ | 无明显缺陷 |
ES6 class 的本质
class 是语法糖
ES6 的 class 本质上是基于原型的继承的语法糖。它并没有引入新的对象继承模型,底层仍然是 prototype + 原型链的机制。
class Person {
constructor(name) {
this.name = name
}
sayHello() {
return `Hello, I'm ${this.name}`
}
}上面的代码等价于:
function Person(name) {
this.name = name
}
Person.prototype.sayHello = function () {
return `Hello, I'm ${this.name}`
}验证 class 的本质
class Foo {
constructor() {
this.x = 1
}
bar() {}
static baz() {}
}
typeof Foo
Foo.prototype.constructor === Foo
Foo.prototype.hasOwnProperty('bar')
Foo.hasOwnProperty('baz')
const f = new Foo()
Object.getPrototypeOf(f) === Foo.prototypeclass 与传统函数的差异
虽然 class 是语法糖,但它与直接使用 function 存在一些重要差异:
class A {}
function B() {}
A()
B()- 不可直接调用:
class声明的构造函数必须通过new调用,否则抛出TypeError - 不存在变量提升:
class声明不会提升,存在暂时性死区(TDZ) - 方法不可枚举:
class中定义的方法默认enumerable为false - 内部自动启用严格模式
- 子类构造函数中
this在super()之前不可访问
class Demo {
method() {}
}
function DemoFunc() {}
DemoFunc.prototype.method = function () {}
Object.getOwnPropertyDescriptor(Demo.prototype, 'method').enumerable
Object.getOwnPropertyDescriptor(DemoFunc.prototype, 'method').enumerable静态方法与静态属性
class MathUtils {
static PI = 3.14159
static square(x) {
return x * x
}
}
MathUtils.square(3)
MathUtils.PI
const m = new MathUtils()
m.square
m.PI静态方法绑定在构造函数本身而非 prototype 上,等价于:
function MathUtils() {}
MathUtils.PI = 3.14159
MathUtils.square = function (x) {
return x * x
}私有字段
ES2022 引入了真正的私有字段(以 # 开头),它不依赖原型链,而是通过 WeakMap 语义在引擎层面保证私有性:
class Counter {
#count = 0
increment() {
this.#count++
}
get value() {
return this.#count
}
}
const c = new Counter()
c.increment()
c.value
c.#countclass 的 extends / super 底层实现
extends 做了什么
class Child extends Parent 在底层完成两条原型链的链接:
- 实例原型链:
Child.prototype.__proto__ === Parent.prototype(实例可以访问父类原型方法) - 静态原型链:
Child.__proto__ === Parent(子类可以继承父类的静态方法)
class Parent {
static create() {
return new this()
}
instanceMethod() {
return 'parent instance method'
}
}
class Child extends Parent {
childMethod() {
return 'child method'
}
}
Child.prototype.__proto__ === Parent.prototype
Child.__proto__ === Parent
Child.create
Child.create() instanceof Childextends 的等价实现
function Parent(name) {
this.name = name
}
Parent.prototype.getName = function () {
return this.name
}
Parent.create = function () {
return new this()
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
Object.setPrototypeOf(Child, Parent)
Child.prototype.getAge = function () {
return this.age
}关键在于 Object.setPrototypeOf(Child, Parent) 这一步——这是传统寄生组合式继承没有的,它建立了构造函数之间的原型链,使得静态方法也能被继承。
super 的工作原理
super 关键字在两种上下文中有不同的含义:
在 constructor 中:super(args) 等价于 Parent.call(this, args),但更准确地说,它调用的是 Reflect.construct(Parent, args, Child),即使用父类构造函数创建对象,但 new.target 指向子类。
class Parent {
constructor() {
this.createdBy = new.target.name
}
}
class Child extends Parent {
constructor() {
super()
}
}
new Parent().createdBy
new Child().createdBy在普通方法中:super.method() 通过方法内部的 [[HomeObject]] 属性确定当前方法所在的对象,然后沿该对象的原型链向上查找方法。
class A {
greet() {
return 'A'
}
}
class B extends A {
greet() {
return `B -> ${super.greet()}`
}
}
class C extends B {
greet() {
return `C -> ${super.greet()}`
}
}
new C().greet()super 的底层机制:[[HomeObject]]
super 不是简单的 this.__proto__。每个方法在定义时都会绑定一个 [[HomeObject]],指向方法所属的对象。super 的查找基于 [[HomeObject]].__proto__,而非 this.__proto__。
这个区别在方法被借用时尤为重要:
class A {
say() {
return 'A'
}
}
class B extends A {
say() {
return `B -> ${super.say()}`
}
}
class C {
say() {
return 'C'
}
}
class D extends C {
say = B.prototype.say
}
const d = new D()
d.say()D 借用了 B.prototype.say,但 super.say() 仍然查找 A.prototype(B.prototype.say 的 [[HomeObject]] 是 B.prototype),而不是 C.prototype。
继承内置类
ES6 class 的 extends 还支持继承内置构造函数,这是 ES5 时代无法正确实现的:
class MyArray extends Array {
first() {
return this[0]
}
last() {
return this[this.length - 1]
}
}
const arr = new MyArray(1, 2, 3)
arr.first()
arr.last()
arr instanceof MyArray
arr instanceof Array
arr.map(x => x * 2) instanceof MyArray在 ES5 中,继承 Array 后,length 属性不会自动更新,arr.map 返回的也不是自定义子类的实例。ES6 通过 new.target 和内部的 Symbol.species 机制解决了这些问题。
extends 的完整 Babel 转译
以下是 Babel 对 class extends 的核心转译逻辑,帮助理解其完整的底层实现:
function _inherits(subClass, superClass) {
if (typeof superClass !== 'function' && superClass !== null) {
throw new TypeError('Super expression must either be null or a function')
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
})
Object.defineProperty(subClass, 'prototype', { writable: false })
if (superClass) Object.setPrototypeOf(subClass, superClass)
}
function _createSuper(Derived) {
return function _createSuperInternal() {
var Super = Object.getPrototypeOf(Derived)
var result = Reflect.construct(Super, arguments, Derived)
return result
}
}_inherits 完成原型链的链接,_createSuper 利用 Reflect.construct 确保 new.target 正确传递,这是支持继承内置类的关键。
完整的手写 class 继承模拟
function Parent(name) {
this.name = name
}
Parent.prototype.sayName = function () {
return this.name
}
function Child(name, age) {
var instance = Reflect.construct(Parent, [name], Child)
instance.age = age
return instance
}
Object.setPrototypeOf(Child.prototype, Parent.prototype)
Object.setPrototypeOf(Child, Parent)
Child.prototype.constructor = Child
Child.prototype.sayAge = function () {
return this.age
}
var c = Reflect.construct(Child, ['Alice', 25])
c.sayName()
c.sayAge()
c instanceof Child
c instanceof Parent使用 Reflect.construct 而非 Parent.call 是 ES6 class 继承与 ES5 模拟继承的根本区别。它确保了 new.target 的正确传递,使得继承内置类(如 Array、Error、Map)成为可能。