继承与深浅拷贝

69 阅读8分钟

认识继承

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

    // 构造函数2
    function Stu(age) {
        this.age = age
    }
    Stu.prototype.sayHi = function () {
        console.log('你好')
    }

    const s1 = new Stu(18)
    console.log(s1)
    console.log(s1.__proto__)
    
    如果 s1 对象内部 具有一个属性为 name
    并且 s1 还可以使用 init 方法
    
    那么 我们就可以说 Stu 这个构造函数 继承自 Person 构造函数
    StuPerson 的子类
    PersonStu 的父类
  

原型继承

    利用自定义原形的方式实现继承关系
    核心: 将子类的原型修改为父类的实例化对象
    优点: 可以使用父类的属性和方法, 实现了继承
    缺点: 1. 原本原型上的方法不能使用了  (因为 原型对象被改变了)
         2. 继承到的属性并不在自己身上, 而是在 原型对象上  (不过不影响使用)
         
    缺点1解释:
            sayHi1 是添加到原本原型上的方法, 后续原型被更改后他就不能使用了
            sayHi2 是添加到新的原型上的方法, 这个方法可以正常使用
            
    // 构造函数1
    function Person(name) {
        this.name = name
    }
    Person.prototype.init = function () {
        console.log('我是 Person 原型上的方法')
    }

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

    // Stu.prototype = {
    //     a: 1,
    //     b: 2
    // }
    
        Stu.prototype.sayHi1 = function () {
        console.log('你好')
    }
    Stu.prototype = new Person('张三')
    Stu.prototype.sayHi2 = function () {
        console.log('你好')
    }

    console.log('Stu 原型对象: ', Stu.prototype)

    const s1 = new Stu(18)
    console.log('Stu 实例对象: ', s1)
    // console.log(s1.name)
    // s1.init()

    // s1.sayHi()
    
    访问 对象 s1 的 name 属性
    
    1. 先在对象本身查找, 找到就使用, 但是自身就只有一个 age 属性, 所以没找到
    
    2. 所以会去自己的 __proto__ 中查找, 也就是 自己构造函数的 原型对象
    
        2.1 但是现在 自己构造函数的原型对象 已经被我们修改为 Person 这个构造函数的实例化对象
        2.2 这个实例化对象上是具有 name 属性的, 并且还有一个 init 方法
        
        3. 所在 实例化对象中找到了 name 属性, 值为: 张三
        

借用构造函数继承

    核心: 把父类构造函数当作普通函数调用, 并利用 call 修改这个函数内部的 this 指向
            (如果不修改的话, 函数的 this 指向了其他的对象)
            
    优点: 把父类的属性全都继承在了自己身上
    
    
    缺点: 1. 只能继承父类的属性, 不能继承父类的方法
    
         2. 每次调用 Stu 的时候, Stu 内部还会自动调用一次 Person 函数
         
        // 构造函数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)

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

    const s1 = new Stu(18, '张三')
    console.log(s1)
    // s1.init()   // 没有继承到, 所以无法使用
    
    
    分析: 
     *      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 属性
    */
    
    

组合继承

    核心: 把原型继承与借用构造函数继承结合起来使用
    
    优点: 实例化对象上具有继承到的属性, 并且能够继承到父类原型上的方法
    
    缺点: 实例化对象上与原型对象上, 都有父类的属性  (多了一套属性, 但是并不影响使用)
    
    
     // 构造函数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('Stu 实例化对象', s1)
    // console.log('Stu 原型对象', Stu.prototype)

    // console.log(s1.name)

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

拷贝继承

    补充: for...in 遍历, 可以遍历到对象的原型上的方法
    
    
    // 构造函数1
    function Person(name) {
        this.name = name
    }
    Person.prototype.init = function () {
        console.log('我是 Person 原型上的方法')
    }

    // const p1 = new Person('张三')
    // for (let key in p1) {
    //     console.log(key, p1[key])
    // }

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

        /**
         *  在 子类构造函数实例化父类构造函数, 得到父类构造函数的实例化对象
         *      然后利用 for...in 可以遍历到原型上的属性这个特点, 将实例化对象的属
                性与其原型上的方法
         *      一起拷贝到子类构造函数的原型中
        */
        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('Stu 实例化对象', s1)
    console.log('Stu 原型对象', s1.__proto__)
    

ES6 类的继承

    语法要求:
        1. 书写子类的时候: class 子类类名 extends 父类类名 {...}
        2. 书写 constructor 的时候: 内部需要书写 super('父类需要的参数')
        
    注意:
        1. extendssuper 必须同时出现才能完成继承
        2. super 必须出现在 constructor 的第一行
        
     额外扩展: ES6 类也能继承 ES5 的构造函数
     验证方法: 将 Person ES6 类的写法更改为 ES5 的构造函数写法即可
     
     
     // 父类
    class Person {
        constructor(name) {
            this.name = name
        }
        init() {
            console.log('我是 Person 原型上的方法')
        }
    }

    // 子类
    class Stu extends Person {
        constructor(age) {
            super('父类需要的参数, 都写在这里边')

            this.age = age
        }
        sayHi() {
            console.log('你好~~~~')
        }
    }

    const s1 = new Stu(18)

    console.log(s1)
    console.log(s1.name)
    s1.init()

