JS中实现继承的方式

104 阅读9分钟

1.背景知识

1.1 什么是

说继承之前,先简单聊一下类,类就是将一些事物的共同特征、属性抽取出来(抽象),组装在一起,形成一个类,这个类就相当于模板,我们可以依照这个模板批量化地生成对象,然后在对生成的对象作进一步的操作。例如:我们要做一款游戏,里面有很多的人物,这些人物都有姓名、年龄、性别等属性,也有走路、吃饭、攻击等动作。我们可以把这些共有的特征抽离出来形成一个类,再基于这个类去生成各个人物。

1.2 为什么要继承

继承这个概念属于面向对象的内容,继承是面向对象三大特性之一(封装、继承、多态),它最大的作用就是实现代码复用,减少冗余。例如A类具有姓名、年龄属性和走路吃饭等动作,B类也具有这些,并且B类还有自己独特的内容,例如家庭地址属性、跳舞的动作,但是如果在B类当中重复写一遍姓名、年龄、走路吃饭这些东西,就会产生代码冗余,也会造成内存上的浪费,此时继承就可解决这个问题,B继承了A,B就有了原属于A的属性、方法,然后B再添加自己独有的属性、方法即可,这样就会大大减少冗余 ,实现了代码复用。

1.3 JavaScript如何实现继承

JavaScript当中没有类的概念,因此需要借助其他的东西来实现类、实现继承。在ES5中,使用构造函数代表类,通过原型链、构造函数、组合式继承、寄生式组合继承等方式来实现继承。

在ES6中,加入了class关键字,可以方便地实现继承

2.ES5实现继承

2.1 原型链法实现继承

声明好两个构造函数,一个作为父类,一个作为子类。为了方便,下文都把构造函数称为类。

关键点,将子类的prototype 赋值为 父类的实例对象。

即: 子类.prototype = new 父类();

因为new 父类(); 产生的是一个父类的实例对象,这个对象本身就已经具备了父类的属性和方法,将它赋值给子类的prototype,就可以让子类的实例化对象顺着原型链找到这里,自然实现了继承。

        //父类
        function Person (){
            this.name = '人';
        }
        Person.prototype.eating = function(){
            console.log(this.name+'吃饭了');
        }
        
         //子类
        function Student(){
            this.num = '1'
        }
        
        //原型链继承
        Student.prototype = new Person();
        
        // 原型的实例等于自身
        Student.prototype.constructor = Student;
        
        Student.prototype.studying = function(){
            console.log(this.name + '正在学习');
        }
        
        //实例化子类
        let stu1 = new Student();
        console.log(stu1.name);
        console.log(stu1.num);
        stu1.eating();
        stu1.studying();
​

1669119481775.png

可以看到,父类中的name属性,eating方法都可以被子类的实例对象使用,实现了继承。

注意一点,如果要给子类的prototype上添加一些方法,就比如上面的studying,就一定要把Student.prototype = new Person();写在前面,因为这句代码一旦执行,子类原有的prototype就会被抛弃,添加在原来的prototype上的任何属性、方法也都随之废弃了, 所以要在,这样我们添加的方法才能加到新的prototype上面。

这种方法也有缺点

原型链法的缺点

1.继承得来的属性,直接打印看不到,但可以 通过原型链访问到

例如,打印上面的stu1对象,结果如下

1669120925330.png

依次展开prototype,就可以看到继承的属性。

1669121055156.png

2.子类的多个实例对象,他们的_ _ proto_ _ 都指向同一个对象,也就是new出来的父类实例,如果这里面有引用类型的数据,某一个子类的实例对象修改了这个引用类型数据,其他实例对象都会受到影响。

//父类
        function Person (){
            this.name = '人';
            //加入引用类型的数据
            this.obj = {
                age:'18'
            }
        }
        Person.prototype.eating = function(){
            console.log(this.name+'吃饭了');
        }
​
        //子类
        function Student(){
            this.num = '1'
        }
​
        //原型链继承
        Student.prototype = new Person();
​
        Student.prototype.studying = function(){
            console.log(this.name + '正在学习');
        }
        
        //实例化3个对象
        let stu1 = new Student();
        let stu2 = new Student();
        let stu3 = new Student();
        
        //其中一个对象修改数据
        stu1.obj.age = '3';
        console.log('stu2',stu2.obj.age);
        console.log('stu3',stu3.obj.age);
​

打印结果,数据已经改变,影响了所有的子类对象

1669121664371.png

3.没法给父类传递参数

2.2利用构造函数实现继承

