Skip to content

原型链与继承

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   │
  └──────────────────┘                                 └────────────────────────┘

代码验证

js
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 的指向会丢失:

js
function Animal() {}
Animal.prototype = {
  eat() {}
}

const a = new Animal()
a.constructor === Animal
a.constructor === Object

这是因为新的字面量对象的 constructor 指向 Object。正确的做法是手动修复:

js
Animal.prototype = {
  constructor: Animal,
  eat() {}
}

或者使用 Object.defineProperty 使其不可枚举,与原生行为一致:

js
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)

当原型链上多个层级存在同名属性时,最近层级的属性会遮蔽上层的属性:

js
function Foo() {}
Foo.prototype.value = 1

const obj = new Foo()
obj.value
obj.value = 2
obj.value

delete obj.value
obj.value

性能考量

原型链越长,查找开销越大。对于性能敏感的场景,应使用 hasOwnPropertyObject.hasOwn(ES2022)检查属性是否为自身属性:

js
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 或被标记为不可写,行为会有所不同:

js
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 操作符不同,它不涉及构造函数的调用。

js
const personProto = {
  greet() {
    return `Hi, I'm ${this.name}`
  }
}

const person = Object.create(personProto)
person.name = 'Bob'
person.greet()
Object.getPrototypeOf(person) === personProto

Object.create(null) 创建一个没有任何原型的"纯净对象",常用于创建安全的字典对象:

js
const dict = Object.create(null)
dict.toString
dict.__proto__

dict['key'] = 'value'

手写实现

js
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__ 就指向了目标原型,而不会执行任何构造逻辑。

验证

js
const proto = { type: 'animal' }
const cat = objectCreate(proto, {
  name: {
    value: 'Tom',
    writable: true,
    enumerable: true,
    configurable: true
  }
})

Object.getPrototypeOf(cat) === proto
cat.name
cat.type

new 操作符的完整过程与手写实现

完整执行过程

当执行 new Foo(args) 时,JavaScript 引擎执行以下四个步骤:

  1. 创建新对象:创建一个空的普通对象 {}
  2. 链接原型:将新对象的 [[Prototype]] 设置为 Foo.prototype
  3. 绑定 this 并执行:以新对象为 this 上下文执行构造函数 Foo,传入参数
  4. 判断返回值:如果构造函数显式返回了一个对象类型(非原始值),则使用该返回值;否则返回新创建的对象

返回值规则的深入理解

js
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 操作符将其视为非对象返回值,仍然返回新创建的实例。

手写实现

js
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
}

验证

js
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

js
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.createdBy

instanceof 原理与手写实现

原理

a instanceof B 的判定规则是:沿着 a 的原型链(__proto__ 链)向上查找,检查是否有某个节点等于 B.prototype。如果找到则返回 true,到达 null 则返回 false

js
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 Object

Symbol.hasInstance

ES6 允许通过 Symbol.hasInstance 自定义 instanceof 的行为:

js
class EvenNumber {
  static [Symbol.hasInstance](instance) {
    return typeof instance === 'number' && instance % 2 === 0
  }
}

4 instanceof EvenNumber
3 instanceof EvenNumber

手写实现

js
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
}

验证

js
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 依赖原型链,因此存在以下局限:

  1. 跨 iframe / realm 失效:不同 iframe 中的 Array 构造函数不同,[] instanceof Array 可能为 false
  2. 原型被修改后失效:动态修改 prototype 会导致之前创建的实例判断失败
  3. 原始值无法判断42 instanceof Number 返回 false
js
function Foo() {}
const foo = new Foo()
foo instanceof Foo

Foo.prototype = {}
foo instanceof Foo

六种继承方式

1. 原型链继承

将子类的 prototype 指向父类的一个实例,利用原型链实现继承。

js
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 指向。

js
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.getNameundefined
  • 方法都在构造函数中定义,每次创建实例都会创建新的函数对象,无法复用

