一篇带你搞懂JavaScript继承的小短文

58 阅读3分钟

引言

在JavaScript中,实现继承的方式主要有以下几种:原型链继承、构造函数继承、组合继承(即使用原型链和构造函数)、原型式继承以及寄生式继承。

ES5的继承方式

1. 原型链继承

通过让子类的原型对象指向父类的一个实例对象来实现继承。

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

Parent.prototype.sayHi = function() {
    console.log('Hi');
};

function Child() {}

// 继承
Child.prototype = new Parent();

const child = new Child();
child.colors.push('yellow');
child.sayHi() // Hi
console.log(child.colors); // ['red', 'blue', 'green', 'yellow']

const child2 = new Child();
console.log(child2.colors); // ['red', 'blue', 'green', 'yellow']

console.log(child.colors === child2.colors); // true

这种方式虽然简单易懂,但是有两个明显的缺点。一是父类的引用属性会被所有的实例共享,二是创建子类实例时不能向父类构造函数传参。

2. 构造函数继承

通过在子类构造函数内部调用父类构造函数来实现继承。

function Parent() {
    this.colors = ['red', 'blue', 'green'];
    this.sayHi = function() {
        console.log('Hi');
    };
}

function Child() {
    Parent.call(this);
}

const child = new Child();
child.colors.push('yellow');
child.sayHi() // Hi
console.log(child.colors); // ['red', 'blue', 'green', 'yellow']

const child2 = new Child();
child2.sayHi() // Hi
console.log(child2.colors); // ['red', 'blue', 'green']

console.log(child.sayHi === child2.sayHi); // false

这种继承方式解决了“原型链继承”主要存在的两个问题。但也有个明显的缺点,方法都在构造函数中定义,每个实例都有自己的方法副本,导致函数复用性差。

3. 组合继承

结合原型链继承和构造函数继承的优点,同时避免它们的缺点。

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

Parent.prototype.sayHi = function() {
    console.log('Hi');
};

function Child() {
    // 第一次调用 Parent 构造函数
    Parent.call(this);
}

// 第二次间接调用 Parent 构造函数
Child.prototype = Object.create(Parent.prototype);

Child.prototype.constructor = Child; // 修正 prototype.constructor 的指向

const child = new Child();
child.colors.push('yellow');
console.log(child.colors); // ['red', 'blue', 'green', 'yellow']

解决了原型链继承和构造函数继承的缺点。实例可以访问父类的方法,而且不会重复创建这些方法。但是这也有个缺点,调用了两次父类构造函数,一次在子类构造函数中,一次在设置原型时。

两次调用父类构造函数分析

在上述代码中,Parent.call(this); 直接调用了父类构造函数 Parent,用于初始化子类 Child 的实例属性。

Child.prototype = Object.create(Parent.prototype); 这行代码则是通过 Object.create() 方法间接调用了父类构造函数。这是因为 Object.create() 方法创建了一个新对象,该对象的原型是传入的对象,即 Parent.prototype。这意味着创建了一个新的 Parent 类型的实例,并将其设置为 Child 类的原型。

所以,在 Child.prototype = Object.create(Parent.prototype); 这行代码中,虽然没有直接调用 Parent 构造函数,但实际上创建了一个新的 Parent 类型的实例。这就是为什么我们说这里有两次调用的原因。

4. 原型式继承

使用一个已有的实例作为新对象的原型,从而实现继承。

function object(o) {
    function F() {};
    F.prototype = o;
    return new F();
}

const parent = {
    colors: ['red', 'blue', 'green'],
    sayHi: function() {
        console.log('Hi');
    }
};

const child = object(parent);
child.colors.push('yellow');
console.log(child.colors); // ['red', 'blue', 'green', 'yellow']

这种继承方式没有调用构造函数的过程,所以没有语义化的构造函数与原型链关系。

5. 寄生式继承

先通过原型式继承创建一个新对象,然后增强这个对象。

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function createObj(o) {
    const clone = object(o);
    clone.sayHello = function() {
        console.log('Hello');
    };
    return clone;
}

const parent = {
    colors: ['red', 'blue', 'green'],
    sayHi: function() {
        console.log('Hi');
    }
};

const child = createObj(parent);
child.colors.push('yellow');
console.log(child.colors); // ['red', 'blue', 'green', 'yellow']

这种继承方式和原型式继承基本一致,所以缺点也与原型式继承一样,但多了一层处理,可以添加额外的功能。

ES6的继承方式

ES6 引入了 classextends 关键字来简化面向对象编程,使得继承更加直观和易于理解。

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

    sayHi() {
        console.log('Hi');
    }
}

class Child extends Parent {
    constructor() {
        super(); // 调用父类的构造函数
        this.newColor = 'yellow';
    }

    sayHello() {
        console.log('Hello');
    }
}

const child = new Child();
child.colors.push('yellow');
console.log(child.colors); // ['red', 'blue', 'green', 'yellow']
child.sayHi(); // Hi
child.sayHello(); // Hello

在现代浏览器和Node.js环境中,由于 ES6 的 extends 提供了更好的语法支持和更高的可读性,通常推荐优先使用 ES6 的继承方式。它不仅简化了继承机制,还提供了更多面向对象编程的特性,如静态方法、私有字段等,这些都是传统继承方式所不具备的。