侧边栏壁纸
博主头像
woku博主等级

成功的路上并不拥挤

  • 累计撰写 50 篇文章
  • 累计创建 13 个标签
  • 累计收到 3 条评论

深拷贝,浅拷贝,JSON.stringify

woku
2022-03-07 / 0 评论 / 0 点赞 / 85 阅读 / 4,834 字

拷贝

现实中的拷贝,就是把别人有的东西复制一份一模一样的给我。像U盘一样,从别人的U盘拷贝一份资料到自己的U盘。

前端(程序)中的拷贝,就是把一个变量里面的值或属性复制一份给另一个变量

浅拷贝

含义

创建一个新对象,新对象中拷贝了原始对象的属性值。如果属性值是基本类型,拷贝的就是基本类型的值,如果是引用类型,那么拷贝的就是内存的地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

实现

var obj = {
    a: 1,
    b: 2,
    c: {
        name: 'wyg'
    }
}

使用浅拷贝的方式,将obj属性值全部复制一份给到newObj

  • 创建一个新对象

  • 遍历原始对象obj的属性值

  • 将属性值依次添加到新对象上

  • 返回这个新对象

    var obj = {
      a: 1,
      b: 2,
      c: {
        name: 'wyg'
      }
    }
    function clone(origin) {
      var target = {}
      for (var k in origin) {
        target[k] = origin[k]
      }
      return target
    }
    var newObj = clone(obj)
    newObj.a = 2
    newObj.c.name = 'zhangsan'
    console.log(newObj.a)  // 改变属性值为基本数据类型的值不会影响原对象
    console.log(obj.a)
    console.log(newObj.c.name)  // newObj.c是引用类型的值,他的指针和obj.c的指针是同一个地址,改变了newObj.c里面的内容,obj.c也会受到影响(是同一个地址)
    console.log(obj.c.name)

浅拷贝只拷贝第一层,第一层如果是引用值类型,拷贝的是引用值的地址。

如果改变了obj的c属性值,newObj的c属性值也会改变

上面的遍历属性方式存在的问题:

遍历的时候,for...in...方法会将原型上的方法也复制到

所以要排除原型上自定义的方法和属性

使用hasOwnProperty来判断是否是自身的属性

       function clone(origin,target) {
            var tar = target || {}
            for(var key in origin) {
                if(origin.hasOwnProperty) {
                    tar[key] = origin[key]
                }
            }
            return tar
        } 

深拷贝

含义

将一个对象从内存中完整拷贝一份下来,在堆内存中开辟一个新区域来存放新对象。修改新对象不会影响原始对象。

在浅拷贝中,由于拷贝引用地址时候,是拷贝的指针地址,深拷贝中如果属性值是引用类型,就自己开辟一个新地址,与原始对象的地址不同。就是深拷贝的思路(你用你的,我用我的,我只是把你的东西在我这里复制了一份一样的)

这里要使用递归的思想,因为不知道原始对象有多少层深度。

简单版:

    function deepClone(origin) {
      if (typeof origin === 'object') {
        var target = {}
        for (var k in origin) {
          target[k] = deepClone(origin[k])
        }
        return target
      } else {
        return origin
      }
    }

方案一:

    function deepClone(origin, target) {
      var tar = target || {}
      var toStr = Object.prototype.toString
      var arrLike = '[object Array]'
      for (var k in origin) {
        if (origin.hasOwnProperty(k)) {
          if (typeof origin[k] === 'object' && typeof origin[k] !== null) {
            tar[k] = toStr.call(origin[k]) === arrLike ? [] : {}
            deepClone(origin[k], tar[k])
          } else {
            tar[k] = origin[k]
          }
        }
      }
      return tar
    }

方案二:

    function deepClone(origin) {
      // 如果是null或者undefined或者是原始值,直接返回本身
      if (origin == undefined || typeof origin !== 'object') {
        return origin
      } 
      if (origin instanceof Date) {
        return new Date(origin)
      }
      if (origin instanceof RegExp) {
        return new RegExp(origin)
      }
      var target = new origin.constructor()
      for (var k in origin) {
        if (origin.hasOwnProperty(k)) {
          target[k] = deepClone(origin[k])
        }
      }
      return target
    }

在判断是数组还是对象时候,如果是数组就创建一个新数组,是对象就创建一个新对象,利用了constructor这一特性创建同类型数据

    var arr = []
    var newArr = new arr.constructor()
    newArr.push(1)
    console.log(arr)
    console.log(newArr)

    var o = {}
    var newO = new o.constructor()
    newO.a = 1
    console.log(o)
    console.log(newO)

循环引用

现象

