原型与继承
发表于: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) // true2. 继承了构造函数继承的优点
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 不受影响?
- 每个实例都有自己的 colors 数组:通过
Parent.call(this, name)在每次创建实例时都会创建新的数组 - 实例属性优先级高于原型属性:访问
child1.colors时,首先找到实例自身的 colors - 原型上的 colors 没有被使用:虽然
Child.prototype上也有 colors,但实例属性将其覆盖
组合继承的本质
- 构造函数继承(
Parent.call):负责实例属性(每个实例独立) - 原型链继承(
Child.prototype = new Parent()):负责原型方法(所有实例共享)
这种组合方式完美地结合了两种继承的优点,是 ES6 Class 出现前最常用的继承模式!