深入了解JavaScript中的对象拷贝:浅拷贝与深拷贝


JavaScript中的对象拷贝:浅拷贝与深拷贝

JavaScript中的对象拷贝是开发中常见的操作,它允许我们创建原对象的副本,以便进行修改或传递而不影响原始对象,它只针对引用类型使用。在拷贝过程中,存在着浅拷贝和深拷贝两种方式,每种方式都有其优缺点和适用场景。

浅拷贝

浅拷贝是指创建一个新的对象或数组,复制原始对象或数组的基本属性或者元素,但是对于原始对象或数组中的引用类型属性(如对象、数组),仅复制其引用地址而不是实际内容。因此,当修改原对象或数组中的引用类型属性时,会影响到新对象或数组。

主要特点:

  1. 复制基本属性和元素: 浅拷贝会复制原始对象或数组的基本属性或元素,包括基本数据类型(如字符串、数字等)或一级深度的对象。
  2. 引用类型属性复制引用地址: 对于原对象或数组中的引用类型属性,浅拷贝仅复制其引用地址,而不是实际内容。
  3. 修改引用类型属性影响拷贝结果: 当修改原对象或数组中的引用类型属性时,会影响新对象或数组,因为它们共享相同的引用地址。

常见的浅拷贝方法:

  1. Object.create(obj) 创建一个新对象,并将现有对象作为新对象的原型(__proto__)。
  2. Object.assign({}, obj) 创建一个新对象,并将现有对象的属性和方法复制到新对象中。
  3. [].concat(arr) 创建一个新数组,将现有数组的元素复制到新数组中。
  4. 数组解构赋值: 可以用于数组的浅拷贝。

Object.create(obj)

使用Object.create(obj)方法会会继承原对象的原型链,创建一个新对象,使用现有的对象来提供新创建的对象的__proto__;因此此方法常用于创建具有特定原型的新对象,通过指定原型对象,可以实现对象之间的原型链继承,使得新创建的对象能够共享原型对象的属性和方法。

如下列代码:

css 复制代码
let a = {
  name: 'XM'
}
let b = Object.create(a) //隐式继承 浅拷贝
a.name = 'XH'
console.log(b.name); //XH

在上述代码中,Object.create(a) 创建了一个新对象 b,它继承了a对象的原型链,因此能够访问a,对象中定义的name属性。
注意 :传入的参数不是一个对象或是 null,会抛出 TypeError 错误。

Object.assign({}, obj)

Object.assign() 方法是用于将一个或多个原对象的属性复制到目标对象中,并返回目标对象。该方法会修改目标对象,若目标对象已有相同属性,则会覆盖原有属性。

Object.create(obj)方法不同的是,Object.assign() 方法不会继承原对象的原型链。

如下列代码所示:

ini 复制代码
const t={ a: 1, b: 2 };
const s = { b: 3, c: 4 }; 
const u = { d: 5 };
const newObj = Object.assign(t, s, u);
console.log(newObj); // 输出: { a: 1, b: 3, c: 4, d: 5 },目标对象被修改

在上述示例中,Object.assign()su 对象的属性复制到 t 目标对象中,最终返回了修改后的 newObjb 属性在 s 中的值覆盖了 t 中的值,而 cd 属性则是新增的。

另外:

css 复制代码
let a={
    like: {
       n: 'coding'
       }
}
let b = Object.assign(a,b)
a.like.n = 'music'
console.log(b.like.n); //music 拷贝了like对象的引用地址,所以b.like.n也会被修改
let c = Object.assign({}, a, b) //使用此方法会将b放到a里面,再将a放到{}里面,a不会被修改

上述代码中,进一步说明了使用Object.assign()方法时,会拷贝原对象的引用地址,当地址中的元素改变时,新对象的相应元素也会发生改变。当使用Object.assign({}, a, b)拼接对象时,当原对象中元素改变时,不会改变a中的元素。

[].concat(arr)

当使用 [].concat(arr) 方法进行浅拷贝时,它可以用于复制一个数组的元素到一个新的数组中,从而创建一个新的数组对象。这是数组的浅拷贝使用用法,不会继承原数组的原型链,并且concat() 方法不会修改原始数组,而是返回一个新数组。。

如下列代码所示:

ini 复制代码
let arr = [1, [2, 3],{n: 10}]
let newArr = [].concat(arr) //浅拷贝 创建了一个新数组
arr[3].n = 100
console.log(newArr); //[ 1, 2, 3, { n: 100 } ]

上述代码中,[].concat(arr) 创建了一个新的数组 newArr,包含了 arr 的所有元素。但需要注意的是,如果原数组中存在嵌套数组,concat() 方法只会复制嵌套数组的引用地址到新数组中,因此在修改原数组中的嵌套数组元素时,新数组中的嵌套数组元素也会受到影响。

