JS中的继承

面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。但Javascript之父在设计语言时,为了使语言轻便,没有引入类的概念,而是通过“原型对象”(prototype)实现。JS中常见的继承方式有基于原型链继承,基于构造函数继承和基于 class 的继承

1.原型链继承

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

模板如下, ⭕️核心就是把子类的prototype设置为父类的实例

// 父类
function Person() {}

// 子类
function Student(){}

// 继承
Student.prototype = new Person()

👇下面来看一个实例:

 //父类型
 function Person(name, age) {
    this.name = name,
        this.age = age,
        this.play = [1, 2, 3]
    this.setName = function () { }
 }
 Person.prototype.setAge = function () { }
 //子类型
 function Student(price) {
    this.price = price
    this.setScore = function () { }
 }
 // 子类型的原型为父类型的一个实例对象
    Student.prototype = new Person() 
    var s1 = new Student(15000)
    var s2 = new Student(14000)
    s1.play.push(4)
    console.log(s1,s2)

打印出来的结果:

可以看到只对s1进行play的修改,s2中的play也发生了变化,这是就是原型链继承的缺点父类的引用属性会被所有子类实例共享。

还有一个缺点就是子类构建实例时不能向父类传递参数。但优点是父类方法可以复用,比如上述的setAge

2.构造函数继承

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

⭕️核心就是在子类(Student)里执行Person.call(this),将父类构造函数的内容复制给了子类的构造函数。

👇下面来看一个实例:

 function Person(name, age) {
    this.name = name,
    this.age = age,
    this.setName = 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)
  var s2 = new Student('Lucy', 20, 15000)
  console.log(s1.setName === s2.setName)

可以发现父类的函数setName(),在子类的实例下是不共享的,构造函数继承的方式中引用属性不会被所有实例共享,构造函数继承的方式不使用prototype继承,而是在子类里面执行父类的构造函数,相当于把父类的代码复制到子类里面执行一遍,这样做的另一个好处就是可以给父类传参。

可以发现原型链继承构造函数继承的优缺点正好完全相反,把他们组合起来就可以得到互补的组合继承

3.组合继承

⭕️组合继承,就是各取上面2种继承的长处,父类中的普通属性使用构造函数继承,函数 使用 原型链继承

👇下面来看一个实例:

function Person(name, age) {
    this.name = name,
    this.age = age,
  }
  Person.prototype.setName = function(){}
  function Student(name, age, price) {
    Person.call(this, name, age)  // 构造函数继承(继承属性)
    this.price = price
  }
  Student.prototype = new Person() // 原型链继承(继承方法)
  var s1 = new Student('Tom', 20, 15000)
  var s2 = new Student('Lucy', 20, 15000)
  console.log(s1.setName === s2.setName)

setName()使用prototype继承就可以被所有子类实例共享

4.原型式继承

⭕️ object内部首先是创建了一个空的构造函数F,然后把F的prototype指向参数proto,最后返回一个F的实例对象,完成继承. 原型式继承看起来跟原型继承很像,事实上,两者因为都是基于prototype继承的,所以也有一些相同的特性,比如引用属性共享问题, 那原型式继承跟原型继承有什么区别呢? 一个比较明显的区别就是clone函数接收的参数不一定要是构造函数,也可以是其他任何对象, 这样我们就相当于是浅复制了一个对象.

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

使用:

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"

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

所以ES5的Object.create()函数,就是基于原型式继承的,上文中代码可以转变为

var yetAnotherPerson = object(person); => var yetAnotherPerson = Object.create(person);

5.寄生组合式继承

组合继承有个缺点,就是父类构造函数里面的代码会执行2遍.

  • 第一次:Student.prototype = new Person()

‘new 的过程’的第三步,其实就是执行了父类构造函数。

复习new的过程

  • 第二次: Person.call(this, name, age)

call的作用是改变函数执行时的上下文。比如:A.call(B)。其实,最终执行的还是A函数,只不过是用B来调用而已。所以,你就懂了Parent.call(this,name,like) ,也就是执行了父类构造函数。

可以用寄生组合式继承来解决这个问题

function inherit(sub, super){
    let prototype = clone(super.prototype)
    prototype.constructor = sub    
    sub.prototype = prototype      
}

这样我们就实现了一个寄生组合式继承的函数inherit,接下来我们来使用一下:

function Person(name){}

function Student(){
    SuperType.call(this)
}

inherit(Student, Person)

我们用 inherit 函数替换了 Student.prototype = new Person() ,从而避免了执行 new Person().

这是一种完美的继承方式。

6.class 的继承

ES6中引入了class关键字,class可以通过extends关键字实现继承,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的

⭕️ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

👇下面来看一个实例:

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)