JavaScript中的继承及其优缺点(七种)

182 阅读11分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

哈喽,我是刘十一,今天我们一起来看看JavaScript中有哪些继承以及它们各自的优缺点吧!!

一、原型链继承

1、关键

子类型的原型 为 父类型的 一个 实例对象。

2、代码示例

 //父类型
 function Person(name, age) {
     this.name = name,
     this.age = age,
     this.play = [1, 2, 3]
     this.setName = function () { }
 }
 Person.prototype.setAge = function () { 
     console.log("111")
 }

 //子类型
 function Student(price) {
     this.price = price
     this.setScore = function () { }
 }
 // Student.prototype.sayHello = function () { }
 //在这里写子类的原型方法和属性是无效的,
 //因为会改变原型的指向,所以应该放到重新指定之后
 Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象
 //Student.prototype.sayHello = function () { }
 //放在此处
 var s1 = new Student(15000)
 var s2 = new Student(14000)
 console.log(s1,s2)

3、分析

本质是通过 将子类的原型 指向 父类的实例,故,子类的实例就可通过 proto 访问到 Student.prototype,即 Person的实例,如此就可以访问到父类的私有方法,随后再通过 proto 指向父类的prototype获取到父类原型上的方法。 即,实现了将 父类的私有、共有方法和属性都当做子类的共有属性

4、优缺点

1)优点

(1)父类新增原型方法/原型属性,子类都能访问到

(2)简单,易于实现

2)缺点

(1)无法实现多继承

(2)来自原型对象的所有属性被所有实例共享,父类的私有属性为引用类型时,子类s1操作这个属性的时候,就会影响到子类s2

(3)创建子类实例时,无法向父类构造函数传参

(4)要想为子类新增属性和方法,必须要在Student.prototype = new Person() 之后执行,不能放到构造器中(因为会改变原型的指向,所以应该放到重新指定之后)

二、构造函数继承

1、关键

在 子类型构造函数中 通过 call () 调用 父类型构造函数

2、代码示例

 function Person(name, age) {
    this.name = name,
    this.age = age,
    this.setName = function () {}
  }
  Person.prototype.setAge = function () {}
  function Student(name, age, price) {
    Person.call(this, name, age)  // 相当于: this.Person(name, age)
    // this.name = name
    // this.age = age
    this.price = price
  }
  var s1 = new Student('Tom', 20, 15000)
  console.log(s1.setAge()) // Uncaught TypeError: s1.setAge is not a function

3、分析

这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些在父类原型上的方法和属性的。

4、优缺点

1)优点

(1)解决了原型链继承中 子类实例 共享 父类属性的问题

(2)创建子类实例时,可以向父类传递参数

(3)可以实现多继承(call 多个父类dui)

2)缺点

(1)实例并不是父类是实例,只是子类的实例

(2)只能继承父类的实例的属性和方法,不能继承原型属性和方法

(3)无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

三、原型式继承

1、关键

原型式继承要求必须有一个对象可以作为另一个对象的基础

2、代码示例

function object(o){
    function F(){}
    F.prototype = 0;
    return new F();
}
//在object函数内部,先创建了一个临时的构造函数,
//然后将传入的对象作为该构造函数的原型,
//最后返回了这个临时类型的新实例。
//从本质上讲,object() 对传入其中的对象执行了一次浅复制。

var person = {
    name: 'Alvin',
    friends: ['Yannis','Ylu']
}

var p1 = object(person);
p1.name = 'Bob';
p1.friends.push('Lucy');

var p2 = object(person);
p2.name = 'Lilei';
p2.friends.push('Hanmeimei');

console.log(person.friends);//Yannis, Ylu, Lucy, Hanmeimei

3、分析

在这个例子中可以作为另一个对象基础的是person对象, 于是我们把它传入到object函数中, 然后该函数就会返回一个新对象,这个新对象将person作为原型, 所以它的原型中就包含了一个基本类型属性name和一个引用类型属性friends。 这就意味着person.friends不仅属于person所有,而且也会被p1和p2共享。 实际上就相当于创建了person对象的两个副本。

