JavaScript的7种继承方式和各自的优缺点

作者: 贺鹏飞 分类: JavaScript 发布时间: 2021-01-20 11:25

前言:

面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class(ES6 引入了class 语法),而是通过“原型对象”(prototype)实现。

ES5继承

构造函数、原型和实例的关系:每一个构造函数都有一个原型对象,每一个原型对象都有一个指向构造函数的指针,而每一个实例都包含一个指向原型对象的内部指针。

1、原型链实现继承

基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法,即让原型对象等于另一个类型的实例。子类型的原型为父类型的一个实例对象。

function Parent () {
    this.names = ['one', 'two'];
}
function Child () {
 

}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.names); // ["one", "two"]
child1.names.push('three');
console.log(child1.names); // ["one", "two", "three"]
var child2 = new Child();
console.log(child2.names); // ["one", "two", "three"]

这种方式实现的本质是通过将子类的原型指向了父类的实例,所以子类的实例就可以通过__proto__访问到 Student.prototype 也就是Person的实例,这样就可以访问到父类的私有方法,然后再通过__proto__指向父类的prototype就可以获得到父类原型上的方法。于是做到了将父类的私有、公有方法和属性都当做子类的公有属性。

优点:

  • 父类新增原型方法/原型属性,子类都能访问到;
  • 简单,易于实现。

缺点

  • 无法实现多继承;
  • 来自原型对象的所有属性被所有实例共享;
  • 创建子类实例时,无法向父类构造函数传参。

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

这种方式关键在于:在子类型构造函数中通用call()调用父类型构造函数

//例子一
function Parent () {
    this.names = ['one', 'two'];
}
function Child () {
    Parent.call(this);
}
var child1 = new Child();
child1.names.push('three');
console.log(child1.names); // ["one", "two", "three"]
var child2 = new Child();
console.log(child2.names); // ["one", "two"]

//例子二
function Parent (name) {
    this.name = name;
}
function Child (name) {
    Parent.call(this, name);
}
var child1 = new Child('one');
console.log(child1.name); // one
var child2 = new Child('two');
console.log(child2.name); // two

优点:

  • 解决了原型链继承中子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call多个父类对象)

缺点

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承原型属性和方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

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

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
    console.log(this.name)
;
}
function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var child1 = new Child('one', '18');
child1.colors.push('black');
console.log(child1.name); // one
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]
var child2 = new Child('two', '20');
console.log(child2.name); // two
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

这种方式融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。不过也存在缺点就是无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部,子类型最终会包含父类型对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。

优点

  • 可以继承实例属性/方法,也可以继承原型属性/方法
  • 不存在引用属性共享问题
  • 可传参
  • 函数可复用

缺点

  • 调用了两次父类构造函数,生成了两份实例

4.原型式继承

不用严格意义上的构造函数,借助原型可以根据已有的对象创建新对象,还不必因此创建自定义类型,因此最初有如下函数:

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

从本质上讲,createObj()对传入其中的对象执行了一次浅复制。

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}
var person = {
    name: 'one',
    friends: ['two', 'three']
}
var person1 = createObj(person);
var person2 = createObj(person);
person1.name = 'person1';
console.log(person2.name); // one
person1.friends.push('four');
console.log(person2.friends); // [ 'two', 'three', 'four' ]

ES5新增Object.create规范了原型式继承,接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象,在传入一个参数的情况下,Object.create()和createObj()行为相同。

var person = {
    name:'one',
    friendes:['two', 'three']
};
var person1 = Object.create(person,{
    name:{
        value:"four"
    }
});
//用这种方法指定的任何属性都会覆盖掉原型对象上的同名属性
console.log(person1.name);  // four
person.friendes.push('four');
console.log(person1.friendes);  // [ 'two', 'three', 'four' ]

缺点:

  • 包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。

5.寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,它创造一个仅用于封装继承过程的函数,在函数内部以某种方式增强对象,最后再返回对象。

function createObj (o) {
    var clone = Object.create(o);
    clone.sayHi = function () {
        console.log('hi');
    }
    return clone;
}

缺点:

  • 跟借用构造函数模式一样,每次创建对象都会创建一遍方法,会因为做不到函数复用而降低效率。

6. 寄生组合式继承

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,不必为了指定子类型的原型而调用超类型的构造函数,只需要超类型的一个副本。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
    console.log(this.name)
}
function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
}
// 关键的三步
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();

var child1 = new Child('one', '18');
console.log(child1);
//Parent { name: 'one', colors: [ 'red', 'blue', 'green' ], age: '18' }

最后我们封装一下这个继承方法:

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}
function prototype(child, parent) {
    var prototype = object(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}
// 当我们使用的时候:
prototype(Child, Parent);

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

ES6继承

7.ES6中class 的继承

ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的

class Parent {
    //调用类的构造方法
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    //定义一般的方法
    showName() {
        console.log("调用父类的方法")
        console.log(this.name, this.age);
    }
}
let p1 = new  Parent('one', 36)
console.log(p1) // Parent { name: 'one', age: 36 }

//定义一个子类
class Child extends Parent {
    constructor(name, age, salary) {
        super(name, age)//通过super调用父类的构造方法
        this.salary = salary
    }
    showName() {//在子类自身定义方法
        console.log("调用子类的方法")
        console.log(this.name, this.age, this.salary); 
    }
}
let s1 = new Child('two', 32, 28000)
console.log(s1)  // Child { name: 'two', age: 32, salary: 28000 }
s1.showName()
// 调用子类的方法
// two 32 28000

优点

  • 语法简单易懂,操作更方便

缺点

  • 并不是所有的浏览器都支持class关键字

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注