浅拷贝与深拷贝

一、对浅拷贝与深拷贝的理解

浅拷贝和深拷贝是针对引用数据类型而言的,对于基本数据类型是没有深浅拷贝的概念。

二、从存储的角度理解

js的基本数据类型(String,Number,Boolean,null,Undefined)是存在栈内存的,当发生赋值b=a时会直接在栈内存中开辟一个新空间。a和b是两块独立的空间。修改b不会影响a的值。

而js对于引用数据类型object。存储时会在栈内存存储引用(堆内存中存值的地址),堆内存存储真正的值,栈内存中的引用指向堆内存的值。

当发生 = 的赋值操作 b = a 时,实际上只是把a的引用复制给了b,也就是说a和b指向的堆内存中同一片地址空间。像这种只拷贝引用的就是浅拷贝

深拷贝就是在堆内存中开辟一块新的空间,把原来对象拷贝对象的值复制到新的存储空间,引用指向新的存储空间。这样a和b就是两个完全不同的对象。修改b的时候不会同步修改a的值。

手写实现深拷贝和浅拷贝

深拷贝

JSON

对于可以直接转成json的object类型。可以通过先将object转成json字符串(JSON.stringify)然后解析(JSON.parse)实现深拷贝。

const obj = {
    name: 'dd',
    age: 18,
    say: {
        text: 'hello'
    }
}
const str = JSON.stringify(obj)
let obj2 = JSON.parse(str)
obj2.say.text = 'hi'
console.log(obj); //{ name: 'dd', age: 18, say: { text: 'hello' } }
console.log(obj2); //{ name: 'dd', age: 18, say: { text: 'hi' } }

但是 JSON.stringify 实现深拷贝有些地方需要注意:

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

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

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

  • 无法拷贝对象原型链

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

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

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

const obj3 = {
func: function() {console.log(1)},
obj: { name: 'h' },
arr: [1,2,3],
und: undefined,
ref: /^123$/,
date: new Date(),
NaN: NaN,
infinity: Infinity,
sym: Symbol(1)
}

console.log(JSON.parse(JSON.stringify(obj3)))
// NaN: null
// arr: (3) [1, 2, 3]
// date: "2021-01-29T16:09:10.788Z"
// infinity: null
// obj:
// name: "h"
// ref: {}

从上面代码中可以到例如:function 和 undefined 都消失了。所以,JSON.stringify 实现深拷贝,还是有很多无法实现的功能,但如果只是基础普通的对象类型,使用 stringify 还是非常快捷方便的。

递归

  • 优点

  • 可以解决JSON方式忽略属性值为undefined和function的属性的问题

  • 对象存在循环引用时仍然会爆栈

function deepCopy (obj) {
    const target = {}
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        // key为对象自身可枚举的属性
        if (
          Object.prototype.toString.call(obj[key]) === '[object Object]'
        ) {
          // 属性值为对象,递归调用
          target[key] = deepCopy(obj[key])
        } else {
          target[key] = obj[key]
        }
      }
    }
    return target
  }
  const objtest = {
    func: function() {console.log(1111)},
    obj: { name: 'dd' , data: { fn: function() { console.log('data') }, child: 'child' }},
    arr: [1,2,3],
    und: undefined,
    ref: /^123$/,
    date: new Date(),
    NaN: NaN,
    infinity: Infinity,
    sym: Symbol(222)
  }
  console.log(deepCopy(objtest));

//   func: [Function: func],
//   obj: { name: 'dd', data: { fn: [Function: fn], child: 'child' } },
//   arr: [ 1, 2, 3 ],
//   und: undefined,
//   ref: /^123$/,
//   date: 2022-04-21T02:06:50.712Z,
//   NaN: NaN,
//   infinity: Infinity,
//   sym: Symbol(222)

 闭包 + 递归

解决了JSON方式和递归方式存在的问题,可实现真正的深拷贝

function deepCopy (copyObj) {
    // 用来记录已经拷贝过的属性值为对象的属性以及属性的值,解决递归循环引用对象的爆栈问题
    const cache = {}

    // 拷贝对象
    function copy (obj) {
      const target = {}
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          // key为对象自身可枚举的属性
          if (
            Object.prototype.toString.call(obj[key]) === '[object Object]'
          ) {
            // 属性值为对象
            if (cache[obj[key]]) {
              // 说明该属性已经被拷贝过一次,现在又拷贝,证明出现了循环引用
              target[key] = cache[obj[key]]
            } else {
              cache[obj[key]] = obj[key]
              target[key] = copy(obj[key])
            }
          } else {
            target[key] = obj[key]
          }
        }
      }
      return target
    }
    return copy(copyObj)
  }
  const objtest = {
    func: function() {console.log(1111)},
    obj: { name: 'dd' , data: { fn: function() { console.log('data') }, child: 'child' }},
    arr: [1,2,3],
    und: undefined,
    ref: /^123$/,
    date: new Date(),
    NaN: NaN,
    infinity: Infinity,
    sym: Symbol(222)
  }
  console.log(deepCopy(objtest));

