Skip to content

原型与继承

作者:青见春山
发表于:2026-07-29
字数统计:6000 字
预计阅读21分钟

涵盖组合继承、寄生组合继承、内存结构图、三种继承方式对比表。

一、组合继承的执行过程分析

这是一个非常经典的组合继承(Combination Inheritance)示例。

1. 代码执行步骤详解

JavaScript
// 1. 定义 Parent 构造函数
function Parent(name) {
    this.name = name
    this.colors = ['red', 'blue', 'green']
}

// 2. 在 Parent 原型上添加方法
Parent.prototype.getName = function () {
    console.log(this.name)
}

// 3. 定义 Child 构造函数
function Child(name, age) {
    // 构造函数继承:每次创建 Child 实例时都会执行
    Parent.call(this, name)  // 在 child 实例上创建 name 和 colors
    this.age = age
}

// 4. 原型链继承:设置 Child 的原型为 Parent 的实例
Child.prototype = new Parent()  // 这里创建了一个 Parent 实例作为原型

// 5. 修正 constructor 指向
Child.prototype.constructor = Child

// 6. 创建实例
var child1 = new Child('kevin', '18')
var child2 = new Child('daisy', '20')

2. 内存结构图

JavaScript
/*
内存结构分析:

1. Child.prototype (由 new Parent() 创建)
   ├── name: undefined (没有传参)
   ├── colors: ['red', 'blue', 'green']  ← 这个数组在原型上,被所有实例共享
   ├── __proto__: Parent.prototype
   └── constructor: Child (被修正后)

2. child1 实例
   ├── name: 'kevin' (通过 Parent.call 添加)
   ├── colors: ['red', 'blue', 'green'] (通过 Parent.call 添加,新数组)
   ├── age: 18
   └── __proto__: Child.prototype

3. child2 实例
   ├── name: 'daisy' (通过 Parent.call 添加)
   ├── colors: ['red', 'blue', 'green'] (通过 Parent.call 添加,另一个新数组)
   ├── age: 20
   └── __proto__: Child.prototype

关键点:
- child1 和 child2 都有自己的 colors 数组(实例属性)
- Child.prototype 上也有一个 colors 数组(原型属性)
- 但实例属性会覆盖原型属性
*/

3. 属性查找机制

JavaScript
// 当访问 child1.colors 时:
// 1. 先查找 child1 自身属性 → 找到了!返回实例上的 colors
// 2. 不会继续查找原型链

// 当执行 child1.colors.push('black') 时:
// 修改的是 child1 实例自身的 colors 数组
// 不会影响 Child.prototype 上的 colors

// 当访问 child2.colors 时:
// 1. 查找 child2 自身属性 → 找到了!返回 child2 自己的 colors
// 2. 这个数组是独立创建的,所以不受 child1 的影响

4. 验证属性位置

JavaScript
// 验证实例属性
console.log(child1.hasOwnProperty('colors'))  // true (实例属性)
console.log(child2.hasOwnProperty('colors'))  // true (实例属性)

// 验证原型属性
console.log(Child.prototype.hasOwnProperty('colors'))  // true (原型属性)

// 但实例属性会覆盖原型属性
console.log(child1.colors === Child.prototype.colors)  // false (不同数组)
console.log(child2.colors === Child.prototype.colors)  // false (不同数组)

// 如果没有实例属性,才会访问原型属性
const child3 = new Child()  // 不传 name
console.log(child3.name)      // undefined (实例属性)
console.log(child3.colors)    // ['red', 'blue', 'green'] (实例属性)

二、组合继承的优势体现

1. 继承了原型链继承的优点

JavaScript
// 优点1:方法共享(节省内存)
console.log(child1.getName === child2.getName)         // true
console.log(child1.getName === Child.prototype.getName)  // true

// 优点2:可以访问原型链上的方法
child1.getName()  // 'kevin'
child2.getName()  // 'daisy'

// 优点3:instanceof 正确
console.log(child1 instanceof Parent)  // true
console.log(child1 instanceof Child)   // true

2. 继承了构造函数继承的优点

JavaScript
// 优点1:每个实例有独立的引用类型属性
child1.colors.push('black')
console.log(child1.colors)  // ['red', 'blue', 'green', 'black']
console.log(child2.colors)  // ['red', 'blue', 'green'] 不受影响

// 优点2:可以向父类传递参数
console.log(child1.name)  // 'kevin'
console.log(child2.name)  // 'daisy'

// 优点3:可以定义自己的实例属性
console.log(child1.age)   // 18
console.log(child2.age)   // 20

三、组合继承的完整验证

JavaScript
console.log('=== 组合继承验证 ===')

