JavaScript 的浅拷贝和深拷贝

什么是浅拷贝和深拷贝

  • 拷贝:指拷贝源对象到目标对象,又分为浅拷贝和深拷贝两种
  • 浅拷贝:如拷贝的对象有属性值是非基础类型(即对象),则浅拷贝拷贝的是对象的引用,而非对象本身,拷贝完成以后更改目标对象,源对象也会被更改
  • 深拷贝:深拷贝完美解决了浅拷贝存在的问题,目标对象是一个全新的对象,更改目标对象不会影响到源对象

浅拷贝

Object.assign()

// 对象的属性值都是基础类型
console.log('--------对象的属性值都是基础类型--------')
const src1 = { a: 'aa' }
const target1 = Object.assign({}, src1)
// 输出源对象
console.log('源对象: ', src1)  // {a: "aa"}
// 输出拷贝后的目标对象
console.log('目标对象: ', target1)  // {a: "aa"}
// 更改目标对象
target1.a = 'aaa'
console.log('更改目标对象后的结果: ')
// 输出更改后的目标对象
console.log('目标对象: ', target1)  // {a: "aaa"}
// 输出源对象,发现源对象没有被改变
console.log('源对象: ', src1)  // {a: "aa"}

// 对象的属性值为非基础类型
console.log('--------对象的属性值为非基础类型--------')
const src2 = {flag: 'src2', a: { b: 'bb' } }
const target2 = Object.assign({}, src2)
// 输出源对象
console.log('源对象: ', src2)  // {flag: "src2", a: {b: "bb"}}
// 输出目标对象
console.log('目标对象: ', target2)  // {flag: "src2", a: {b: "bb"}}
// 更改目标对象
target2.flag = 'target2'
target2.a.b = 'bbb'
console.log('更改目标对象后的结果: ')
// 输出更改后的目标对象
console.log('目标对象: ', target2)  // {flag: "target2", a: {b: "bbb"}}
// 输出源对象,发现源对象被改了
console.log('源对象: ', src2)  // {flag: "src2", a: {b: "bbb"}}

in运算符

// 通过for in循环复制对象
function copy (target, src) {
  for (let key in src) {
    // 过滤掉原型链上的属性,只复制src对象自身的属性和值
    if (src.hasOwnProperty(key)) {
      target[key] = src[key]
    }
  }
  return target
}
// 对象的属性值都是基础类型
console.log('--------对象的属性值都是基础类型--------')
const src1 = { a: 'aa' }
const target1 = copy({}, src1)
// 输出源对象
console.log('源对象: ', src1)  // {a: "aa"}
// 输出拷贝后的目标对象
console.log('目标对象: ', target1)  // {a: "aa"}
// 更改目标对象
target1.a = 'aaa'
console.log('更改目标对象后的结果: ')
// 输出更改后的目标对象
console.log('目标对象: ', target1)  // {a: "aaa"}
// 输出源对象,发现源对象没有被改变
console.log('源对象: ', src1)  // {a: "aa"}

// 对象的属性值为非基础类型
console.log('--------对象的属性值为非基础类型--------')
const src2 = {flag: 'src2', a: { b: 'bb' } }
const target2 = copy({}, src2)
// 输出源对象
console.log('源对象: ', src2)  // {flag: "src2", a: {b: "bb"}}
// 输出目标对象
console.log('目标对象: ', target2)  // {flag: "src2", a: {b: "bb"}}
// 更改目标对象
target2.flag = 'target2'
target2.a.b = 'bbb'
console.log('更改目标对象后的结果: ')
// 输出更改后的目标对象
console.log('目标对象: ', target2) // {flag: "target2", a: {b: "bbb"}}
// 输出源对象,发现源对象被改了
console.log('源对象: ', src2)  // {flag: "src2", a: {b: "bbb"}}

深拷贝

JSON

  • 优点
    javascript的内置方法,简单、性能好
  • 缺点
    会忽略掉对象中属性值为undefined和函数的属性
    由于方法的底层实现用了递归,如果对象存在循环引用,会爆栈(报循环引用的错)

实现

// 直接上对象的属性值为非基础类型的对象
const src = { flag: 'src', a: { b: 'bb' } }
console.log('源对象: ', src)  // 源对象:  {flag: "src", a: {b: "bb"}}
const target = JSON.parse(JSON.stringify(src))
console.log('目标对象: ', target) // 目标对象: {flag: "src", a: {b: "bb"}}
console.log('--------更改目标对象--------')
target.flag = 'target'
target.a.b = 'bbb'
console.log('目标对象: ', target)  // 目标对象:  {flag: "target", a: {b: "bbb"}}
// 发现源对象没有被改变
console.log('源对象: ', src)  // 源对象: {flag: "src", a: {b: "bb"}}

// 异常情况 - 属性值为undefined和function的情况
const obj = { a: 'a', b: undefined, c: function () { conole.log('c function') }}
console.log('源对象: ', obj)  // 源对象:  {a: "a", b: undefined, c: ƒ}
const copyObj = JSON.parse(JSON.stringify(obj))
// 对象的b和c属性不见了
console.log('拷贝后的目标对象: ', copyObj)  // 拷贝后的目标对象:  {a: "a"}

