前端面试篇之JS继承(三)

471 阅读4分钟

前面总结了一下js数组、字符串身上的方法。

前端面试篇之JS数组(一)

前端面试篇之JS字符串(二)

这篇来总结一下js中继承的方法

1. 原型链继承

原型链继承是通过将子类型的原型设置为父类型的实例来实现的。所有实例共享父类型的引用属性,但也因此存在共享数据导致的问题。

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

Parent.prototype.sayName = function() {
    console.log(this.name);
};

function Child() {}

Child.prototype = new Parent();

let child1 = new Child();
child1.colors.push('black');
console.log(child1.colors); // ["red", "blue", "green", "black"]

child1.sayName(); // Parent

let child2 = new Child();
console.log(child2.colors); // ["red", "blue", "green", "black"] 共享了colors数组

这种方式的关键是这段代码Child.prototype = new Parent(),创建Parent的实例,然后赋值给Child.prototype.

缺点:子类型实例共享父类型的引用属性,修改一个实例的属性会影响其他实例。此外,无法向父类型传递参数。

2. 借用构造函数(经典继承)

通过在子类型的构造函数中调用父类型的构造函数,可以实现父类型的属性继承。每个实例都有独立的属性,但无法继承父类型原型上的方法。

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

function Child(name) {
    Parent.call(this, name); // 借用Parent的构造函数
}

let child1 = new Child('child1');
child1.colors.push('black');
console.log(child1.colors); // ["red", "blue", "green", "black"]

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

这里主要是在子构造函数Child中通过Parent.call(this, name)调用了父构造函数Parent,这样使得Child的实例能够拥有自己的name属性和独立的colors数组,而不是简单地从Parent的原型链上继承这些属性,从而确保了每个Child实例的数据独立性,避免了实例间的属性污染。

缺点:无法继承父类型原型链上的方法。

3. 组合继承(原型链 + 借用构造函数)

组合继承结合了原型链继承和借用构造函数的优点,通过原型链继承方法,通过构造函数继承属性。

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

Parent.prototype.sayName = function() {
    console.log(this.name);
};

function Child(name, age) {
    Parent.call(this, name); // 第二次调用Parent构造函数
    this.age = age;
}

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

let child1 = new Child('child1', 18);
child1.colors.push('black');
console.log(child1.colors); // ["red", "blue", "green", "black"]
child1.sayName(); // "child1"

let child2 = new Child('child2', 20);
console.log(child2.colors); // ["red", "blue", "green"]
child2.sayName(); // "child2"

缺点:Parent 构造函数被调用了两次,造成了多余的属性存在于子类型的原型链上。

4. 原型式继承

原型式继承通过对一个已有对象进行浅复制来实现。

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

let parent = {
    name: 'Parent',
    colors: ['red', 'blue', 'green']
};

let child = object(parent);
child.name = 'Child';
child.colors.push('black');

console.log(child.name); // "Child"
console.log(child.colors); // ["red", "blue", "green", "black"]

let child2 = object(parent);
console.log(child2.colors); // ["red", "blue", "green", "black"]

核心实现是function object(o) {}利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

缺点:与原型链继承相同,共享引用类型属性。

5. 寄生式继承

寄生式继承通过创建一个封装继承过程的函数,对对象进行扩展并返回该对象。

function createAnother(original) {
    let clone = Object.create(original); // 创建新对象
    clone.sayHi = function() {
        console.log('Hi');
    };
    return clone;
}

let parent = {
    name: 'Parent',
    colors: ['red', 'blue', 'green']
};

let child = createAnother(parent);
child.sayHi(); // "Hi"

缺点:无法实现函数的复用,每次都要重新定义方法。

6. 寄生组合继承

寄生组合继承通过借用构造函数继承属性,通过原型链继承方法,但避免了多余属性的创建。

function inheritPrototype(child, parent) {
    let prototype = Object.create(parent.prototype); // 创建父类原型的副本
    prototype.constructor = child; 
    child.prototype = prototype; 
}

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

Parent.prototype.sayName = function() {
    console.log(this.name);
};

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

inheritPrototype(Child, Parent);

let child1 = new Child('child1', 18);
child1.colors.push('black');
console.log(child1.colors); // ["red", "blue", "green", "black"]
child1.sayName(); // "child1"

let child2 = new Child('child2', 20);
console.log(child2.colors); // ["red", "blue", "green"]
child2.sayName(); // "child2"

优点:避免了组合继承中多余属性的创建,且保留了借用构造函数的优点。

7. ES6 类继承

在 ES6 中,引入了 class 关键字,使得继承的实现变得更加直观和简洁。类继承通过 extends 关键字来实现,子类可以通过 super() 调用父类的构造函数。

代码示例:

javascript
复制代码
class Parent {
    constructor(name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }

    sayName() {
        console.log(this.name);
    }
}

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

    sayAge() {
        console.log(this.age);
    }
}

let child1 = new Child('child1', 18);
child1.colors.push('black');
console.log(child1.colors); // ["red", "blue", "green", "black"]
child1.sayName(); // "child1"
child1.sayAge(); // 18

let child2 = new Child('child2', 20);
console.log(child2.colors); // ["red", "blue", "green"]
child2.sayName(); // "child2"
child2.sayAge(); // 20

优点

  • 语法更加简洁明了,类的定义和继承更加直观。
  • 可以在子类构造函数中使用 super() 调用父类的构造函数,从而继承父类的属性和方法。
  • 支持静态方法和静态属性的定义。

这个类继承的方法就感觉更像传统面向对象语言的继承方式。如c++、java差不多。