Skip to content

JavaScript继承完全指南

前言

继承是面向对象编程的重要概念,JavaScript作为一门基于原型的语言,提供了多种实现继承的方式。本文将详细介绍JavaScript中的各种继承方式及其优缺点。

一、原型链继承

1.1 基本原理

通过将子类的原型指向父类的实例来实现继承。

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

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

function Child() {}

// 核心:子类原型指向父类实例
Child.prototype = new Parent();
Child.prototype.constructor = Child;

// 使用
const child1 = new Child();
const child2 = new Child();

child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green', 'black']

1.2 优缺点

优点:

  • 继承了父类的属性和方法
  • 可以继承父类原型上的属性和方法

缺点:

  • 引用类型的属性被所有实例共享
  • 创建子类实例时无法向父类构造函数传参

二、构造函数继承

2.1 基本原理

在子类构造函数中调用父类构造函数。

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

function Child(name) {
    // 核心:调用父类构造函数
    Parent.call(this, name);
}

// 使用
const child1 = new Child('child1');
const child2 = new Child('child2');

child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green']

2.2 优缺点

优点:

  • 避免了引用类型的属性被所有实例共享
  • 可以向父类构造函数传参

缺点:

  • 方法都在构造函数中定义,每次创建实例都会创建一遍方法
  • 无法继承父类原型上的方法

三、组合继承

3.1 基本原理

结合原型链继承和构造函数继承。

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);
    this.age = age;
}

// 第二次调用父类构造函数
Child.prototype = new Parent();
Child.prototype.constructor = Child;

Child.prototype.getAge = function() {
    return this.age;
};

// 使用
const child1 = new Child('child1', 18);
const child2 = new Child('child2', 20);

child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green']
console.log(child1.getName()); // 'child1'
console.log(child2.getAge()); // 20

3.2 优缺点

优点:

  • 融合原型链继承和构造函数的优点
  • 是JavaScript中最常用的继承模式

缺点:

  • 会调用两次父类构造函数

四、寄生组合继承

4.1 基本原理

通过寄生方式,砍掉父类的实例属性,避免了组合继承中多余的父类实例属性。

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);
    this.age = age;
}

// 核心:将父类原型复制给子类
function inheritPrototype(child, parent) {
    const prototype = Object.create(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

inheritPrototype(Child, Parent);

Child.prototype.getAge = function() {
    return this.age;
};

// 使用
const child1 = new Child('child1', 18);
const child2 = new Child('child2', 20);

console.log(child1.getName()); // 'child1'
console.log(child2.getAge()); // 20

4.2 优缺点

优点:

  • 只调用一次父类构造函数
  • 避免了在子类原型上创建多余的属性
  • 原型链保持不变

缺点:

  • 实现较为复杂

五、ES6 Class继承

5.1 基本语法

使用class关键字实现继承。

javascript
class Parent {
    constructor(name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }
    
    getName() {
        return this.name;
    }
}

class Child extends Parent {
    constructor(name, age) {
        super(name); // 调用父类构造函数
        this.age = age;
    }
    
    getAge() {
        return this.age;
    }
}

// 使用
const child = new Child('child', 18);
console.log(child.getName()); // 'child'
console.log(child.getAge()); // 18

5.2 特点

  • 语法更加清晰、易懂
  • 实现了真正的继承
  • 必须先调用super()
  • 类不存在变量提升

六、实际应用示例

6.1 实现一个简单的组件继承系统

javascript
class Component {
    constructor(props = {}) {
        this.props = props;
    }
    
    setState(state) {
        this.state = { ...this.state, ...state };
        this.render();
    }
    
    render() {
        throw new Error('Component must implement render method');
    }
}

class Button extends Component {
    constructor(props) {
        super(props);
        this.state = { clicked: false };
    }
    
    render() {
        return `<button class="${this.state.clicked ? 'active' : ''}">${this.props.text}</button>`;
    }
}

const button = new Button({ text: '点击我' });

6.2 实现一个事件发射器

javascript
class EventEmitter {
    constructor() {
        this._events = {};
    }
    
    on(event, callback) {
        if (!this._events[event]) {
            this._events[event] = [];
        }
        this._events[event].push(callback);
        return this;
    }
    
    emit(event, ...args) {
        const callbacks = this._events[event];
        if (callbacks) {
            callbacks.forEach(callback => callback.apply(this, args));
        }
        return this;
    }
}

class Logger extends EventEmitter {
    log(message) {
        console.log(message);
        this.emit('log', message);
    }
}

const logger = new Logger();
logger.on('log', message => console.log('Logged:', message));

七、面试常见问题

7.1 原型链继承和class继承的区别?

javascript
// 原型链继承
function Animal() {}
Animal.prototype.eat = function() {};

function Dog() {}
Dog.prototype = new Animal();

// class继承
class Animal {
    eat() {}
}

class Dog extends Animal {
    constructor() {
        super();
    }
}

主要区别:

  1. 语法更加清晰
  2. class继承必须调用super
  3. class不存在变量提升
  4. class内部默认严格模式

7.2 如何实现多继承?

javascript
// 使用混入(Mixin)模式
const MixinA = {
    methodA() {}
};

const MixinB = {
    methodB() {}
};

class MyClass {
    constructor() {
        Object.assign(this, MixinA, MixinB);
    }
}

八、最佳实践建议

  1. 优先使用ES6的class继承
  2. 避免深层次的继承关系
  3. 优先使用组合而不是继承
  4. 注意继承带来的耦合问题
  5. 合理使用super关键字
javascript
// 不好的写法
class A extends B extends C extends D {
    // 深层次继承
}

// 好的写法
class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {};
    }
}

总结

JavaScript提供了多种继承实现方式,每种方式都有其适用场景:

  1. 原型链继承:最基础的继承方式
  2. 构造函数继承:解决引用类型共享问题
  3. 组合继承:最常用的继承方式
  4. 寄生组合继承:最理想的继承方式
  5. ES6 class继承:最现代的继承方式

在实际开发中,推荐使用ES6的class继承,它提供了更清晰的语法和更好的功能支持。同时,要注意继承带来的复杂性,合理使用继承来构建代码。