JS 关于继承和深浅拷贝

80 阅读7分钟

1.认识继承

    //构造函数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 构造函数
     *        Stu 是 Person 的子类
     *        Person 是 Stu 的父类
    */

原型继承

  • 利用自定义原型的方式实现继承关系
  • 核心: 将子类的原型修改为父类的实例化对象
  • 优点: 可以使用父类的属性和方法,实现了继承
  • 缺点:
    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 = new Person('张三')
    
    console.log(Stu.prototype)
    
    const s1 = new Stu(18)
    console.log(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() // 没有继承到,所以无法使用
    
    /**
     * 分析:
     *    通过 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)
    console.log(s1.init)
    /**
     * 访问 s1 对象的 name 属性
     * 
     *    1. 去对象自身内部查找,现在找到了,直接使用.并且停止查找
    */

拷贝继承

  • for...in 遍历,可以遍历到对象的原型上的方法
    //构造函数1
    function Person(name) {
      this.name = name
    }
    Person.prototype.init = function () {
      console.log('我是 Person 原型上的方法')
    }
    
    
    //构造函数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. extends 和 super 必须同时出现才能完成继承
    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()
        super('父类需要的参数,都写在这里面')
        this.age = age

      }
      sayHi(){
        console.log('你好')
      }
    }
    
    const s1 = new Stu(18)
    console.log(s1)
    console.log(s1.name) // 父类需要的参数,都写在这里面
    s1.init() // 我是 Person 原型上的方法
    

深浅拷贝

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

浅拷贝

  • 将一份数据拷贝到另外一个变量中, 修改第一层数据时不会互相影响, 但是修改第二层数据时会互相影响
    let obj = {
      name:'张三',
      age: 18,
      info: {
        width: 100,
        height: 200
      }
    }
    let newObj = {}

    for (let key in obj){
      newObj[key] = obj[key]
    }
    newObj.age = 99 // 只有newObj被修改了,obj里面age不会被修改
    newObj.info.width = 999 // obj和newObj里面都被修改了
    console.log('obj',obj)
    console.log('newObj',newObj)

深拷贝:

  • 将一份数据拷贝到另外一个变量中, 不管修改哪一层数据, 两个对象之间都不会互相影响
  • 复杂方法
    let obj = {
      name:'张三',
      age: 18,
      info: {
        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],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(newObj,obj)
    newObj.info.width = 999
    console.log('newObj', newObj)
    console.log('obj', obj)
  • 简单方法
    newObj = JSON.parse(JSON.stringify(obj))
    newObj.age = 99 // 只有newObj被修改了,obj里面age不会被修改
    newObj.info.width = 999 // obj和newObj里面都被修改了
    console.log('obj',obj)
    console.log('newObj',newObj)

    //_.deepClone('目标对象','原始对象')