继承

123 阅读8分钟

一. 认识继承

  • 如果 实例化对象(s1) 内部 具有一个 name 属性
  • 并且 s1 还可以使用 init 方法
    • 那么 我们就可以说 Stu 这个构造函数 继承了 Person
    • Stu 是 Person 的子类
    • Person 是 Stu 的父类
        // 构造函数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__)

二. 原型继承

什么是原型继承 ?

  • 利用自定义原型的继承方式, 实现继承
    • 子类,prototype = new 父类()

缺点 :

  1. 原本原型上的方法不能使用, (因为原型被改变了)
  2. 继承到的属性并不是在自己身上, 而是在原型上, (不过并不影响使用)
        // 构造函数 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('你好');
        }

        Stu.prototype = {
            a: 1,
            b: 2
        } // 修改对象的原型对象

        Stu.prototype = new Person('张三')  // '张三'
        console.log('stu原型对象',Stu.prototype);  // {a:1,b:2}

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

构造函数继承

核心 :

  • 把 父类函数 当做 子类函数调用时, 并利用 this 修改了这个函数内部的 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) {
            // 1. 自动创建出来一个对象 (这个函数内部的this 就指向了这个对象, 所以你可以通过 this 向这个对象上添加属性)

            // 2. 手动向对象上添加属性
            this.age = age
            // 将 Person 内部的this 修改为了 第一步被自动创建出来的对象,并传递一个参数 name 给 Person 函数使用
            Person.call(this,'这是一个名字')
            // 3.自动返回这个对象
        }

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

        const s1 = new Stu(18,'张三')
        console.log(s1);
        s1.init()   // 没有继承到, 无法使用(构造函数继承 继承不到方法)


        /*
           分析 :  通过new 关键字 调用 Stu 这个构造函数, 得到一个实例化对象,存储在了 常量 s1 内部

              调用 Stu 函数时, 发生的 事情 :
                 1. 给对象上添加一个 age 属性, 并将形参的值 赋值给他
                 2. 调用 Person 并通过 call 方法 改变了 this 就相当于Stu 构造函数内部 被自动创建出来的 对象
                    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) {
            this.age = age
            // 1. 借用构造函数继承, 得到弗雷德属性(放在了对象上, 并且没有继承类型院系能上的方法)
            Person.call(this, name)
        }
        // 利用原型继承, 得到父类的属性(原型上)与方法
        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 属性:
                 去对象内部自身查找, 找到了, 直接使用, 并且停止查找
        */

拷贝继承

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

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

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

            const p1 = new Person('张三')
            for (let key in p1) {
                Stu.prototype[key] = p1[key]
            }
        }
        Stu.prototype.sayhi = function () {
            console.log('我是子类方法');
        }

        // 3. 创建实例化对象
        const s1 = new Stu(22, '传名字')
        console.log('Stu实例化对象', s1);
        console.log('Stu原型对象', s1.__proto__);

ES6类继承

语法 :

   class 子类名 extends 父类名{
       constructor(age){
       super('父类需要的参数, 都写在这里')
       his.age = age
        }
   }

语法要求 :

  1. 书写 子类时, class子类名 extends 父类名{...}
  2. 书写 consturctor 时, 内部需要 书写 super('父类需要的参数')

注意 :

  1. extends 和 super 必须同时出现 才能完成继承
  2. super 必须出现在 consturctor 内第一行
  • 额外扩展 : ES6 类也能继承 ES5 的构造函数
    • 验证方法 : 将 Person 更改为 ES5 的构造函数
        // 父类
        class Person {
            constructor(name) {
                this.name = name
            }
            init() {
                console.log('我是父类方法');
            }
        }

        // 子类
        class Stu extends Person {
            constructor(age) {
                super('父类需要的参数, 都写在这里')
                this.age = age
            }
            sayhi() {
                console.log('我是子类方法');
            }

        }

        const s1 = new Stu(18)
        console.log(s1);  // age : 18 name : '父类需要的参数, 都写在这里'
        console.log(s1.name);  // '父类需要的参数, 都写在这里'
        s1.init()  // '我是父类方法'
        s1.sayhi() // '我是子类方法'

深浅拷贝

