JavaScript 深浅拷贝的原理和实现

简介

为了巩固一下身为前端开发小菜鸟必备的脸面,从而让脸皮更加厚实一些,在这条道路活的更加滋润一些,故此开始整理一些JavaScript(脸面)使用的一些基础细节。
欢迎各位大牛莅临指导,从而让我在加厚脸皮的道路上走的更远,从而可以用脸皮挡子弹,挡导弹,挡核弹,然后无敌。哈哈哈!
今天探究一下深浅拷贝的原理和实现。

浅拷贝的原理和实现

  • 浅拷贝的原理

    通过创建一个新的对象,来接受需要复制或者引用的对象值。如果对象属性是基本数据类型,则直接复制基本类型的值给新对象;如果对象属性是引用数据类型,则复制的是内存中的地址给新对象,这时复制的地址指向的对象是具有共享性的,当其他地方修改该地址指向的对象后,新创建的对象中复制的地址所指向的对象也是会同步变化的。

  • 浅拷贝的实现

    • Object.create()

      语法: Object.create(proto) // proto: 新创建对象的原型对象。
      Object.create(proto, propertiesObject) // propertiesObject:如果该参数被指定且不为 undefined,则该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。

      let a = {
          name: 'niu',
          age: 18,
          sex: 'gril',
          sym: Symbol(1),
          list: [
              1, 2, 3, 4
          ],
          obj: {
              car: 'BMW',
              hourse: 'large hourse'
          },
          say() {
              console.log('I am saying')
          }
       }
       a.pointer = a;
       Object.defineProperty(a, 'test', {
          value: 'testValue',
          enumerable: false
       })
       let b = Object.create(a)
       a.name = 'jiao'
       a.obj.car = 'bieke'
       console.log(a)
       console.log(b)
      

      在这里插入图片描述
      通过上述代码可以得出,使用Object.create方法可以:

      • 源对象作为目标对象的原型对象,可以通过原型链访问到源对象的属性和方法;
      • 当源对象改变自身的属性或者方法的时候,两者都是同步更改的,从而验证了源对象仅仅只是目标对象的原型对象,它们之间仅仅只是引用的关系。
    • Object.assign()

      语法:Object.assign(target, …sources),该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(可以是多个来源)。

      let a = {
          name: 'niu',
          age: 18,
          sex: 'gril',
          sym: Symbol(1),
          list: [
              1, 2, 3, 4
          ],
          obj: {
              car: 'BMW',
              hourse: 'large hourse'
          },
          say() {
              console.log('I am saying');
          }
       }
       let b = {
          eat() {
              console.log('I am eating')
          },
          age: 20
       }
       a.pointer = a;
       Object.defineProperty(b, 'test', {
          value: 'testValue',
          enumerable: false
       })
       let c = Object.assign({}, a, b); 
       a.name = 'jiao'
       a.list.push(5)
       console.log(a) 
       console.log(b)   
       console.log(c)   
      

      在这里插入图片描述

      从上面的代码中可以看到,当修改了对象a的name属性值为’jiao‘时,并不影响对象c的name属性,然而当在对象a的list属性中push了一个新的元素后,对象c的list属性也发生了改变。对象c将对象a的sym属性成功的进行了拷贝,而对于对象b的test属性无法拷贝,从而证实了Object.assign方法实现了我们想要的结果。同时使用该方法的时候需要注意以下几点:

      • 它不会拷贝对象的继承属性;
      • 它不会拷贝对象的不可枚举属性;
      • 他可以拷贝对象的Symbol属性。
    • 扩展运算符方式

      使用js的扩展运算符,在构造对象的同时完成浅拷贝的功能。
      语法:let cloneObj = {…obj1, …, …objX};
      let cloneArr = […arr1, …, …arrX]。

       let a = {
          name: 'niu',
          age: 18,
          sex: 'gril',
          sym: Symbol(1),
          list: [
              1, 2, 3, 4
          ],
          obj: {
              car: 'BMW',
              hourse: 'large hourse'
          },
          say() {
              console.log('I am saying');
          }
       }
       let b = {
          eat() {
              console.log('I am eating')
          },
          age: 20
       }
       Object.defineProperty(b, 'test', {
          value: 'testValue',
          enumerable: false
       })
       let c = {...a, ...b};
       console.log(b)
       console.log(c)
       a.name = 'jiao'
       a.list.push(5)  
      
      let d = [1, 2, 3, {
          name: 'niu'
       }, 4]
      let e = [4, 5, 6, 7, 8, 9, {
          sex: 'boy'
      }]
      let f = [...d, ...e];
      f[0] = 10;
      d[3].name = 'jiao';
      e[6].sex = 'girl'
      console.log(d)
      console.log(e)
      console.log(f)
      

      在这里插入图片描述

      主要不同的地方在于使用语法的不同,其他地方同Object.assign方法类似,都具有相同的缺陷。

    • slice方法拷贝数组

      slice方法具有局限性,它仅仅只针对数组类型,返回一个新的数组对象,不会改变原数组对象。
      语法:arr.slice(begin, end)。

       let a = [1, 2, 3, {
          name: 'niu'
       }, 4]
       let b = a.slice();
       b[0] = 10;
       a[3].name = 'jiao';
       console.log(a)
       console.log(b)
      

      输出结果
      通过对比发现使用slice方法,对数组中的基本数据类型进行了赋值操作,对数组中的引用数据类型进行了赋址操作,符合浅拷贝的要求。同时也暴露出浅拷贝的限制-它只能拷贝一层对象,如果存在对象的多层嵌套,那么浅拷贝就无能无力了。

    • concat方法拷贝数组

      concat方法同slice方法一样,具有局限性,它也是仅仅只针对数组类型,返回一个新的数组对象,不会改变原数组对象。
      语法: let newArr = arr.concat(arr1, arr2,…,arrX)

       let a = [1, 2, 3, {
          name: 'niu'
       }, 4]
       let b = [4, 5, 6, 7, 8, 9, {
          sex: 'boy'
       }]
      let c = [].concat(a, b)
      c[0] = 10;
      a[3].name = 'jiao';
      b[6].sex = 'girl'
      console.log(a)
      console.log(b)
      console.log(c)
      

      在这里插入图片描述

    • 原生实现一个浅拷贝

      • 对基本数据类型做一个赋值操作;
      • 对引用数据类型开辟一个新的存储,并且拷贝一层对象属性。
      // 浅拷贝
      function shallowClone(obj) {
          if (typeof obj === 'object' && obj !== null) {
              let properties = Object.getOwnPropertyNames(obj)
              let target = Array.isArray(obj) ? [] : {}
              for (let i = 0; i < properties.length; i++) {
                  target[properties[i]] = obj[properties[i]];
              }
              return target
          } else {
              return obj
          }
      }
      
      let a = {
          name: 'niu',
          sex: 'boy',
          age: 18,
      }
      Object.defineProperty(a, 'weight', {
          value: 60,
          enumerable: false
      })
      let b = [1,2,3,4,5,6]
      console.log(shallowClone(a)) // {age: 18, name: 'niu', sex: 'boy', weight: 60}
      console.log(shallowClone(b)) // [1,2,3,4,5,6]
      