为了解决上述原型链法的缺点,构造函数法出现了,具体如下:

声明好父类子类,实现继承的关键点就是 在子类中调用父类,并且使用call或者apply将父类this指向子类的this,这样也能顺势实现向父类传参。

        //父类
        function Person (name,age,obj){
            this.name = name;
            this.age = age;
            this.obj = obj
        }
        Person.prototype.eating = function(){
            console.log(this.name+'吃饭了');
        }
​
        //子类中调用父类,并绑定this
        function Student(name,age,obj){
            Person.call(this,name,age,obj)
            this.sum = 1
        }
        
         Student.prototype.studying = function(){
            console.log(this.name + '正在学习');
        }
        
        //实例化子类
        let stu1 = new Student('大龙',16,{name:'大龙对象'});
        let stu2 = new Student('二龙',17,{name:'二龙对象'});
        let stu3 = new Student('三龙',18,{name:'三龙对象'});
        

打印出stu1

1669451705181.png

可以看到,继承来的属性都展示在对象中了,可以直接使用,传递的参数也起到了作用。

为什么能继承? 我的理解是,子类中调用父类,并且改变父类的this为子类的this,就相当于父类中的this都改为子类

//父类
function Person (name,age,obj){
            Student.name = name;
            Student.age = age;
            Student.obj = obj
        }

或者可以这么理解,以上的操作相当于直接把父类里面的代码 放在子类里面

//子类 function Student(name,age,obj){ Person.call(this,name,age,obj) 把这一行换成下面三行

this.name = name;

this.age = age;

this.obj = obj

this.sum = 1 }

这样一看,就成功继承父类的属性。并且每个实例化对象都有自己的属性,并不共用同一个,所以不会出现原型链法的缺点2。上面已经实例化了 stu1 、stu2,可以打印测试一下:

1669452721819.png

stu1里面obj.name 被修改了,stu2.obj.name不受影响。

构造函数法的缺点

不能够继承父类prototype上的属性或者方法,可以打印测试一下

1669452950301.png

可以看到,父类的prototype上有eating方法, 但是子类的实例化对象是访问不到的。

2.3组合式继承

前面提到的2种方法都有问题,那我们结合一下他们,采用组合式继承

        //父类
        function Person (name,age,obj){
            this.name = name;
            this.age = age;
            this.obj = obj
        }
        Person.prototype.eating = function(){
            console.log(this.name+'吃饭了');
        }
​
        //子类
        function Student(name,age,obj){
            Person.call(this,name,age,obj)
            this.sum = 1
        }
​
        //这里使用原型链的方法
        Student.prototype = new Person();
        
         // 原型的实例等于自身
        Student.prototype.constructor = Student;
​
        Student.prototype.studying = function(){
            console.log(this.name + '正在学习');
        }
        
        //实例化,并且打印
        let stu1 = new Student('大龙',16,{name:'大龙对象'});
        console.log(stu1);
        stu1.eating();

打印结果如下:

1669453666340.png

看上去,前面2种方法的缺点我们都已经解决了,但是这种方法也有缺点

组合式继承的缺点

使用子类去实例化对象的时候,父类被执行了两次,一次是在call的时候,call()会调用一次,还有一次是 new Person(),增加了性能开销。

另外,我们new Person() 时产生了一个对象(把它作为了Student.prototype),里面有未定义的name,age,obj,他们也是占用着内存空间的,可以打印看一下:

1669477284845.png

2.4寄生式组合继承

在聊寄生式组合继承之前,我们先了解一下原型继承 和 继承继承

2.4.1原型式继承

这里介绍一个很重要的函数,Object.create() ,它可以创建一个新的对象,并且这个对象的原型可以被指定。例如:

let animal = {
  eats: true
};
​
// 创建一个以 animal 为原型的新对象
let rabbit = Object.create(animal); // 与 {__proto__: animal} 相同

可以看到,rabbit._ _ proto _ _ 就是animal,这种情况下,就相当于我们直接实现继承。

1669617868725.png

        let person = {
            name:'我是父亲',
            age:'18',
            obj:{
                name:'obj的name'
            },
        }
​
        let stu1 = Object.create(person);
        stu1.name = '我是stu1';
        stu1.obj.name = 'stu1修改了'
​
        let stu2 = Object.create(person);
        
​
        console.log(stu1.name);
        console.log(stu1.obj);
        console.log(stu2.name);
        console.log(stu2.obj);