4、拓展

在ECMAScript5中新增了Object.create()方法,该方法规范了原型式继承。 这个方法接收两个参数:一个用作新对象原型的对象,另一个是可选的,用于新对象定义额外的属性的对象。 在只传入一个参数的情况下,Object.create()与上面的object()方法行为相同。看下面示例:

var person = {
    name: 'Alvin',
    friends: ['Yannis','Ylu']
}

var p1 = Object.create(person);
p1.name = 'Bob';
p1.friends.push('Lucy');

var p2 = Object.create(person);
p2.name = 'Lilei';
p2.friends.push('Hanmeimei');

console.log(person.friends);//Yannis, Ylu, Lucy, Hanmeimei

Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义。 以 这种方式指定任何属性都会覆盖原型对象上的同名属性。如:

var person = {
    name: 'Alvin',
    friends: ['Yannis','Ylu']
}

var p1 = Object.create(person,{
    name:{
        value:'Lucy'
    }
})

console.log(p1.name);//Lucy

5、优缺点

1)优点

ECMAScript5通过新增 Object.create()方法规范了原型式继承。

2)缺点

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

四、寄生式继承

1、关键

构造一个实例,让实例(instance)的prototype 代理 父类的prototype。

2、代码示例

 //父类型
 function Person(name, age) {
     this.name = name,
     this.age = age,
     this.play = [1, 2, 3]
     this.setName = function () { }
 }
 Person.prototype.setAge = function () { 
     console.log("111")
 }

 //子类型
 function Student(price) {
     Person.call(this, name,age);  // 相当于: this.Person(name,age)
     this.price = price
     this.setScore = function () { }
 }
 
// 关键的三步
var F = function () {};

F.prototype = Person.prototype;

Student.prototype = new F();

3、分析

有个问题:一定需要中间变量吗,直接把Student的prototype指向Person不可以吗? Child.prototype = Parent.prototype 其实这样做是有效果的,但是两个prototype指向了同一个对象的引用,子类的独立性就没了。 这就是为什么处理原型链时必须new一次。

4、寄生式继承的思路

我们期望得到的结果是, 能让子类的原型间接和父类联系起来。 如果能有一个原型, prototype是父类的, 然后让 子类的prototype 指向 这个原型,不就联系上了吗?

Obj.prototype = Parent.prototype
Child.prototype = new Obj()

可以发现,prototype被添加到了原型链中,而且并没有调用构造函数。

5、优缺点

1)优点

(1)使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承。

2)缺点

(1)虽然其优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。

(2)跟构造函数继承类似,调用一次函数就得创建一遍方法,无法实现函数复用,效率较低

五、组合继承(原型链+构造函数)

1、关键

通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

2、代码示例

function Person(name, age) {
    this.name = name,
    this.age = age,
    this.setAge = function () { }
}
Person.prototype.setAge = function () {
    console.log("111")
}
function Student(name, age, price) {
    Person.call(this,name,age)
    this.price = price
    this.setScore = function () { }
}
Student.prototype = new Person()
Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的
Student.prototype.sayHello = function () { }
var s1 = new Student('Tom', 20, 15000)
var s2 = new Student('Jack', 22, 14000)
console.log(s1)
console.log(s1.constructor) //Student
console.log(Person.prototype.constructorr) //Person

3、分析

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

4、优缺点

1)优点

(1)可以继承实例属性/方法,也可以继承原型属性/方法

(2)不存在引用属性共享问题

(3)可传参

(4)函数可复用

2)缺点

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

六、组合继承(优化一)

1、关键

通过 父类原型和子类原型 指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点

2、代码示例