深拷贝的原理和实现

  • 深拷贝的原理

    将一个对象从内存中完整的拷贝出来一份给目标对象,并在堆内存中开辟出一个全新的空间去存储新的对象。新对象的修改不会影响原对象,两者实现真正的分离。

  • 深拷贝的实现

    • JSON.stringify方法(简略版)

      将一个对象序列化为JSON字符串,并将对象里面的内容转变为字符串;最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象。

      class Animal {
          constructor() {
              this.name = 'name'
          }
          listen() {
              console.log('I am listening')
          }
      }
      let a = {
          name: 'niu',
          sex: 'boy',
          age: 18,
          say() {},
          u: undefined,
          sym: Symbol(),
          date: new Date(),
          reg: new RegExp(/\d/),
          num: NaN,
          num1: Infinity,
          num2: -Infinity     
      }
      Object.defineProperty(a, 'weight', {
          value: 60,
          enumerable: false
      })
      a.__proto__ = new Animal();
      
      function deepClone(target) {
          return JSON.parse(JSON.stringify(target))
      }
      let b = deepClone(a);
      a.name = 'jiao';
      console.log(a);
      console.log(b);
      

      在这里插入图片描述

      • 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;

      • 拷贝 Date 引用类型会变成字符串;

      • 无法拷贝不可枚举的属性;

      • 无法拷贝对象的原型链;

      • 拷贝 RegExp 引用类型会变成空对象;

      • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;

      • 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。

    • 手写递归实现(基础版)

      
      // 递归实现深拷贝
      function deepClone(source) {
          // 基本数据类型
          if (typeof source !== 'object' || !source) {
              return source
          }
          // 数组 || 对象
          let target = Array.isArray(source) ? [] : {} 
          for (let i in source) {
              target[i] = deepClone(source[i]);
          }
          return target
       }
      
      class Animal {
          constructor() {
              this.name = 'name'
          }
          listen() {
              console.log('I am listening')
          }
      }
      
      let a = {
          name: 'niu',
          sex: 'boy',
          age: 18,
          say() {
              console.log('I am saying')
          },
          u: undefined,
          sym: Symbol(1),
          arr: [1,2,3,4,5],
          arr1: new Array(5).fill(1),
          date: new Date(),
          err: new Error(),
          reg: new RegExp(/\d/),
          num: NaN,
          num1: Infinity,
          num2: -Infinity     
      }
      Object.defineProperty(a, 'weight', {
          value: 60,
          enumerable: false
      })
      a.__proto__ = new Animal();
      
      const b = deepClone(a);
      console.log(a)
      console.log(b)
      

      在这里插入图片描述

      通过上图可以得出,基础版并不能兼顾所有的情况,它存在下述的问题:

      • 它只是针对普通的引用数据类型做递归复制,而对于Date、RegExp、Error、FormData 这样的引用类型并不能正确地拷贝;
      • 无法复制不可枚举的属性;
      • 无法拷贝源对象的原型链;
      • 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
    • 手写递归实现(改进版)

      // 递归实现深拷贝
      function deepClone(source, hash = new WeakMap()) {
          let _toString = Object.prototype.toString
          // 基本数据类型
          if (typeof source !== 'object' || !source) {
              return source
          }
          // DOM Node
          if (source.nodeType && 'cloneNode' in source) {
              return source.cloneNode(true)
          }
          // Date
          if (_toString.call(source) === '[object Date]') {
              return new Date(source.getTime())
          }
          // RegExp
          if (_toString.call(source) === '[object RegExp]') {
              const flags = [];
              if (source.global) flags.push('g')
              if (source.multiline) flags.push('m')
              if (source.ignoreCase) flags.push('i')
              return new RegExp(source.source, flags.join(''))
          }
          // Error
          if (_toString.call(source) === '[object Error]') {
              return new Error(source.message)
          }
          // ... Map对象 Set对象等都需要重新生成一个新的实例返回
      
          // 循环引用通过weakMap解决
          if (hash.has(source)) {
              return hash.get(source)
          }
          // 获取source对象的所有自身属性的描述符
          let own = Object.getOwnPropertyDescriptors(source)
          // 继承原型链
          let target = Object.create(Object.getPrototypeOf(source), own)
          hash.set(source, target);
          for (let key of Reflect.ownKeys(source)) {
              target[key] = deepClone(source[key], hash)
          }
          return target
       }
      
      class Animal {
          constructor() {
              this.name = 'name'
          }
          listen() {
              console.log('I am listening')
          }
      }
      let a = {
          name: 'niu',
          sex: 'boy',
          age: 18,
          say() {
              console.log('I am saying')
          },
          u: undefined,
          sym: Symbol(1),
          arr: [1,2,3,4,5],
          arr1: new Array(5).fill(1),
          date: new Date(),
          err: new Error('错误了'),
          reg: new RegExp(/\d/),
          num: NaN,
          num1: Infinity,
          num2: -Infinity,
          pointer: {
              name: 'jiao',
              sex: 'girl',
              list: [1,2,3,4,5, {a: 6}],
              obj: {
                  b: 'b',
                  c: 'c'
              }
          }     
      }
      Object.defineProperty(a, 'weight', {
          value: 60,
          enumerable: false
      })
      a.__proto__ = new Animal();
      
      const b = deepClone(a);
      a.pointer.name = 'jiaojiao'
      console.log(a)
      console.log(b)
      

      在这里插入图片描述
      针对上面的问题,通过下述方法去进行解决:

      • 针对遍历对象的不可枚举属性,通过使用Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组, 结合Object.getOwnPropertyDescriptors()方法返回所有属性的属性值以及属性描述信息进行处理;
      • 当参数为RegExp、Error、Date等类型时,直接生成一个新的实例进行返回;
      • 拷贝对象的原型链则是通过Object.create创建一个新的对象,并继承传入原对象的原型链处理;
      • 使用weakMap类型作为hash表,去检测是否循环引用,如果存在循环,则直接返回weakMap存储的值。
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值