// 异常情况 - 对象存在循环引用
const objLoop = {}
const b = {objLoop}
objLoop.b = b
console.log('源对象: ', objLoop) // {b: {objLoop: {b: {objLoop: {b: ...}}}}}
// 报错: Uncaught TypeError: Converting circular structure to JSON
const copyObjLoop = JSON.parse(JSON.stringify(objLoop))


MessageChannel

  • 优点
    JavaScript的内置API,简单,且可以复制属性值为undefined的对象,也可以解决循环引用的问题
  • 缺点
    对象有属性值为函数时会报错
    方法是异步的

实现

// 拷贝方法
function deepCopy (obj) {
  return new Promise((resolve, reject) => {
    const {port1, port2} = new MessageChannel()
    port1.postMessage(obj)
    port2.onmessage = function(e) {
      resolve(e.data)
    }
  })
}

// 示例一,正常对象
// 源对象
let src = { flag: 'src', a: { b: 'bb' } }
// 目标对象
let target = {}
deepCopy(src).then(res => {
  target = res
  console.log('源对象: ', src)  // 源对象:  {flag: "src", a: {b: "bb"}}
  console.log('目标对象: ', target) // 目标对象: {flag: "src", a: {b: "bb"}}
  console.log('--------更改目标对象--------')
  target.flag = 'target'
  target.a.b = 'bbb'
  console.log('目标对象: ', target)  // 目标对象:  {flag: "target", a: {b: "bbb"}}
  // 发现源对象没有被改变
  console.log('源对象: ', src)  // 源对象: {flag: "src", a: {b: "bb"}}
})

// 示例二,属性值为undefined的情况
const obj = { a: 'a', b: undefined}
target = {}
deepCopy(obj).then(res => {
  console.log('源对象: ', obj)  // 源对象:  {a: "a", b: undefined, c: ƒ}
  target = res
  console.log('拷贝后的目标对象: ', target)  // 拷贝后的目标对象:  {a: "a", b: undefined}
})

// 示例三, 属性值有函数时,会报错: Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'MessagePort': function () {} could not be cloned.
/*
const obj1 = { a: 'a', b: undefined, c: function () {}}
target = {}
deepCopy(obj1).then(res => {
  console.log('源对象: ', obj)  // 源对象:  {a: "a", b: undefined, c: ƒ}
  target = res
  console.log('拷贝后的目标对象: ', target)
})
*/

// 示例四,循环引用
const obj2 = {}
const b = {obj2}
obj2.b = b
target = {}
deepCopy(obj2).then(res => {
  console.log('源对象: ', obj2) // {b: {obj2: {b: {obj2: {b: ...}}}}}
  target = res
  console.log('目标对象: ', target) // {b: {obj2: {b: {obj2: {b: ...}}}}}
})


递归

  • 优点
    可以解决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 obj1 = {a: 'a', b: {c: 'cc'}}
console.log('源对象: ', obj1) // 源对象:  {a: "a", b: {c: "cc"}}
const copyObj1 = deepCopy(obj1)
console.log('拷贝后的目标对象: ', copyObj1) // 拷贝后的目标对象:  {a: "a", b: {c: "cc"}}
console.log('-----改变目标对象的属性---------')
copyObj1.b = 'bb'
console.log('源对象: ', obj1) // 源对象:  {a: "a", b: {c: "cc"}}
console.log('目标对象: ', copyObj1) // 目标对象:  {a: "a", b: "bb"}

// 示例二, 对象存在循环引用
const objLoop = {}
const b = {objLoop}
objLoop.b = b
console.log('源对象: ', objLoop) // {b: {objLoop: {b: {objLoop: {b: ...}}}}}
// 报错: Uncaught RangeError: Maximum call stack size exceeded
const copyObjLoop = deepCopy(objLoop)


闭包 + 递归

说明

解决了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 obj1 = {a: 'a', b: {c: 'cc'}}
console.log('源对象: ', obj1) // 源对象:  {a: "a", b: {c: "cc"}}
const copyObj1 = deepCopy(obj1)
console.log('拷贝后的目标对象: ', copyObj1) // 拷贝后的目标对象:  {a: "a", b: {c: "cc"}}
console.log('-----改变目标对象的属性---------')
copyObj1.b = 'bb'
console.log('源对象: ', obj1) // 源对象:  {a: "a", b: {c: "cc"}}
console.log('目标对象: ', copyObj1) // 目标对象:  {a: "a", b: "bb"}

// 示例二, 对象存在循环引用
const objLoop = {}
const b = {objLoop}
objLoop.b = b
console.log('源对象: ', objLoop) // 源对象: {b: {objLoop: {b: {objLoop: {b: ...}}}}}
const copyObjLoop = deepCopy(objLoop)
console.log('目标对象: ', copyObjLoop) // 目标对象: {b: {objLoop: {b: {objLoop: {b: ...}}}}}
console.log('-----改变目标对象------')
copyObjLoop.b = 'bb'
console.log('源对象: ', objLoop) // 源对象: {b: {objLoop: {b: {objLoop: {b: ...}}}}}
console.log('目标对象: ', copyObjLoop) // 目标对象: {b: "bb"}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值