含义 :

  • 将一个 引用数据类型, 拷贝到另外一个变量中, 但是根据拷贝的方法不同, 展示的效果也有差异

浅拷贝 :

  • 将一份数据拷贝到另外一个变量中, 修改第一层数据时不会被影响, 但是修改第二层数据时会被影响
        // 浅拷贝 :
        let obj = {
            name: '张三',
            age: 18,
            init: {
                width: 100,
                height: 200
            }
        }
        let newobj = {}
        for(let key in obj){
            // console.log(key);  // name  age  init
            newobj[key] = obj[key]
        }
        newobj.age = 99
        newobj.init.width = 999
        console.log('newobj' ,newobj); // age : 99 init : width :999  
        console.log('obj', obj);  // age : 18  init : width :999  

深拷贝 :

  • 将一份数据拷贝到另外一个变量中, 不管修改哪一层数据, 两个对象之间都不会互相影响
        // 深拷贝 : 
        let obj = {
            name: '张三',
            age: 18,
            init: {
                width: 100,
                height: 200
            }
        }
        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('目标对象','原始对象')
                    deepClone(target[key], orinig[key])
                } else if (Object.prototype.toString.call(origin[key]) === 'object Array') {
                    // 表明当前这个 key 的值为一个数组
                    target[key] = []
                    deepClone(target[key], orinig[key])
                } else {
                    // 表明当前这个key 的值 一定不是对象 或者 数组
                    target[key] = origin[key]
                }
            }
        }
        deepClone(newobj, obj)
        console.log(deepClone);

        newobj.age = 99
        newobj.init.width = 999
        console.log('newobj',newobj);
        console.log('obj',obj);

        // 针对工作 :
        newobj = JSON.parse(JSON.stringify(obj))   // 把obj装成json 格式 字符串

赋值 :

        赋值
           let obj = {
            name : '张三',
            age : 18,
            init : {
                width : 100,
                height : 200
            }
           }
           let obj2 = obj
           obj2.age = 20
           console.log(obj);  // 20  因为直接赋值,对象存的是地址 , 所以修改obj2 会影响 obj, 所以它内部的 name 值也就改成了 20 

函数的定义与调用

定义 :

  • 在堆内存中 开启一个空间
  • 将函数的函数体保存到内存中
  • 将堆内存的地址保存在变量名(函数名), 最后将这个变量名存储在 栈内存中

调用 :

  • 根据变量名(函数名) 中的地址, 找到对应的函数
  • 然后再调用 栈中 开一个新的空间(函数的执行空间)
  • 在执行空间中, 对函数形参进行赋值
  • 在执行空间中, 进行变量得预解析
  • 在执行空间中, 执行函数的代码, 销毁当前函数的执行空间

永不销毁的执行空间

  • 正常写一个函数
  • 在这个函数内, 向外否会一个 引用数据类型( 复杂数据类型)
  • 当满足上述条件时, 这个函数的执行空间将不会被销毁
        function fn() {
            const obj = {
                name: 'fn函数的name',
                age: '不知道'
            }
            return obj
        }
        // 变量名 newobj 内部保存着fn 函数中声明的一个对象 obj 地址,所以fn 函数就不会被销毁掉, 如果销毁了, 那么对象也无法访问了
        const newobj = fn()
        // newobj.name = '张三'  // 张三
        console.log(newobj);

        // 将 newobj 的值修改后就与函数内部的 对象切断了练习, 那么这个函数的执行空间就会被销毁
        // newobj = null

闭包

什么是闭包 ?

  1. 需要直接 或者 间接 返回一个函数
  2. 内部函数需要访问外部函数的局部变量

好处 :

  • 延长变量得生命周期

弊端 :

执行空间不会被销毁, 如果大量使用 会造成 内存泄漏

        function outter() {
            let a = 100
            let obj = {
                name: 'outter函数',
                age: '99'
            }
            function inner() {
                // console.log(a);  // 100
                // console.log(obj);
                return a
            }
            //1. 直接返回一个函数
            return inner
        }
        const newfn = outter()

        // console.log(newfn);  // 得到一个函数(inner)
        //newfn()  // 没有输出 控制台什么也没有
        console.log(newfn());
        // let num = newfn()
        // console.log(num);