数组解构赋值

数组解构赋值可以用于复制数组的元素到一个新数组中,从而实现浅拷贝的效果。

如下列代码:

scss 复制代码
let arr = [1, [2, 3],{n: 10}]
let newArr = [...arr] //浅拷贝
arr.push(4) //往arr里面添加元素,newArr不会被修改
console.log(newArr); //[ 1, [2, 3], { n: 10 } ]

上述代码中,通过使用[...arr]进行数组解构赋值,创建了一个新的数组newArr。当修改原始数组中的元素时,新数组不受影响,因为它们是两个不同的数组。需要注意的是,当处理嵌套数组时,嵌套数组的解构会生成一个新的嵌套数组,但其中的引用类型元素仍然保持引用关系。

浅拷贝的实现方法

javascript 复制代码
let obj = {
  name: 'TM',
  age: 21,
  like: {
    n: 'coding'
  }
}
function shallowCopy(obj) {
  // 不是引用类型就不拷贝
  if(typeof obj !== 'object' || obj === null) return
  // 如果形参obj是数组,就创建一个新数组,如果是对象,就创建一个新对象
  // if (Array.isArray(obj)) {
  //   let objCopy = []
  // } else {
  //   let objCopy = {}
  // }
  let objCopy = obj instanceof Array ? [] : {}
  // obj instanceof Array ? [] : {} 是一个条件(三元)运算符,
  // 它会检查 obj 是否是一个数组。如果 obj 是一个数组,那么就创建一个新的空数组[],否则创建一个新的空对象 { }。

  // 遍历obj,将obj的属性赋值给objCopy
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      objCopy[key] = obj[key]
      // 如果是数组 onjCopy[0]=obj[0]
      // 如果是对象 objCopy.name=obj.name
    }
  }
  return objCopy
}
let newObj = shallowCopy(obj)
obj.like.n = 'music'
console.log(newObj); //{ name: 'TM', age: 21, like: { n: 'music' } }

深拷贝

深拷贝是指创建一个新的对象或数组,复制原始对象或数组的所有内容,包括基本属性、引用类型属性(对象、数组等)以及嵌套的对象或数组内容。深拷贝不会受到原始对象修改的影响,它会完全复制所有嵌套对象的内容,确保新对象与原对象完全独立。

深拷贝的方法:

JSON.parse(JSON.stringify(obj))

利用 JSON.stringify() 将原对象转换成 JSON 字符串,再用 JSON.parse() 将 JSON 字符串转换成新的对象。这种方法会忽略undefined、symbol函数,会抛弃对象的constructor,也就是深拷贝之后,不管这个对象原本的构造函数是什么,在深拷贝之后都会变成Object。需要注意的是,此方法不能处理undefined、symbol、function这些数据类型,也无法处理循环引用。

如下列代码:

yaml 复制代码
let obj = {
  name: 'TM',
  age: 18,
  a: {
    n: 1
  },
  b: undefined,
  c: null,
  d: function () { },
  e: Symbol('hello'),
  f: {
    n: 100
  }
}
let obj2 = JSON.parse(JSON.stringify(obj)); // 深拷贝
obj.age = 20;
obj.a.n = 2

console.log(obj2); // { name: 'TM', age: 18, a: { n: 1 }, c: null, f: { n: 100 } }

如上述代码所示,使用JSON.parse(JSON.stringify(obj))将转换后的对象拷贝给obj2,即使更改obj的元素后,obj2中的元素也没有发生改变,因此深拷贝不会受到原始对象修改的影响。

深拷贝的实现方法

在js中实现深拷贝,可以使用递归调用的方法实现

javascript 复制代码
let obj = {
  name: 'TM',
  age: 18,
  a: {
    n: 1
  },
  b: undefined,
  c: null,
  d: function () { },
  e: Symbol('hello'),
  f: {
    n: 100
  }
}
function deepCopy(obj) {
  let objCopy = {}
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      //区分obj[key]是原始类型或者引用类型
      if (obj[key] instanceof Object) {// 不能直接赋值
        objCopy[key] = deepCopy(obj[key]) // 递归调用 
      } else {
        objCopy[key] = obj[key]
      }
    }
  }
  return objCopy
}
let obj2 = deepCopy(obj)
obj.age = 20;
console.log(obj2);

结语

在大部分情况下浅拷贝都能满足我们的需求,但对于嵌套的引用类型属性,需要特别小心,因为它们共享相同的引用地址。相比之下,深拷贝能够完全复制所有的嵌套内容,但在处理循环引用、特殊类型(如函数、undefined 等)以及性能方面需要更多的考虑。

相关推荐
慧一居士14 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead16 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子6 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina6 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路7 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_7 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
伍哥的传说8 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js