3. 组合继承(伪经典继承)

结合原型链继承和构造函数继承,取两者之长。这是 ES6 之前最常用的继承方式。

js
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

优点

  • 结合了两种继承的优点
  • 既能继承原型方法,又能传参,引用类型不共享
  • instanceofisPrototypeOf 都正常工作

缺点

  • 父类构造函数被调用两次:一次在 new Parent()(设置原型时),一次在 Parent.call(this)(创建实例时)。导致 Child.prototype 上存在多余的父类实例属性,虽然被实例自身属性遮蔽,但仍然浪费内存

4. 原型式继承

不使用构造函数,直接基于已有对象创建新对象。道格拉斯·克罗克福德在 2006 年提出,后被标准化为 Object.create

js
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. 寄生式继承

在原型式继承的基础上,通过一个工厂函数增强对象,添加额外的方法或属性。

js
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() 来建立原型链,避免执行父类构造函数中的实例化逻辑。

js
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 + 原型链的机制。

js
class Person {
  constructor(name) {
    this.name = name
  }
  sayHello() {
    return `Hello, I'm ${this.name}`
  }
}

上面的代码等价于:

js
function Person(name) {
  this.name = name
}
Person.prototype.sayHello = function () {
  return `Hello, I'm ${this.name}`
}

验证 class 的本质

js
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.prototype

class 与传统函数的差异

虽然 class 是语法糖,但它与直接使用 function 存在一些重要差异:

js
class A {}
function B() {}

A()
B()
  1. 不可直接调用class 声明的构造函数必须通过 new 调用,否则抛出 TypeError
  2. 不存在变量提升class 声明不会提升,存在暂时性死区(TDZ)
  3. 方法不可枚举class 中定义的方法默认 enumerablefalse
  4. 内部自动启用严格模式
  5. 子类构造函数中 thissuper() 之前不可访问
js
class Demo {
  method() {}
}

function DemoFunc() {}
DemoFunc.prototype.method = function () {}

Object.getOwnPropertyDescriptor(Demo.prototype, 'method').enumerable
Object.getOwnPropertyDescriptor(DemoFunc.prototype, 'method').enumerable

静态方法与静态属性

js
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 上,等价于:

js
function MathUtils() {}
MathUtils.PI = 3.14159
MathUtils.square = function (x) {
  return x * x
}

私有字段

ES2022 引入了真正的私有字段(以 # 开头),它不依赖原型链,而是通过 WeakMap 语义在引擎层面保证私有性:

js
class Counter {
  #count = 0

  increment() {
    this.#count++
  }

  get value() {
    return this.#count
  }
}

const c = new Counter()
c.increment()
c.value
c.#count

class 的 extends / super 底层实现

extends 做了什么

class Child extends Parent 在底层完成两条原型链的链接:

  1. 实例原型链Child.prototype.__proto__ === Parent.prototype(实例可以访问父类原型方法)
  2. 静态原型链Child.__proto__ === Parent(子类可以继承父类的静态方法)
js
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 Child

extends 的等价实现

js
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 指向子类。

js
class Parent {
  constructor() {
    this.createdBy = new.target.name
  }
}

class Child extends Parent {
  constructor() {
    super()
  }
}

new Parent().createdBy
new Child().createdBy

在普通方法中super.method() 通过方法内部的 [[HomeObject]] 属性确定当前方法所在的对象,然后沿该对象的原型链向上查找方法。

js
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__

这个区别在方法被借用时尤为重要:

js
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.prototypeB.prototype.say[[HomeObject]]B.prototype),而不是 C.prototype

继承内置类

ES6 classextends 还支持继承内置构造函数,这是 ES5 时代无法正确实现的:

js
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 的核心转译逻辑,帮助理解其完整的底层实现:

js
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 继承模拟

js
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 的正确传递,使得继承内置类(如 ArrayErrorMap)成为可能。

用心学习,用代码说话 💻