可以看到,我们用这种方式,以一个现有的对象为模板,直接就实现了继承,当然它的缺点也很明显,引用类型数据一旦被修改就影响到所有的实例,并且不能传递参数。

1669618051910.png

2.4.2寄生式继承

在上面原型式继承的基础上,加以改进,就可以实现寄生式继承,我们封装一个clone函数,利用函数可以在继承的时候对实例化的对象添加一些个性化的内容

    let person = {
        name:'我是父亲',
        age:'18',
        obj:{
            name:'obj的name'
        },
    }
​
    function clone(original){
        let clone = Object.create(original);
        clone.eating = function(){
            console.log('我在吃饭')
        }
        return clone;
    }
    let stu1 = clone(person);
    stu1.name = '我是stu1';
    stu1.obj.name = 'stu1修改了'
​
    let stu2 = clone(person);
    
​
    console.log(stu1.name);
    console.log(stu1.obj);
    stu1.eating();
    console.log(stu2.name);
    console.log(stu2.obj);

1669627712810.png

2.4.3寄生式组合继承

介绍以上两种方式,都是为了引出一个重要的继承方式,那就是寄生式组合继承,它融合了以上继承方式的优点,规避了它们的缺点,是一个较为成熟的继承实现方案。

废话不多说,具体做法如下:

       //父类
       function Person (name,age,obj){
            this.name = name;
            this.age = age;
            this.obj = obj
        }
        Person.prototype.eating = function(){
            console.log(this.name+'吃饭了');
        }
​
        //子类
        function Student(name,age,obj){
            Person.call(this,name,age,obj)
            this.sum = 1
        }
        
        //关键点
        Student.prototype = Object.create(Person.prototype);
        
        // 原型的实例等于自身
        Student.prototype.constructor = Student;
​
        //给子类添加方法
        Student.prototype.studying = function(){
            console.log(this.name + '正在学习');
        }
​
        let stu1 = new Student('大龙',16,{name:'大龙对象'});
        let stu2 = new Student('二龙',17,{name:'二龙对象'});
        let stu3 = new Student('三龙',18,{name:'三龙对象'});
​
        
        stu1.eating();
        stu1.studying();
        stu2.eating();
        stu2.studying();
        
        console.log(stu1);
        stu1.obj.name='stu1修改了obj的name'
        console.log(stu2);

打印结果,可以发现实现的效果很好,如下

1669452721819.png

我们可以把这个实现继承的部分过程封装成一个函数

//封装函数
        function clone(parent,child){
            child.prototype = Object.create(parent.prototype);
            child.prototype.constructor = child;
        }
​
       //父类
       function Person (name,age,obj){
            this.name = name;
            this.age = age;
            this.obj = obj
        }
        Person.prototype.eating = function(){
            console.log(this.name+'吃饭了');
        }
​
        //子类
        function Student(name,age,obj){
            Person.call(this,name,age,obj)
            this.sum = 1
        }
        
        //利用封装的函数实现一部分功能
        clone(Person,Student);
        
​
        //给子类添加方法
        Student.prototype.studying = function(){
            console.log(this.name + '正在学习');
        }

3.ES6实现继承

ES6引入了语法糖class,使得类的使用变得很方便,具体用法如下:

class User {
  //这里必须用constructor 
  constructor(name) {
    this.name = name;
  }
 //必须写成 方法名(){} 这种格式,函数之间不需要逗号
  sayHi() {
    alert(this.name);
  }
  
  dance(){
      console.log('跳舞');
  }
​
}
​
// 用法:
let user = new User("John");
user.sayHi();

使用class关键字就可以创建一个类,使用类实例化对象的时候依旧是使用new进行调用。

如何实现类的继承呢,这就需要结合使用extends、super来完成了,看代码:

//定义一个手机类
        class Phone {
​
            constructor(brand,price) {
                this.name = name;
                this.price= price; 
            }
​
            call() {
                console.log('打电话!');
            }
​
        }
        //定义子类,继承父类,要用extends 
        class SmartPhone extends Phone{
​
            constructor(brand,price,color,size){
                //使用super,将会执行父类constructor中的内容
                //也相当于 Phone.call(this,brand,price)
                super(brand,price);
                this.color = color;
                this.size = size;
            }
            //子类的方法直接写下面
            music(){
                console.log('播放音乐!');
            }
​
        }
​
        //实例化子类
        let xiaomi = new SmartPhone('小米',8888,'黑色','中号');
        console.log(xiaomi);
        xiaomi.call();
        xiaomi.music();

打印结果:

1669823809355.png

可以看到,实现了继承。