function Person(name, age) {
    this.name = name,
    this.age = age,
    this.setAge = function () { }
}
Person.prototype.setAge = function () {
    console.log("111")
}
function Student(name, age, price) {
    Person.call(this,name,age)
    this.price = price
    this.setScore = function () { }
}
Student.prototype = Person.prototype
Student.prototype.sayHello = function () { }
var s1 = new Student('Tom', 20, 15000)
console.log(s1)

3、分析

但这种方式没办法辨别是对象是子类还是父类实例化

4、优缺点

1)优点

(1)避免了组合继承的缺点,不会初始化两次 实例方法/属性

2)缺点

(1)没办法辨别实例是 子类 还是 父类 创造的,子类与父类的构造函数 指向了同一个

七、组合继承(优化二)

1、关键

借助原型 可以基于 已有的对象 来创建对象。 var B = Object.create(A)以A对象为原型,生成了B对象。 B继承了A的所有属性和方法。

2、代码示例

function Person(name, age) {
    this.name = name,
    this.age = age
}
Person.prototype.setAge = function () {
    console.log("111")
}
function Student(name, age, price) {
    Person.call(this,name,age)
    this.price = price
    this.setScore = function () { }
}
Student.prototype = Objectcreate(Person.prototype)//核心代码

Student.prototype.constructor = Student //核心代码
var s1 = new Student('Tom', 20, 15000)
console.log(s1 instanceof Student, s1 instanceof Person) // true true
console.log(s1.constructor) //Student
console.log(s1)

3、分析

Student 继承了所以 Person 原型对象的属性和方法。 目前来说,是最完美的继承方法。

4、优缺点

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

八、寄生组合式继承

1、关键

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

2、代码示例

下面代码展示了 寄生组合式继承的 基本模式:

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

function inheritPrototype(son,father){
    var prototype = object(father.prototype);//创建对象
    prototype.constructor = son;//增强对象
    son.prototype = prototype;//指定对象
}
//在这个例子中inheritPrototype()函数 实现了寄生组合式继承的最简单形式。
//这个函数接收两个参数:子类型构造函数和父类型的构造函数。
//在函数内部,第一步是创建父类型原型的一个副本。
//第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。
//最后一步就是将新创建的副本赋值给子类型的原型。

这样我们就可以调用inheritPrototype()函数来替换上面例子中为子类型原型赋值的语句了,如下:

function Father(name){
    this.name = name;
    this.friends = ['Yannis','Lucy']
}

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

function Son(name, age){
    Father.call(this,name);//第二次调用Father()
    this.age = age;
}

//Son.prototype = new Father();//第一次调用Father()

inheritPrototype(Son,Father); //新语句
//或者直接用Object.create()也可以实现,效果是一样的
//Son.prototype = Object.create(Father.prototype);
//Son.prototype.constructor = Son;

Son.prototype.sayAge = function(){
    console.log(this.age)
}

3、分析

这个例子的高效率体现在它只调用了一次Father构造函数,并且因此避免了在Son.prototype上创建不必要的多余的属性。与此同时,原型链还能保持不变。

这就是寄生式组合继承,也是目前相对来说比较好的继承方式.

4、寄生组合式继承的思路

不必为了子类型的原型而调用父类型的构造函数,我们所需的无非是要父类型原型的一个副本而已。 本质上就是使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。

5、优缺点

组合继承优点、寄生继承的优点,目前JS继承中使用的都是这个继承方法

九、ES 6 中 class继承

ES6 中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字 定义类的 静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。 ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面( Parent.apply(this) )。 ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

1、关键

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

2、代码示例

class Person {
  //调用类的构造方法
  constructor(name, age) {
     this.name = name
     this.age = age
   }
  //定义一般的方法
  showName() {
     console.log("调用父类的方法")
     console.log(this.name, this.age);
   }
}

let p1 = new  Person('kobe', 39)
console.log(p1)

//定义一个子类
class Student extends Person {
   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 Student('wade', 38, 1000000000)
console.log(s1)
s1.showName()

3、优缺点

1)优点

(1)语法简单易懂,操作更方便

2)缺点

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