JavaScript 原型链继承
在JavaScript中,继承是通过原型链实现的,这与其他面向对象编程语言中的类继承有很大不同。理解原型链是掌握JavaScript面向对象编程的关键。本文将帮助你全面了解JavaScript原型链继承的工作原理和实际应用。
什么是原型链?
在JavaScript中,每个对象都有一个内部链接指向另一个对象,称为它的原型(prototype)。这个原型对象也有自己的原型,以此类推,形成了所谓的"原型链",直到达到一个原型为null
的对象。
当我们尝试访问一个对象的属性时,JavaScript引擎会:
- 先检查对象本身是否有该属性
- 如果没有,则检查对象的原型
- 如果原型也没有,则继续检查原型的原型
- 这个过程会一直持续到找到该属性或到达原型链的末端(
null
)
原型相关的重要概念
在深入了解原型链继承之前,需要理解几个关键概念:
1. __proto__
vs prototype
__proto__
:每个对象都有的内部属性,指向该对象的原型prototype
:函数对象的特有属性,当函数作为构造函数使用时,新创建的对象的__proto__
会指向这个prototype
function Person(name) {
this.name = name;
}
const person1 = new Person('Alice');
console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
2. constructor
属性
每个原型对象都有一个constructor
属性,指回与之关联的构造函数。
function Person(name) {
this.name = name;
}
const person1 = new Person('Alice');
console.log(Person.prototype.constructor === Person); // true
console.log(person1.constructor === Person); // true
实现原型链继承的方式
1. 构造函数继承
这是最基本的继承方式,通过将父构造函数的prototype
赋值给子构造函数的prototype
:
function Animal(name) {
this.name = name;
}
Animal.prototype.sayName = function() {
console.log('我的名字是:' + this.name);
};
function Dog(name, breed) {
Animal.call(this, name); // 继承属性
this.breed = breed;
}
// 继承方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复constructor指向
Dog.prototype.bark = function() {
console.log('汪汪汪!');
};
const myDog = new Dog('小黑', '拉布拉多');
myDog.sayName(); // 输出:我的名字是:小黑
myDog.bark(); // 输出:汪汪汪!
console.log(myDog.breed); // 输出:拉布拉多
Object.create(proto)
方法创建一个新对象,使用现有的对象作为新创建对象的原型。
2. 类继承(ES6)
ES6引入了class
语法,使继承更加简单易读:
class Animal {
constructor(name) {
this.name = name;
}
sayName() {
console.log('我的名字是:' + this.name);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
bark() {
console.log('汪汪汪!');
}
}
const myDog = new Dog('小白', '金毛');
myDog.sayName(); // 输出:我的名字是:小白
myDog.bark(); // 输出:汪汪汪!
console.log(myDog.breed); // 输出:金毛
虽然使用了class
语法,但JavaScript的继承仍然是基于原型链实现的。class
只是语法糖,让代码看起来更像传统的面向对象语言。
原型链继承的实际应用案例
案例1:创建UI组件库
假设我们正在开发一个UI组件库,可以使用原型链继承来创建基础组件和衍生组件:
// 基础组件
function UIComponent(id) {
this.element = document.getElementById(id);
}
UIComponent.prototype.show = function() {
this.element.style.display = 'block';
};
UIComponent.prototype.hide = function() {
this.element.style.display = 'none';
};
// 按钮组件
function Button(id, text) {
UIComponent.call(this, id);
this.setText(text);
}
Button.prototype = Object.create(UIComponent.prototype);
Button.prototype.constructor = Button;
Button.prototype.setText = function(text) {
this.element.innerText = text;
};
Button.prototype.onClick = function(callback) {
this.element.addEventListener('click', callback);
};
// 使用示例
const submitButton = new Button('submit-btn', '提交');
submitButton.onClick(function() {
console.log('按钮被点击了!');
});
submitButton.hide(); // 隐藏按钮
submitButton.show(); // 显示按钮
案例2:游戏开发中的角色系统
在游戏开发中,我们可以使用原型链继承创建不同类型的游戏角色:
class GameCharacter {
constructor(name, health, power) {
this.name = name;
this.health = health;
this.power = power;
}
attack(target) {
console.log(`${this.name}攻击了${target.name},造成${this.power}点伤害`);
target.health -= this.power;
}
isAlive() {
return this.health > 0;
}
}
class Warrior extends GameCharacter {
constructor(name) {
super(name, 100, 15);
this.armor = 10;
}
// 重写父类方法
attack(target) {
console.log(`${this.name}使用剑攻击了${target.name},造成${this.power}点伤害`);
target.health -= this.power;
}
// 新增特殊技能
defend() {
console.log(`${this.name}举起盾牌,防御力提高`);
this.armor += 5;
}
}
class Mage extends GameCharacter {
constructor(name) {
super(name, 70, 25);
this.mana = 100;
}
// 重写父类方法
attack(target) {
if (this.mana >= 10) {
console.log(`${this.name}施放火球术攻击了${target.name},造成${this.power}点伤害`);
target.health -= this.power;
this.mana -= 10;
} else {
console.log(`${this.name}法力不足,无法施放法术`);
}
}
// 新增特殊技能
teleport() {
if (this.mana >= 30) {
console.log(`${this.name}施放传送术,瞬间移动到安全位置`);
this.mana -= 30;
} else {
console.log(`${this.name}法力不足,无法传送`);
}
}
}
// 使用示例
const warrior = new Warrior('亚瑟');
const mage = new Mage('莫甘娜');
warrior.attack(mage);
// 输出:亚瑟使用剑攻击了莫甘娜,造成15点伤害
mage.attack(warrior);
// 输出:莫甘娜施放火球术攻击了亚瑟,造成25点伤害
warrior.defend();
// 输出:亚瑟举起盾牌,防御力提高
mage.teleport();
// 输出:莫甘娜施放传送术,瞬间移动到安全位置
原型链继承的优缺点
优点
- 内存效率高:所有实例共享原型上的方法,不需要为每个实例创建方法的副本
- 动态扩展:可以在运行时动态地修改原型,影响所有实例
- 灵活性高:可以轻松实现多层次的继承关系
缺点
- 原型属性共享问题:如果原型上有引用类型的属性,所有实例会共享此引用
- 初学者理解难度:原型链概念对初学者来说可能比传统的类继承更难理解
- 构造函数参数传递:在复杂的继承链中,构造函数参数的传递可能变得复杂
function Parent() {
this.colors = ['red', 'blue', 'green'];
}
function Child() {}
Child.prototype = new Parent(); // 继承
const child1 = new Child();
const child2 = new Child();
child1.colors.push('black');
console.log(child2.colors); // ['red', 'blue', 'green', 'black']
// 共享引用类型属性导致的问题
最佳实践
-
使用ES6 class语法:对大多数用例,ES6的class语法更加清晰易用
-
使用Object.create()而不是直接赋值:
javascript// 推荐
Child.prototype = Object.create(Parent.prototype);
// 不推荐
Child.prototype = Parent.prototype; // 这会导致修改Child.prototype也会修改Parent.prototype -
不要忘记修复constructor指向:
javascriptChild.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 修复指向 -
避免在原型上放置引用类型属性:
javascriptfunction Animal() {
// 在构造函数中定义引用类型属性
this.foods = []; // 每个实例都有自己的foods数组
}
// 不要这样做
Animal.prototype.foods = []; // 所有实例共享同一个数组
总结
JavaScript的原型链继承是一种强大而灵活的机制,它允许对象继承其他对象的属性和方法。虽然与传统的基于类的继承不同,但一旦理解了原型链的工作原理,就能利用它创建出灵活而高效的对象继承模型。
ES6引入的class
语法使继承变得更加直观,但理解底层的原型链机制仍然对成为一个专业的JavaScript开发者至关重要。
练习
-
创建一个
Shape
基类,然后创建Circle
和Rectangle
子类。每个类都应该有适当的属性和计算面积的方法。 -
实现一个简单的动物分类系统,使用原型链创建
Animal
基类,然后派生出Mammal
、Bird
类,再从这些类派生出具体的动物种类。 -
修改以下代码,解决共享引用类型属性的问题:
javascriptfunction Parent() {}
Parent.prototype.names = [];
function Child() {}
Child.prototype = new Parent();
const child1 = new Child();
const child2 = new Child();
child1.names.push("John");
console.log(child2.names); // 包含"John",修复这个问题
要深入理解JavaScript原型链,建议阅读《JavaScript高级程序设计》或《你不知道的JavaScript》系列中关于原型的章节。