JavaScript 拷贝全解析:从浅拷贝到深拷贝的避坑指南

229 阅读3分钟

js的拷贝

前言(引言):

在 JavaScript 开发中,​​数据拷贝​​是每个开发者都会遇到的基础操作,但也是隐藏陷阱最多的领域之一。当你把一个对象赋值给另一个变量时,是创建了独立副本还是共享了数据?为什么修改新数组会影响原数组?如何真正"克隆"一个包含嵌套结构的复杂对象?

这些问题背后,涉及 JavaScript 的核心特性——​​按值传递与按引用传递​​的差异。浅拷贝只复制表层,深拷贝才真正"斩断"所有引用关联。错误的拷贝方式可能导致数据污染、状态混乱甚至内存泄漏。

本文将系统剖析 JavaScript 中各种拷贝方法的原理、性能差异和适用场景

V8 是如何储存数据的

1. 栈内存

  • 储存基本数据类型(UndefinedNullBooleanNumberStringBigIntSymbol)和引用数据类型的地址

2. 堆内存

  • 储存引用数据类型(ObjectFunctionArrayDate

拷贝

  • 复刻一个对象和原对象长的一模一样

浅拷贝

浅拷贝只拷贝第一层的数据,若数据中存在引用类型,修改引用类型的数据,拷贝的数据也会改变

1.Object.create(obj)

let obj = {
    a: 1
}
let obj2 = Object.create(obj)   //创建一个新对象,让新对象的隐式原型指向obj
console.log(obj2.a)   // 1

2.[].concat(arr)

let arr1 = [1,2,3]
let arr2 = [4,5]
let arr3 = [].concat(arr1)
console.log(arr1.concat(arr2));    //[1,2,3,4,5]    把arr2中的元素拼接到arr1中
console.log(arr1);     //[1,2,3]
console.log(arr3);     //[1,2,3]
  • 注意:用concat拼接是先创建一个新数组,然后将拼接好的数组放到新数组上去,不会影响原数组

3.数组的解构

let arr1 = [1,2,3]
let arr2 = [...arr1]  //数组的解构
console.log(arr2);  //[1,2,3]

4.arr.slice(0,arr.length)

let arr1 = ['a','b','c']
arr1.splice(1,1)   
console.log(arr1);   //['a','c']
let arr2 = ['a','b','c']
let arr3 = arr2.slice(0,arr2.length) 
console.log(arr3);   //['a','b','c']  

注意:

  • arr.splice() 会影响原数组,arr.slice(0,arr.length) 不会影响原数组

  • arr.slice(0,arr.length)是左闭右开区间

  • arr.slice(0,arr.length)不能增加元素

  • arr.splice(x,y,z)x为从第x元素开始,y表示删除y个元素,z表示增加元素z

5.Object.assign(obj,{})

let obj = {
  name : 'CJ',
  age : 18
}
let girlfriend = {
  girlname : 'ZHT'
}
let newObj = Object.assign(obj,{})  
console.log(obj);  //{ name: 'CJ', age: 18 }

注意:

  • Object.assign()对象的拼接,不分前后顺序

  • 会改变原对象

6.arr.toReversed().reverse()

let arr = [1,2,3]
let newArr = arr.toReversed().reverse()
console.log(newArr);
console.log(arr);

注意:

  • toReversed()和reverse()都是将数组反转

  • toReversed()不改变原数组,reverse()改变原数组

eg

let obj = {
  name: 'CJ',
  age: 22,
  like: {
    a: '唱',
    b: '跳',
    c: 'rap'
  }
}
let newObj = Object.assign({},obj)
obj.like.a = '篮球'
console.log(newObj);

❤如何手写一个浅拷贝❤

Object.prototype.d = 4 
let obj = {
  name: 'CJ',
  age: 22,
  like: {
    a: '唱',
    b: '跳',
    c: 'rap'
  }
}
      //构造一个拷贝函数
function shallowCopy(obj) {
  let newObj = {}
  for (let key in obj) {
    if(obj.hasOwnProperty(key)){
      newObj[key] = obj[key]
    }
  }
  return newObj
}
console.log(shallowCopy(obj));

注意:

Object.prototype.d = 4 是在Object的原型上增加一个属性,由于for in 的缘故会把Object的原型上的属性也给拷贝过来,那么就会与我们所需要拷贝的不符,那么我们用obj.hasOwnProperty(key)方法判断是不是我们所要拷贝的对象里面的属性

深拷贝

层层拷贝,新对象不受原对象的影响

1.JSON.parse(JSON.stringify(obj))

let obj = {
  name: 'CJ',
  age: 22,
  like: {
    a: '唱',
    b: '跳',
    c: 'rap'
  }
}
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj);

注意:

  • 无法识别 bigint 类型,无法处理 undefined,symbol,function

  • 无法处理循环引用

2.structuredClone()

let obj = {
  name: 'CJ',
  age: 22,
  like: {
    a: '唱',
    b: '跳',
    c: 'rap'
  }
}
let newObj = structuredClone(obj)

❤如何手写一个深拷贝❤

let obj = {
  name: 'CJ',
  age: 22,
  like: {
    a: '唱',
    b: '跳',
    c: 'rap'
  }
}
function deepCopy(obj) {
  let newObj = {}
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      //先判断 obj[key] 值的类型,如果是原始类型,直接赋值,如果是引用类型,进行递归
      if (typeof obj[key] === 'object' && obj[key] !== null) {
        newObj[key] = deepCopy(obj[key])
      } else
        newObj[key] = obj[key]
    }
  }
  return newObj
}
console.log(deepCopy(obj));
  • 深拷贝比浅拷贝多一个判断obj[key] 值的类型,如果是原始类型,直接赋值,如果是引用类型,则进行一次递归