继承、拷贝、闭包

119 阅读7分钟

一、认识继承

    //  构造函数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
   function Person(name){
    this.name = name
   }
   Person.prototype.init = function(){
    console.log('我是Person原型上的方法')
   }
  //  构造函数2
   function Stu(age){
    this.age = age
   }
   
   Stu.prototype = new Person('张三')
   Stu.prototype.sayHi = function(){
    console.log('你好')
   }
   console.log(Stu.prototype)

   const s1 = new Stu(18)
   console.log(s1)
   console.log(s1.name)
   s1.init()
   s1.sayHi()

访问对象s1name属性

  • 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.call(this, name) //将Person内部的this修改为了第一步被自动创建出来的对象,并传递一个参数name给Person函数使用
      // 3.自动返回这个对象
    }
    Stu.prototype.sayHi = function () {
      console.log('你好')
    }

    const s1 = new Stu(18, '张三')
    console.log(s1)
  //  s1.init() //没有继承到,所以无法使用

四、组合继承

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

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

  • 缺点:实例化对象上与原型对象上,都有父类的属性(多了一套属性,但是并不影响使用)

    //  构造函数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的原型对象', s1.__proto__)

    console.log(s1.name) //张三
    s1.init()

五、拷贝继承

  • 补充: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) {
        // console.log(key, p1[key])
        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 更改为 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)
    // console.log(obj)

    //深拷贝
    let obj = {
      name: '张三',
      age: 18,
      info: {
        width: 100,
        height: 280
      }
    }
    let newObj = {}

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

      // 1.通过 for...in 遍历对象,拿到对象的所有属性,拷贝到目标对象target中
      for (let key in origin) {
        // 2.根据遍历到的这个属性的属性值是什么类型的,决定执行什么代码
        if (Object.prototype.toString.call(origin[key]) === '[object Object]') {
          // 表明当前这个key的值为一个对象
          target[key] = {}
          // deepClone('目标对象', '原始对象')
          deepClone(target[key], origin[key])
        }
        else if
          (Object.prototype.toString.call(origin[key]) === '[object Array]') {
          // 表明当前这个key的值为一个数组
          target[key] = []
          deepClone(target[key], origin[key])
        }
        else {
          target[key] = origin[key]
        }
      }
    }
    // deepClone(newObj, obj)
    // newObj.age = 99
    // newObj.info.width = 999
    // console.log('newObj', newObj)
    // console.log('obj', obj)


    //针对工作
    newObj = JSON.parse(JSON.stringify(obj))
    deepClone(newObj, obj)
    newObj.age = 99
    newObj.info.width = 999
    console.log('newObj', newObj)
    console.log('obj', obj)

八、函数的定义与调用

1.定义:

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

2.调用:

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

九、永不销毁的执行空间

  • 1.正常书写一个函数
  • 2.在这个函数内向外返回一个引用数据类型
  • 3.当满足上述条件时,这个函数的执行空间将 不会被销毁
    function fn() {
      // var a = 100
      const obj = {
        name: 'fn函数的name',
        age: '不知道'
      }
      return obj
    }

    // 变量newObj内部保存着fn函数中声明的一个对象obj的地址,所以fn函数就不会被销毁,如果销毁了,那么对象也就无法访问了
    const newObj = fn()
    console.log(newObj)

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

闭包

  • 1.需要直接或者间接的返回一个函数

  • 2.内部函数需要访问外部函数的局部变量

  • 好处

延长变量的生命周期,在函数外可以使用函数内的变量

  • 弊端: 执行空间不会被销毁,如果大量使用会造成内存泄漏
    function outer() {
      let a = 100
      let obj = {
        name: 'outer函数',
        age: '随意'
      }


      function inner() {
        // console.log(a)
        // console.log(obj)
        // return a
        return obj
      }

      // 1.直接返回一个函数
      return inner
    }
    const newFn = outer()
    // console.log(newFn) //得到一个函数(inner)
    // newFn()
    let num = newFn()
    console.log(num)