Js中实现 继承 的几种方法详解

107 阅读3分钟

1. 原型继承

 利用自定义原形的方式实现继承关系

 核心: 将子类的原型修改为父类的实例化对象

 优点: 可以使用父类的属性和方法, 实现了继承

 缺点: 1.(Stu) 原本原型上的方法不能使用了  (因为 原型对象被改变了)
       2. 继承到的属性并不在自己身上, 而是在 (Stu)原型对象上  (不过不影响使用)
      
   <script>
        // 构造函数 Person
        function Person(name) {
            this.name = name
        }
        Person.prototype.init = function () {
            console.log('我是 Person 原型上的方法')
        }

        // 构造函数 Stu
        function Stu(age) {
            this.age = age
        }
        Stu.prototype.sayHi1 = function () { //sayHi1 是添加到(Stu)原本原型上的方法, 后续原型被更改后他就不能使用了
            console.log('你好1')
        }

        Stu.prototype = new Person('张三') //核心: 将子类的原型修改为父类的实例化对象
        Stu.prototype.sayHi2 = function () {
            console.log('你好2')
        }
        const s1 = new Stu(18)
        console.log(s1.name) //张三
        s1.init() //我是 Person 原型上的方法
        s1.sayHi2() //你好2  sayHi2 是添加到新的原型上的方法, 这个方法可以正常使用
        s1.sayHi1() //报错 uncaught TypeError: s1.sayHi1 is not a function 
                    //sayHi1 是添加到(Stu)原本原型上的方法, 后续原型被更改后他就不能使用了
    </script>
    
    
        /**
         *  访问 对象 s1 的 name 属性
         *      1. 先在对象本身查找, 找到就使用, 但是自身就只有一个 age 属性, 所以没找到
         * 
         *      2. 所以会去自己的 __proto__ 中查找, 也就是 自己构造函数的 原型对象
         *          
         *          2.1 但是现在 自己构造函数的原型对象 已经被我们修改为 Person 这个构造函数的实例化对象
         *          2.2 这个实例化对象上是具有 name 属性的, 并且还有一个 init 方法
         * 
         *      3. 所在 实例化对象中找到了 name 属性, 值为: 张三
         * 
        */

2. 借用构造函数继承

   借用构造函数继承
  
       核心: 把父类构造函数当作普通函数调用, 并利用 call 修改这个函数内部的 this 指向
               (如果不修改的话, 函数的 this 指向了其他的对象)
  
       优点: 把父类的属性全都继承在了自己身上
  
       缺点: 1. 只能继承父类的属性, 不能继承父类的方法
             2. 每次调用 Stu 的时候, Stu 内部还会自动调用一次 Person 函数
  
<script>
    // 构造函数1
    function Person(name) {
        this.name = name
    }
    Person.prototype.init = function () {
        console.log('我是 Person 原型上的方法')
    }

    // 构造函数2
    function Stu(age, name) {
        // 1. 自动创建出来一个对象  (这个函数内部的 this 就指向了 这个对象, 所以你可以通过 this 向着个对象上添加属性)

        // 2. 手动向对象上添加属性
        this.age = age
        // 将 Person 内部的 this 修改为了 第一步被自动创建出来的对象, 并传递一个参数 name 给 Person 函数使用
        Person.call(this, name) //核心: 把父类构造函数当作普通函数调用, 并利用 call 修改这个函数内部的 this 指向

        // 3. 自动返回这个对象
    }
    Stu.prototype.sayHi = function () {
        console.log('你好')
    }

    const s1 = new Stu(18, '张三')
    s1.sayHi()//你好
    s1.init()//Uncaught TypeError: s1.init is not a function  没有继承到, 所以无法使用
