JS中的深拷贝与浅拷贝

125 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情

数据类型

js的数据类型分为两类基本数据类型引用数据类型,前者是存储在栈内存中,后者是将其地址存在栈内存中,而真实数据存储在堆内存中。

JavaScript中基本数据类型有StringNumberUndefinedNullBooleanSymbol,基本类型在赋值的过程中都是深拷贝,从一个变量复制基本类型的值到另一个变量后这两个变量的值是完全独立的,互不影响。

引用数据类型如ObjectArray...,当变量复制引用类型值的时候会复制一个地址,通过这个地址在堆内存中获取到相应的值,所以引用类型的值是按引用访问。那这样复制与被复制的对象用的都是同一个地址,修改其中一个值会影响到另一个,那怎么解决这个问题呢,且往下看

浅拷贝

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

浅拷贝方法

1. slice

slice 方只仅仅针对数组类型slice 方法通过截取开始和结束来位置会返回一个新的数组对象,是不会影响和改变原始数组的。但是,数组元素是引用类型的话,也会影响到原始数组。

var arr1 = [1, 2, {num: 3}]
var arr2 = arr1.slice()
arr2[0] = 88
arr2[2].num = 99
console.log(arr1) // [1, 2, {num: 99}]
console.log(arr2) // [88, 2 {num: 99}]

arr1[0]元素是基本数据类型,所以arr2能够改变其值,而不影响arr1的值。而arr[2]是引用数据类型,arr1arr2指向同一块堆内存地址,所以这两个变量中num都变成了99

2. concat

concat通过链接数组组成一个新的数组,拷贝和slice相似都是浅拷贝且都只针对数组类型,遇到拷贝数组有引用类型原数组也会被改变

var arr1 = [1, 2, {num: 3}]
var arr2 = [].concat(arr1)
arr2[0] = 88
arr2[2].num = 99
console.log(arr1) // [1, 2, {num: 99}]
console.log(arr2) // [88, 2 {num: 99}]

3. 扩展运算符

拷贝对象

let obj = {
  name: '小鱼',
  info: {
    age: 18,
  }
}

let copyObj = {
  ...obj
}

copyObj.name = '小鸡' // 基本数据类型
copyObj.info.age = 88 // 引用数据类型
console.log(obj)  //{ name: '小鱼', info: { age: 88 } }
console.log(copyObj);  //{ name: '小鸡', info: { age: 88 } }

很明显修改了copyObj同时obj也被修改了,因为是浅拷贝,引用的还是同一个地址

拷贝数组

var arr1 = [1, 2, {num: 3}]
var arr2 = [...arr1]
arr2[0] = 88
arr2[2].num = 99
console.log(arr1) // [1, 2, {num: 99}]
console.log(arr2) // [88, 2 {num: 99}]

4. object.assign

object.assign 可以用于对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象,可以接受多个来源。

let obj = {
  name: '小鱼',
  info: {
    age: 18,
  }
}
let obj2 = {
    sym: Symbol(1)
}
let copyObj = Object.assign({}, obj, obj2)
copyObj.name = '小鸡'
copyObj.info.age = 88
console.log(obj)  //{ name: '小鱼', info: { age: 88 } }
console.log(copyObj);  //{ name: '小鸡', info: { age: 88 },sym: Symbol(1) }
  • 它不会拷贝对象的继承属性
  • 它不会拷贝对象的不可枚举的属性
  • 可以拷贝 Symbol 类型的属性

实现浅拷贝

上面这些方法都只能实现浅拷贝,只能拷贝一层对象,如果遇到引用类型则会改变元宿主的值。那所以总结浅拷贝的原理就是如果是基本数据类型,则直接拷贝值;如果引用数据类型,拷贝一层对象属性且两个对象指向同一个内存地址

function shallowCopy(obj) {
    if (typeof obj !== 'object') return;
    let newObj = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        newObj[key] = obj[key];
      }
    }
    return newObj;
}

// 测试
var arr1 = [1, 2, {num: 3}]
var arr2 = shallowCopy(arr1)
arr2[0] = 88
arr2[2].num = 99
console.log(arr1) // [1, 2, {num: 99}]
console.log(arr2) // [88, 2 {num: 99}]

深拷贝

深拷贝是对于复杂数据类型在堆内存中开辟了一块内存地址用于存放复制的对象并且把原有的对象复制过来,这两个对象是相互独立的,也就是两个不同的地址,达到互不影响的目的。

深拷贝方法

1. JSON.stringify

把一个对象序列化成为JSON的字符串,并将对象里面的内容转换成字符串,最后再用JSON.parse()的方法将JSON 字符串生成一个新的对象。

var arr1 = [1, 2, {num: 3}]
var arr2 = JSON.parse(JSON.stringify(arr1))
arr2[0] = 88
arr2[2].num = 99
console.log(arr1) // [1, 2, {num: 3}]
console.log(arr2) // [88, 2 {num: 99}]

可以看到这次的结果和上面的不一样了,arr1没有受到任何影响,看来我们的目的是暂且达到了,为什么说是暂且呢,且往下看

let obj = {
    name: '小鱼',
    info: {
        age: 18
    },
    arr: [1],
    und: undefined,
    sym: Symbol('999'),
    date: new Date(),
    regexp: /^A/,
    nan: Nan
}
let copyObj = JSON.parse(JSON.stringify(obj));
console.log(copyObj);

// {
//    name: '小鱼,
//    date: "2023-02-10T10:03:08.974Z",
//    arr: [1],
//    info: {age:18},
//    nan: null,
//    regexp: {},
// }
  • 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,会直接忽略
  • 拷贝Date得到的是字符串
  • 拷贝RegExp得到的是空对象
  • 拷贝 NaN、Infinity 以及 -Infinity,得到的是null
  • 无法拷贝对象的循环应用

实现深拷贝

实现思想:原始类型直接拷贝,引用类型递归。

通过第三方实现

1. lodash

image.png

2. jQuery

image.png

3. 递归实现

for in遍历传入参数的值,如果值是引用类型则再次调用deepClone函数,并且传入第一次调用deepClone参数的值作为第二次调用deepClone的参数,如果不是引用类型就直接复制。

function deepCopy(obj) {
    // 过滤原始类型
    if (typeof obj !== 'object') return obj;​
    // 过滤null类型 因为typeof null==object,使用不能使用typeof判断null数据类型
    if (obj == null) return obj;​
    let newObj = Array.isArray(obj) ? [] : {};
    // let newObj=obj instanceof Array ?[]:{};// 拷贝Date对象
    if (obj instanceof Date) {
      newObj = new Date(obj)
    }
    // 拷贝RegExp对象
    if (obj instanceof RegExp) {
      newObj = new RegExp(obj)
    }​
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) { //自己本身具有的属性
        newObj[key] = typeof obj[key] == 'object' ? deepCopy(obj[key]) : obj[key];
      }
    }
    return newObj;
}

image.png