function Parent(name) {
    this.name = name
    this.colors = ['red', 'blue', 'green']
}

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 child1 = new Child('kevin', 18)
const child2 = new Child('daisy', 20)

// 1. 验证实例属性独立
child1.colors.push('black')
console.log('child1 colors:', child1.colors)  // ['red', 'blue', 'green', 'black']
console.log('child2 colors:', child2.colors)  // ['red', 'blue', 'green']

// 2. 验证方法共享
console.log('方法共享:', child1.getName === child2.getName)  // true
console.log('child1 name:', child1.getName())  // 'kevin'
console.log('child2 name:', child2.getName())  // 'daisy'

// 3. 验证原型链
console.log('instanceof Parent:', child1 instanceof Parent)  // true
console.log('instanceof Child:', child1 instanceof Child)    // true

// 4. 验证属性位置
console.log('child1 自身有 colors?', child1.hasOwnProperty('colors'))  // true
console.log('child2 自身有 colors?', child2.hasOwnProperty('colors'))  // true
console.log('Child.prototype 有 colors?', Child.prototype.hasOwnProperty('colors'))  // true

// 5. 验证访问优先级
console.log('child1.colors === Child.prototype.colors?',
            child1.colors === Child.prototype.colors)  // false

四、组合继承的潜在问题

虽然组合继承很好,但有一个小问题:

JavaScript
// 问题:会调用两次父类构造函数
function Parent(name) {
    console.log('Parent 构造函数被调用')
    this.name = name
    this.colors = ['red', 'blue', 'green']
}

function Child(name, age) {
    Parent.call(this, name)  // 第一次调用 Parent
    this.age = age
}

Child.prototype = new Parent()  // 第二次调用 Parent(没有传参)

// 结果:
// 1. Child.prototype 上有一个无用的 name 和 colors
// 2. 每次创建 Child 实例时,实例属性会覆盖原型属性
// 3. 多了一次不必要的函数调用

解决方案:使用 Object.create(寄生组合继承)

JavaScript
// 改进版:寄生组合继承(完美解决方案)
function Parent(name) {
    this.name = name
    this.colors = ['red', 'blue', 'green']
}

Parent.prototype.getName = function() {
    return this.name
}

function Child(name, age) {
    Parent.call(this, name)  // 只调用一次 Parent
    this.age = age
}

// 使用 Object.create 代替 new Parent()
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
Child.prototype.getAge = function() {
    return this.age
}

const child1 = new Child('kevin', 18)
const child2 = new Child('daisy', 20)

// 验证
child1.colors.push('black')
console.log(child1.colors)  // ['red', 'blue', 'green', 'black']
console.log(child2.colors)  // ['red', 'blue', 'green']
console.log(child1.getName())  // 'kevin'
console.log(child2.getName())  // 'daisy'

五、为什么 child2.colors 不受影响的深层原因

JavaScript
// 关键代码分析
function Child(name, age) {
    // 每次创建实例时,这里都会执行
    Parent.call(this, name)
    // 等价于:
    // this.name = name
    // this.colors = ['red', 'blue', 'green']  // 创建新数组

    this.age = age
}

// 创建 child1 时:
// 1. 创建一个新对象 {}
// 2. 执行 Parent.call(this, 'kevin')
//    → 在这个新对象上创建 name = 'kevin'
//    → 在这个新对象上创建 colors = ['red', 'blue', 'green'] (新数组)
// 3. 设置 age = 18

// 创建 child2 时:
// 重复上述过程,创建另一个新对象和新数组

// 所以 child1 和 child2 的 colors 是内存中两个完全独立的数组
// 修改 child1.colors 不会影响 child2.colors

六、完整对比总结

特性原型链继承构造函数继承组合继承
引用类型独立❌ 共享✅ 独立✅ 独立
向父类传参❌ 不支持✅ 支持✅ 支持
方法共享✅ 共享❌ 不共享✅ 共享
instanceof✅ 正确❌ 不正确✅ 正确
内存效率
调用父类次数1次1次/实例2次(可优化)

七、核心要点

为什么 child2.colors 不受影响?

  1. 每个实例都有自己的 colors 数组:通过 Parent.call(this, name) 在每次创建实例时都会创建新的数组
  2. 实例属性优先级高于原型属性:访问 child1.colors 时,首先找到实例自身的 colors
  3. 原型上的 colors 没有被使用:虽然 Child.prototype 上也有 colors,但实例属性将其覆盖

组合继承的本质

  • 构造函数继承Parent.call):负责实例属性(每个实例独立)
  • 原型链继承Child.prototype = new Parent()):负责原型方法(所有实例共享)

这种组合方式完美地结合了两种继承的优点,是 ES6 Class 出现前最常用的继承模式!

八、关联文档