</script>


    /**
     *  分析: 
     *      35行 通过 new 关键字 调用 Stu 这个构造函数, 得到一个实例化对象, 存储在了 常量 s1 内部
     * 
     *      调用 Stu 函数时 发生的事情
     *          1. 给对象上天加一个 age 属性, 并将形参的值 赋值给它
     * 
     *          2. 调用 Person 并通过 call 方法 改变了 这个 Person 这个函数内部的 this  指向
     *              2.1 所以现在 Person 函数内部的 this 就相当于是 Stu 构造函数内部被自动创建出来的 对象
     * 
     *              调用 Person 函数是 发生的事情
     *                  this.name = 形参
     * 
     *          3. 代码执行完毕之后, new Stu 时内部被自动创建出来的对象被添加了两个属性
     *              3.1 是在 Stu 函数内部添加的 age 属性
     *              3.2 是在 Person 函数内部添加的 name 属性
    */

3.组合继承

     核心: 把原型继承与借用构造函数继承结合起来使用

     优点: 实例化对象上具有继承到的属性, 并且能够继承到父类原型上的方法

     缺点: 实例化对象上与原型对象上, 都有父类的属性  (多了一套属性, 但是并不影响使用)
     
<script>
    // 构造函数1
    function Person(name) {
        this.name = name
    }
    Person.prototype.init = function () {
        console.log('我是 Person 原型上的方法')
    }

    // 构造函数2
    function Stu(age, name) {
        this.age = age

        // 1. 借用构造函数继承, 得到 父类的属性 (放在了 对象上, 并且没有继承父类原型上的方法)
        Person.call(this, name)
    }
    // 2. 利用原型继承, 得到父类的属性(原型上)与方法
    Stu.prototype = new Person('这个字符串没有意义')

    Stu.prototype.sayHi = function () {
        console.log('你好')
    }

    // 创建实例化对象
    const s1 = new Stu(18, '张三')
    console.log(s1.name)//张三
    s1.init()//我是 Person 原型上的方法
    s1.sayHi()//你好
</script>

     /**
     *  分析:
     *  访问 s1 对象的 name 属性
     * 
     *      1. 去对象自身内部查找, 现在找到了, 直接使用, 并且停止查找
    */
    

4.拷贝继承

补充: for...in 遍历, 可以遍历到对象的原型上的方法

在 子类构造函数 实例化父类构造函数, 得到父类构造函数的实例化对象
然后利用 for...in 可以遍历到原型上的属性这个特点, 将实例化对象的属性与其原型上的方法
一起拷贝到子类构造函数的原型中

<script>
    // 构造函数1
    function Person(name) {
        this.name = name
    }
    Person.prototype.init = function () {
        console.log('我是 Person 原型上的方法')
    }

    // 构造函数2
    function Stu(age, name) {
        this.age = age

        const p1 = new Person('张三')
        
        //核心
        for (let key in p1) {
            Stu.prototype[key] = p1[key]
        }
    }
    Stu.prototype.sayHi = function () {
        console.log('你好')
    }

    // 创建实例化对象
    const s1 = new Stu(18, '张三')
    console.log(s1.name)//张三
    s1.init()//我是 Person 原型上的方法
    s1.sayHi()//你好
</script>

5.ES6 类的继承

    语法要求:
            1. 书写子类的时候: class 子类类名 extends 父类类名 {...}
            2. 书写 constructor 的时候: 内部需要书写 super('父类需要的参数')

    注意:
        1. extendssuper 必须同时出现才能完成继承
        2. super 必须出现在 constructor 的第一行

    额外扩展: ES6 类也能继承 ES5 的构造函数
    验证方法: 将 Person ES6 类的写法更改为 ES5 的构造函数写法即可
    
    <script>
        // 父类
        class Person {
            constructor(name) {
                this.name = name
            }
            init() {
                console.log('我是 Person 原型上的方法')
            }
        }

        // 子类
        class Stu extends Person {
            constructor(age) {
                super('父类name')
                this.age = age
            }
            sayHi() {
                console.log('你好~~~~')
            }
        }

        const s1 = new Stu(18)
        console.log(s1.name)//父类name
        s1.init()//我是 Person 原型上的方法
        s1.sayHi()//你好~~~~

    </script>