js继承的各种方式

js中es6之前没有类和继承,但是可以通过各种巧妙的方式来实现继承

继承应该达到的状态:
1.子类可以使用父类中的属性和方法
2.子类不同的实例之间不会互相影响
3.子类实例能够向父类传参
4.能实现多继承(一个子类可继承多个父类)
5.父类的方法能被复用(不会过多的消耗内存),而不是每创建一个子类实例都生成一份父类方法

一.原型链继承

1.具体实现:父类的实例作为子类的原型对象,核心实现代码标注如下:

         // 父类(我就不写成父构造函数了,这样比较简洁)
        function Father(name, age) {
            (this.name = name), (this.age = age), (this.arr = [1, 2, 3]), (this.value = 33)
        }
        Father.prototype = {
            say() {
                alert('hellow')
            },
        }
        // 子类
        function Child(name, age) {
            ;
            (this.name = name), (this.age = age)
        }

        Child.prototype = new Father() // 核心实现代码--->父类的实例作为子类的原型对象
        
        // 创建实例并让constructor重新指回Child
        let newChild = new Child('zkp')
        newChild.constructor =Child  
        let newChild2 = new Child('zhy')
        newChild.constructor =Child

        // 两个不同的子类实例上都有父类里的属性和方法
        console.log(newChild.arr) //  [1, 2, 3]
        console.log(newChild2.arr) //  [1, 2, 3]
        console.log(newChild.value) // 33
        console.log(newChild2.value) // 33

        // 在一个子类实例身上修改继承的父类的基础属性值,不会影响到其他子类实例
        newChild.value = 55
        console.log(newChild.value) // 55
        console.log(newChild2.value) // 33

        // 致命缺陷: 在一个子类实例身上修改继承父类的引用属性值,其他子类实例其他子类实例的值也会跟着改变
        newChild.arr.push(100)
        console.log(newChild.arr) // [1, 2, 3 , 100]
        console.log(newChild2.arr) // [1, 2, 3 , 100]
复制代码

2.优点:易于实现,一行代码就能实现

3.缺点:

1--->创建子类实例的时候不能向父类传参
2--->父类的引用数据类型属性被子类实例修改后,所有的子类实例上的该属性值也会跟着被修改

二.借用构造函数继承

1.具体实现:借父类的构造函数来增强子类实例(call,apply),相当于把父类的实例属性复制一份到子类实例

         // 父类
        function Father(name, age) {
            this.name = name,
            this.age =age,
            this.arr= [2, 3, 4, 5, 6]
        }
        Father.prototype = {
            say() {
                alert('hellow');
            },
        }
        // 子类
        function Child(name, age) {
            Father.call(this, name, age)  // 核心代码--->在子类中利用call()调用父类并向父类传参
        }

        let newChild = new Child('zkp', 11)  
        let newChild2 = new Child('zhy', 6)
        // 子类实例修改父类引用属性 其他子类实例上该属性不会改变
        newChild.arr.push(100)  
        console.log(newChild.arr); // [2, 3, 4, 5, 6, 100]
        console.log(newChild2.arr); // [2, 3, 4, 5, 6]

        // newChild.say() //  newChild.say is not a function  报错 只是继承了父类的构造函数 无法使用父类的原型方法
复制代码

2.优点:

1.解决了子类实例共享父类引用属性的问题
2.创建子类实例时,可以向父类构造函数传参
3.可以实现多继承(call多个)

3.缺点:无法继承父类原型中的方法,除非父类的方法写入构造函数中,但这样就实现不了函数的复用

三.组合式继承(伪经典继承)

1.具体实现:

        // 父类
        function Father(name, age) {
            this.name = 'hahaha',
            this.age = 11,
            this.arr = [2, 3, 4, 5, 6]
        }
        Father.prototype = {
            say() {
                alert('hellow');
            },
        }
        // 子类继承步骤:
        function Child(name, age) {
            Father.call(this, name, age) // 1.call方法调用父类的构造函数 并改变this的指向为子类函数(实现传参) 
        }
        // 2.子构造函数的原型对象指向父构造函数的实例 
        Child.prototype = new Father()
        
        // 3.创建实例并让constructor重新指回Child
        let newChild = new Child('zkp',12)
        newChild.constructor =Child  
        let newChild2 = new Child('zhy',6)
        newChild2.constructor =Child  
        
        newChild.say() // alert ('hellow') 页面弹出hellow  子函数的实例继承了父函数的原型中的方法
        console.log(newChild.name); // hahaha  子类的实例可以向父类传参
        
        // 一个子类实例修改引用类型属性 其他实例该引用类型不会改变
        newChild.arr.push(100)
        console.log(newChild.arr,newChild2.arr); // [2, 3, 4, 5, 6, 100] ,[2, 3, 4, 5, 6]
        // 缺点:父类的构造函数会被调用两次 call方法调用一次 new Father 一次
复制代码

2.核心

把实例方法都放在原型对象上,以实现函数复用。同时还要保留借用构造函数方式的优点,通过call(this)继承父类的基本属性和引用属性并保留能传参的优点.
通过Child.prototype = new Father()继承父类函数,实现函数复用

优点:

  1. 不存在引用属性共享问题(不同子类实例之间不会互相影响)
  2. 可传参
  3. 函数可复用

4.缺点:也有一点小缺点

子类原型上有一份多余的父类实例属性,因为父类构造函数被调用了两次,生成了两份,而子类实例上的那一份屏蔽了子类原型上的,造成内存浪费