深浅拷贝

    含义:
        通常是指将一个引用数据类型, 拷贝到另外一个变量中, 但是根据拷贝的方法不同
        展示出的效果也有差异
        
        浅拷贝: 将一份数据拷贝到另外一个变量中, 修改第一层数据是不会互相影响, 但是修改
        第二层数据时会互相影响
        
        深拷贝: 将一份数据拷贝到另外一个变量中, 不管修改那一层数据, 两个对象之间都不会
        互相影响
        
        // 赋值
    // let obj = {
    //     name: '张三',
    //     age: 18,
    //     info: {
    //         width: 100,
    //         height: 280
    //     }
    // }
    // let obj2 = obj
    // obj2.age = 20
    // console.log(obj)    // 因为是直接赋值, 所以修改 obj2 会影响 obj, 所以它内
    部的 name 的值也被更改了 20

    // 浅拷贝
    // let obj = {
    //     name: '张三',
    //     age: 18,
    //     info: {
    //         width: 100,
    //         height: 280
    //     }
    // }
    // let newObj = {}
    // for (let key in obj) {
    //     newObj[key] = obj[key]
    // }
    // newObj.age = 99
    // newObj.info.width = 999
    // console.log('newObj', newObj)
    // console.log('obj', obj)
    
    
    // 深拷贝
    let obj = {
        name: '张三',
        age: 18,
        info: {
            width: 100,
            height: 280
        }
    }
    let newObj = {}

    // 针对面试
    function deepClone(target, origin) {
        /**
         *  target: 目标对象
         *  origin: 原始对象
         * 
         *      需求: 将原始对象 origin 内部的所有属性, 拷贝到目标对象 target 中
        */

        // 1. 通过 for...in 遍历对象, 拿到对象的所有的 key 与 对应的 value
        for (let key in origin) {
            // 2. 根据 遍历到的这个属性的属性值是什么类型的, 决定执行什么代码
            if (Object.prototype.toString.call(origin[key]) === '[object Object]') {


                // 表明当前这个 key 的值为一个对象
                target[key] = {}
                deepClone(target[key], origin[key])


            } else if (Object.prototype.toString.call(origin[key]) === '[object Array]') {
                // 表明当前这个 key 的值为一个数组
                target[key] = []
                deepClone(target[key], origin[key])
            } else {
                // 表明当前这个 key 的值 一定不是对象或者数组
                target[key] = origin[key]
            }
        }
    }
    
    
       /**
     *  分析:
     *      调用 deepClone, 第一个参数是一个空对象
     *          第二个参数内部有三个属性, 分别为 name-age-info, 其中 info 是一个对象
     *
     *      函数在执行的时候, 首先会执行一个 for...in 循环遍历 第二个参数
     *
     *          for..in 循环第一次执行  key === 'name'  origin[key] === '张三'
     *              开始运行 循环内部的 分支语句, 因为当前的 value 类型为 string, 所以会走最后一个 else 分支
     *              那么就是:   target[key] = origin[key]       target.name = '张三'
     *
     *          for...in 循环第二次执行 key === 'age'   origin[key] === 18
     *              开始运行 循环内不的 分支语句, 因为当前的 value 类型为 number, 所以会走最后一个 else 分支
     *              那么就是: target[key] = origin[key]         target.age = 18
     *
     *          两轮循环结束
     *              此时 origin 这个对象 {name: '张三', age: 18}
     *
     *          for...in 循环第三次执行 key === 'info'  origin[key] === {width: 100, height: 280}
     *              开始运行 循环内不的 分支语句, 因为当前的 value 类型为 Object, 所以会走第一个 分支
     *              分支内部的代码
     *                  1. target[key] = {}        {name: '张三', age: 18, info: {}}
     *
     *                  2. deepClone(target[key], origin[key])
     *                          调用 deepClone 第一个参数是一个 空对象
     *                          第二个参数内部有两个属性, 分别为 width, height
     *
     *                          函数在执行的时候, 首先会执行一个 for...in 循环遍历第二个参数
     *                              for...in 循环第一次执行 key === 'width' origin[key] === 100
     *                              开始运行 循环内部的 分支语句, 因为当前的 value 类型为 number, 所以会走最后一个 else 分支
     *                              那么就是: target[key] = origin[key]         target.width = 100
     *
     *                              for...in 循环第二次执行 key === 'height' origin[key] === 280
     *                              开始运行 循环内部的 分支语句, 因为当前的 value 类型为 number, 所以会走最后一个 else 分支
     *                              那么就是: target[key] = origin[key]         target.height = 280
     *                  这两轮 for 执行完毕后 target.info === {width: 100, height: 280}
    */
    // deepClone(newObj, obj)

    // // newObj.age = 99
    // // newObj.info.width = 999
    // console.log('newObj', newObj)
    // console.log('obj', obj)

    // 针对工作
    newObj = JSON.parse(JSON.stringify(obj))

    newObj.age = 99
    newObj.info.width = 999
    console.log('newObj', newObj)
    console.log('obj', obj)