如果一个对象是下面这种情况

    var test1 = {
      a: 1,
      b: {
        name: 'wyg'
      }
    }
    test1.test1 = test1

如果使用上面的deepClone方法拷贝test1,就会出现递归进入死循环导致栈内存溢出。

原因就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况

解决

上面的问题就是循环的引用了自身,导致一直递归。

我们可以开辟一个存储空间来看当前对象有没有被拷贝过,如果拷贝过了就直接返回,没有就继续拷贝。

存储空间是key-value形式,key是当前对象,value是拷贝的对象

   function deepClone(origin, hashMap = new WeakMap()) {
      if (origin == undefined || typeof origin !== 'object') {
        return origin
      }
      if (origin instanceof Date) {
        return new Date(origin)
      }
      if (origin instanceof RegExp) {
        return new RegExp(origin)
      }
      var hashKey = hashMap.get(origin)
      if (hashKey) {
        return hashKey
      }
      var target = new origin.constructor()
      hashMap.set(origin, target)
      for (var k in origin) {
        if (origin.hasOwnProperty(k)) {
          target[k] = deepClone(origin[k], hashMap)
        }
      }
      return target
 }

JSON.stringify

能干啥

使用JSON.stringify()方法将javascript对象或值转为JSON字符串

使用JSON.stringify和JSON.parse也能实现深拷贝(通过先对对象进行JSON字符串化,再通过parse解析出来)

九大特性

  • 对于 undefined、任意的函数以及 symbol 三个特殊的值分别作为对象属性的值、数组元素、单独的值时 JSON.stringify()将返回不同的结果

    • 对象属性值

          var obj = {
              a: 1,
              b: undefined,
              c: function () {},
              d: null,
              e: Symbol('wyg')
          }
          var newObj = JSON.stringify(obj)
          console.log(newObj)
      

      如果对象属性值是undefined,任意的函数,symbol时,JSON.stringify会直接忽略这些属性进行序列化

    • 数组元素

          var arr = [1, undefined, function(){}, null, Symbol('wyg')]
          var newArr = JSON.stringify(arr) // '[1,null,null,null,null]'
      

      undefined、任意的函数以及 symbol 作为数组元素值时,JSON.stringify() 会将它们序列化为 null

    • 单独值

      console.log(JSON.stringify(function(){}))  // undefined
      

      undefined、任意的函数以及 symbolJSON.stringify() 作为单独的值进行序列化时都会返回 undefined

  • 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中

    这个就是说,一个对象的属性序列化后属性的顺序不一定和原对象的属性顺序一致,因为有的属性会被忽略

  • 转换值如果有 toJSON() 函数,该函数返回什么值,序列化结果就是什么值,并且忽略其他属性的值。

           var o = {
              a: 1,
              b: 2,
              toJSON: function() {
                  return '我是json.stringify后的值'
              }
          }
    
          console.log(JSON.stringify(o))
    
  • JSON.stringify() 将会正常序列化 Date 的值。

    实际上 Date 对象自己部署了 toJSON() 方法,因此 Date 对象会被当做字符串处理。

    console.log(JSON.stringify(new Date()))  // "2022-02-17T05:58:59.547Z"
    
  • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值

          var arr = [
              new Number(1), 
              new String('123'), 
              new Boolean(false)
          ]
          console.log(JSON.stringify(arr))
          // [1,"123",false]
    
  • NaN和Infinity格式的数值及 null 都会被当做 null

          var arr = [
              NaN, 
              Infinity, 
              null
          ]
          console.log(JSON.stringify(arr)) // [null,null,null]
    
  • 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。

             // 不可枚举的属性默认会被忽略:  
             var o = Object.create(null, {
                  x: {
                      value: 1,
                      enumerable: true
                  },
                  y: {
                      value: 2,
                      enumerable: false
                  }
              })
             console.log(JSON.stringify(o))  // {"x":1}
    
          var set = new Set([1, 2, 3]) 
          var map = new Map() 
          map.set([1, 2, 3], 'arr') 
          console.log(JSON.stringify(set))
          console.log(JSON.stringify(map))
    
  • 对象是循环引用的情况,使用JSON.stringify会报错

          var data = {
              a: 1,
              b: 2
          }
          data.c = data
          var dd = JSON.stringify(data)
    
    
  • 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们

          JSON.stringify({
              [Symbol.for("foo")]: "foo"
          }, function (k, v) {
              if (typeof k === "symbol") {
                  return "a symbol";
              }
          });
    

通过JSON.stringify的学习,我们可以知道使用它进行深拷贝会存在的问题

JSON序列化时会忽略undefined function Symbol,而Map/Set/WeakMap/WeakSet则会被序列化成可枚举的属性

0

评论区