四.原型式继承(对象中的继承,更像是拷贝而不是继承)

1.具体实现:

        // 创建对象obj
        let obj = {a:1 , b:2 , c:[1,2,3]}
        
        // 在函数中把一个对象作为一个空构造函数的原型对象 并返回该构造函数的调用
        function CreateObj(o) {
            function F() {}
            F.prototype = o;
            return new F();
        }
        let newObj = CreateObj(obj);
        let newObj2=CreateObj(obj)

        //新创建的对象拥有obj的属性和方法
        console.log(newObj.a , newObj.b , newObj.c); // 1 , 2 , [1, 2, 3]
        console.log(newObj2.a , newObj2.b , newObj2.c); // 1 , 2 , [1, 2, 3]

        // 通过一个实例修改引用类型值 其他实例也会被改变
        newObj.c.push(100)
        console.log(newObj2.a , newObj2.b , newObj2.c); // 1 , 2 ,  [1, 2, 3, 100]
        console.log(newObj.a , newObj.b , newObj.c); // 1 , 2 ,  [1, 2, 3, 100]
       
复制代码

3.核心:通过空的构造函数作为跳板 ,返回该构造函数的调用(类似于复制一个对象,用函数来包装)

3.优缺点

优点:

  1. 从已有对象衍生新对象,不需要创建自定义类型(一种新的创建对象方式) es5中内置方法Object.create()用到了这种方式

缺点:

  1. 父类引用属性会被所有实例共享,因为是用整个父类对象来充当了子类原型对象,所以这个缺陷无可避免
  2. 无法实现代码复用(新对象是现取的,每次new F()返回的都是一个新的对象,也没用到原型,无法复用)

五.寄生式继承

1.具体实现:

 let obj = {
                a: 1,
                b: [1, 2, 3],
                c() {
                    alert('hellow')
                },
            }

            function CreateObj(o) {
                function F() {}
                F.prototype = o
                return new F()
            }
            // 寄生继承就是在原型继承的基础上再封装 给队象增加方法和属性(对象增强) 实际上跟原型式继承是一样的
            function CreateChild(o) {
                let newObj = CreateObj(o) // 创建对象 或者用 var newObj = Object.create(o)
                // 增强对象
                newObj.x = function () {
                    alert('这是给新对象增强的方法')
                }),
                newObj.y = 'biubiu~'
                ......
                return newObj
            }

            let p = CreateChild(obj)
            p.x()  //弹出信息
            console.log(p.y)     // biubiu~
            console.log(p.a, p.b)    // 1 , [1 , 2 , 3]
复制代码

缺点:

函数还是不能复用,一个实例修改原型上的引用属性,其他实例依然会跟着改变(就是给原型式继承套上一层函数而已,让原型式看起开更像继承,并没有解决根本问题)

到这里的几个分析:

1.以上的方式多少都会有点缺陷,要达到完美继承就需要在组合式继承(伪经典继承)身上进行改造,但是要达到能传参,并且还要实现多继承的目的,那么在子类内部调用父类构造函数Father.call()这一步就不能动,

所以只能考虑Child.prototype = new Father 这一步,

2.最终需要达到目的:Child.Prototype.__ proto __ = Father.prototype(子类的原型指向父类的原型, 解决父类调用两次的缺陷) ----->(也就是一个对象继承另一个对象,上面的寄生式和原型式继承方式),
另外最后还是需要将子类实例的constructor指回子类

六.寄生式组合式继承(完美继承)

1.具体实现:

            //1. 父类 实例属性放在构造函数中
            function Father(name, age) {
                this.name = name
                this.age = age
                this.hobby = ['敲代码', '解Bug', '睡觉']
            }
            // 父类方法放在原型上实现复用
            Father.prototype.sayName = function () {
                console.log(this.name, this.age, 666)
            }

            //2. 子类
            function Child(name, age) {
                Father.call(this, name, age) // 调用父类的构造函数 (继承父类的属性)
                this.a = 1
            }

            // 3. 利用跳板创建对象
            function CreateObj(o) {
                function F() {}
                F.prototype = o
                return new F()
            }

            // 4.子类的原型对象用CreateObj(Father)创建
            Child.prototype = CreateObj(Father.prototype)
            console.log(Child.prototype.__proto__ === Father.prototype) // true 实现对Child.prototype = new Father的改造

            /* 或者直接把3,4写成es5中的Object.create() 一行代码实现
            Child.prototype = Object.create(Father.prototype) */

            // 5.创建子类实例 constructor属性指回子类
            let zkp = new Child('zkp', 12)
            zkp.constructor = Child
            let zhy = new Child('zhy', 6)
            zhy.constructor = Child
            
            // 验证:
            console.log(zkp.a , zhy.a) // 1 , 1  子类自己的属性
            // 一个子类实例修改继承的引用类型属性 其他实例不会被改变
            zkp.hobby.push('吃饭')
            console.log(zkp.name, zkp.age, zkp.hobby) // zkp , 12 , ['敲代码', '解Bug', '睡觉', '吃饭']
            console.log(zhy.name, zhy.age, zhy.hobby) // zhy , 6 , ['敲代码', '解Bug', '睡觉']
            // 子类调用父类的原型方法
            zkp.sayName() // zkp 666
            zhy.sayName() // zhy 666
复制代码

优点:

完美实现了函数复用,传参,实例之间不会相互影响,多继承

猜你喜欢

转载自juejin.im/post/7019185008527015949