//   func: [Function: func],
//   obj: { name: 'dd', data: { name: 'dd', data: [Object] } },
//   arr: [ 1, 2, 3 ],
//   und: undefined,
//   ref: /^123$/,
//   date: 2022-04-21T02:58:35.309Z,
//   NaN: NaN,
//   infinity: Infinity,
//   sym: Symbol(222)

真正的深拷贝

解决上述的几个问题的思路:

  • 如果是 Date,RegExp 直接创建一个新的实例并返回

  • 如果是循环引用就用 weakMap 解决。

  • 原型的上的方法,可以结合 Object.getOwnPropertyDescriptors 获取对象上的所有属性及特性及 Object.getPrototypeOf 原型上的方法和 Object.create 结合使用,创建一个新对象,并继承传入原对象的原型链。

  • 对象的不可枚举属性以及Symbol 类型,可以使用 Reflect.ownKeys 方法。

const objtest = {
    func: function() {console.log(1111)},
    obj: { name: 'dd' , data: { fn: function() { console.log('data') }, child: 'child' }},
    arr: [1,2,3],
    und: undefined,
    ref: /^123$/,
    date: new Date(),
    NaN: NaN,
    infinity: Infinity,
    sym: Symbol(222)
  }
  const _completeDeepClone = (target, map = new Map()) => {
    // 日期类型直接返回一个新的日期对象
    if (target instanceof Date) return new Date(target)
    // 正则对象直接返回新的正则对象
    if (target instanceof RegExp) return new RegExp(target)
    // 循环引用使用 weakMap 解决
    if (map.has(target)) return map.get(target)
    const desc = Object.getOwnPropertyDescriptors(target)
    // 获取原型上的方法和对象的描述信息,创建新的对象
    const copyObj = Object.create(Object.getPrototypeOf(target), desc)
    map.set(target, copyObj)
  
    // 循环递归遍历内容,防止还会有共计内存的问题
    for (const key of Reflect.ownKeys(target)) {
      let item = target[key]
      if (typeof item === 'object' && item !== null && typeof item !== 'function') {
        copyObj[key] = _completeDeepClone(item)
      } else {
        copyObj[key] = item
      }
    }
    return copyObj
  }
  console.log(_completeDeepClone(objtest))
//   func: [Function: func],
//   obj: { name: 'dd', data: { fn: [Function: fn], child: 'child' } },
//   arr: Array { '0': 1, '1': 2, '2': 3 },
//   und: undefined,
//   ref: /^123$/,
//   date: 2022-04-21T01:54:57.395Z,
//   NaN: NaN,
//   infinity: Infinity,
//   sym: Symbol(222)

浅拷贝

Object.assign()

ES6 中 Object 的一方法,可以是来合并多个JS对象(能用来实现浅拷贝) 第一个参数拷贝的目标对象,后面的参数是拷贝的来源对象

  • 语法

Object.assign(target, ...sources)

注意: Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。 使用 Object.assign 方法注意以下几点:

  • 不会拷贝对象的继承属性

  • 不会拷贝对象的不可枚举属性

  • 可以拷贝 Symbol 类型的属性

扩展运算法

利用扩展运算法,可以实现浅拷贝的的功能。

拷贝对象

 let obj = {a: 1, b: 2}
  let obj1 = {...obj}
  obj1.b = 3
  console.log(obj1)// {a: 1, b: 3}
  console.log(obj)// {a: 1, b: 2}

拷贝数组

  • 能利用原生的数组的方法实现浅拷贝的有好多个

  • concat 拷贝数组

  let arr = [1,2,3]
  let arr1 = arr.concat()
  arr1[0] = 5
  console.log(arr1) // [5, 2, 3]
  console.log(arr) // [1, 2, 3]
  • slice 拷贝数组
let arr = [1,2,3]
let arr2 = arr.slice()
arr2[2] = 4
console.log(arr2) //[1, 2, 4]
console.log(arr) // [1, 2, 3]
  • Array.from 拷贝数组
let arr = [1,2,3]
let arr4 = Array.from(arr)
arr4.push(1)
console.log('arr4', arr4) // [1,2,3,1]
console.log(arr) // [1,2,3]

实现浅拷贝

手写浅拷贝的思路:

  1. 基础类型做最基本的赋值就可。

  2. 引用数据类型,需要开辟一个新的存储,并拷贝一层对象属性。

function shallowCopy(target) {
  // 引用类型需要开辟一个新的存储地址
  if (typeof target === 'object' && target !== null) {
    const copy = Array.isArray(target) ? [] : {}
    for (const prop in target) {
      if (target.hasOwnProperty(prop)) {
        copy[prop] = target[prop]
      }
    }
    return copy
  }
  // 如果是基础类型就直接返回
  return target
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值