跳到主要内容

JavaScript 原型链继承

在JavaScript中,继承是通过原型链实现的,这与其他面向对象编程语言中的类继承有很大不同。理解原型链是掌握JavaScript面向对象编程的关键。本文将帮助你全面了解JavaScript原型链继承的工作原理和实际应用。

什么是原型链?

在JavaScript中,每个对象都有一个内部链接指向另一个对象,称为它的原型(prototype)。这个原型对象也有自己的原型,以此类推,形成了所谓的"原型链",直到达到一个原型为null的对象。

当我们尝试访问一个对象的属性时,JavaScript引擎会:

  1. 先检查对象本身是否有该属性
  2. 如果没有,则检查对象的原型
  3. 如果原型也没有,则继续检查原型的原型
  4. 这个过程会一直持续到找到该属性或到达原型链的末端(null

原型相关的重要概念

在深入了解原型链继承之前,需要理解几个关键概念:

1. __proto__ vs prototype

  • __proto__:每个对象都有的内部属性,指向该对象的原型
  • prototype:函数对象的特有属性,当函数作为构造函数使用时,新创建的对象的__proto__会指向这个prototype
javascript
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属性,指回与之关联的构造函数。

javascript
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

javascript
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语法,使继承更加简单易读:

javascript
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组件库,可以使用原型链继承来创建基础组件和衍生组件:

javascript
// 基础组件
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:游戏开发中的角色系统

在游戏开发中,我们可以使用原型链继承创建不同类型的游戏角色:

javascript
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();
// 输出:莫甘娜施放传送术,瞬间移动到安全位置

原型链继承的优缺点

优点

  1. 内存效率高:所有实例共享原型上的方法,不需要为每个实例创建方法的副本
  2. 动态扩展:可以在运行时动态地修改原型,影响所有实例
  3. 灵活性高:可以轻松实现多层次的继承关系

缺点

  1. 原型属性共享问题:如果原型上有引用类型的属性,所有实例会共享此引用
  2. 初学者理解难度:原型链概念对初学者来说可能比传统的类继承更难理解
  3. 构造函数参数传递:在复杂的继承链中,构造函数参数的传递可能变得复杂
javascript
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']
// 共享引用类型属性导致的问题

最佳实践

  1. 使用ES6 class语法:对大多数用例,ES6的class语法更加清晰易用

  2. 使用Object.create()而不是直接赋值

    javascript
    // 推荐
    Child.prototype = Object.create(Parent.prototype);

    // 不推荐
    Child.prototype = Parent.prototype; // 这会导致修改Child.prototype也会修改Parent.prototype
  3. 不要忘记修复constructor指向

    javascript
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child; // 修复指向
  4. 避免在原型上放置引用类型属性

    javascript
    function Animal() {
    // 在构造函数中定义引用类型属性
    this.foods = []; // 每个实例都有自己的foods数组
    }

    // 不要这样做
    Animal.prototype.foods = []; // 所有实例共享同一个数组

总结

JavaScript的原型链继承是一种强大而灵活的机制,它允许对象继承其他对象的属性和方法。虽然与传统的基于类的继承不同,但一旦理解了原型链的工作原理,就能利用它创建出灵活而高效的对象继承模型。

ES6引入的class语法使继承变得更加直观,但理解底层的原型链机制仍然对成为一个专业的JavaScript开发者至关重要。

练习

  1. 创建一个Shape基类,然后创建CircleRectangle子类。每个类都应该有适当的属性和计算面积的方法。

  2. 实现一个简单的动物分类系统,使用原型链创建Animal基类,然后派生出MammalBird类,再从这些类派生出具体的动物种类。

  3. 修改以下代码,解决共享引用类型属性的问题:

    javascript
    function 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》系列中关于原型的章节。