Javascript的深浅拷贝原来是这样的 | 七日打卡

1,113 阅读5分钟

前言

Javascript(下面简称JS)世界里将数据类型分为了两种:原始数据类型引用数据类型

现共有八种数据类型:StringNumberBooleanNullUndefinedObjectSymbol(ES6)Bigint(ES10)

  • 原始数据类型:StringNumberBooleanNullUndefinedSymbolBigint
  • 引用数据类型:Object(Array、Function、Date、RegExp等等)。

不同类型的存储方式:

  • 原始数据类型:原始数据类型的值在内存中占据固定大小,保存在栈内存中。
  • 引用数据类型:引用数据类型的值是对象,在栈内存中只是保存对象的变量标识符以及对象在堆内存中的储存地址,其内容是保存中堆内存中的。

不同类型的拷贝方式:

  • 原始数据类型

    对于原始数据类型,从一个变量拷贝到另一个变量,只是很单纯的赋值过程,两者是不会有什么影响的。

    let a = 1;
    let b = a;
    a = 3;
    
    console.log(a, b); // 3 1
    
  • 引用数据类型

    而对于引用数据类型,从一个变量拷贝到另一个变量,本质是拷贝了对象的储存地址,两个变量最终都还是指向同一个对象。

    let a = {name: '橙某人'};
    let b = a;
    a.name = 'yd';
    
    console.log(a, b);
    
    let c = [1];
    let d = c;
    c[0] = 2;
    
    console.log(c, d);
    

所以本章我们要讲的深浅拷贝针对是引用数据类型,下面我们就来仔细瞧瞧看看吧。

浅拷贝 与 深拷贝

概念

为什么存在浅拷贝深拷贝
简单一句话:“防止父对象数据被篡改。”

  • 浅拷贝:完成拷贝后可能存在彼此之间操作互相影响的就是浅拷贝。
  • 深拷贝:完成拷贝后彼此之间操作绝对不会有互相影响的就是深拷贝。

浅拷贝

  • 数组浅拷贝 - concat()slice()
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[0] = 20;
console.log(arr, newArr); // [1, 2, 3] [20, 2, 3]

let arr1 = [1, 2, 3];
let newArr1 = arr1.slice();
newArr1[0] = 20;
console.log(arr1, newArr1); // [1, 2, 3] [20, 2, 3]
  • 对象浅拷贝
function copy(object) {
  let o = {};
  for(let key in object) {
    o[key] = object[key];
  }
  return o;
}
let obj = {name: '橙某人'};
let newObj = copy(obj);
newObj.name = 'YD';
console.log(obj, newObj); // {name: "橙某人"} {name: "YD"}

懵了? 上面不是说不是说不互相影响的就是深拷贝吗?小编欺骗你?咋会呢? 不要着急。

我在上面留了个心眼,就是浅拷贝可能存在彼此之间操作互相影响,也就是说浅拷贝也可能存在不会互相影响的情况。

文字游戏?小编玩起了文字游戏?给我打。。。

害,我也没有办法,这两个概念就是怎么,如果单纯考虑对象只有一层深度且也只是普通的原始数据类型值的话,那你说上面的例子是深拷贝,那我不反驳,哈哈。

但他们确实是浅拷贝啦,如果是如下多层对象深度的话,就跑不了了。

// 数组
let arr = [1, 2, [1]];
let newArr = arr.concat();
newArr[2][0] = 10;
console.log(arr, newArr); // [1, 2, [10]] [1, 2, [10]]

let arr1 = [1, 2, [1]];
let newArr1 = arr1.slice();
newArr1[2][0] = 10;
console.log(arr1, newArr1); // [1, 2, [10]] [1, 2, [10]]

// 对象
function copy(object) {
  let o = {};
  for(let key in object) {
    o[key] = object[key];
  }
  return o;
}
let obj = {name: '橙某人', hobby: {val: 'basketball'}};
let newObj = copy(obj);
newObj.hobby.val = 'football';
console.log(obj, newObj); // {name: '橙某人', hobby: {val: 'football'}} {name: '橙某人', hobby: {val: 'football'}}

对象多层级浅拷贝就无法处理了,这种情况就需要深拷贝了。

深拷贝

深拷贝就不是简单的拷贝引用地址了,而是在堆中重新分配内存,并且把源对象的所有属性都进行新建拷贝,以保证深拷贝的对象引用不包含任何原有对象或对象上的任何对象属性,复制后的对象与原来的对象是完全隔离的。

JSON.parse()JSON.stringify()

  • 数组深拷贝 - JSON.parse()JSON.stringify()
let arr = [1, 2, [1]];
let newArr = JSON.parse(JSON.stringify(arr));
newArr[2][0] = 10;
console.log(arr, newArr); // [1, 2, [1]] [1, 2, [10]]

let arr1 = [1, 2, [1]];
let newArr1 = JSON.parse(JSON.stringify(arr1));
newArr1[2][0] = 10;
console.log(arr1, newArr1); // [1, 2, [1]] [1, 2, [10]]
  • 对象深拷贝 - JSON.parse()JSON.stringify()
let obj = {name: '橙某人', hobby: {val: 'basketball'}};
let newObj = JSON.parse(JSON.stringify(obj));
newObj.hobby.val = 'football';
console.log(obj, newObj); // {name: "yd", hobby: {val: 'basketball'}} {name: "yd", hobby: {val: 'football'}}

利用JSON.parse()JSON.stringify()是不是非常简单粗暴?哈哈,但是呢。简单就肯定有弊端了,如:

let arr = [() => {}, { b: () => {} }, new Date()];
let newArr = JSON.parse(JSON.stringify(arr));
console.log(newArr);

let obj = {a: () => {}, b: new Date()};
let newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);

可怕不? 函数不见了?日期类型变成字符串? 这就是弊端了,只能说且用且谨慎了。

递归

上面方式虽然比较简单方便,但用的时候还是要稍微想一下,省得被坑了。

下面我们就用递归来实现下深拷贝,其中的思路:在对属性值进行拷贝的时候我们先判断下一下它的类型,如果是对象,我们就递归调用深拷贝函数,如果不是就直接拷贝过去,就怎么简单。

// 深拷贝函数
function deepCopy(obj) {
  if (typeof obj !== 'object') return;
  let result = obj instanceof Array ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
    }
  }
  return result;
}

测试代码。

// 数组
let arr = [1, 2, [1]];
let newArr = deepCopy(arr);
newArr[2][0] = 10;
console.log(arr, newArr); // [1, 2, [1]] [1, 2, [10]]

let arr1 = [1, 2, [1]];
let newArr1 = deepCopy(arr1);
newArr1[2][0] = 10;
console.log(arr1, newArr1); // [1, 2, [1]] [1, 2, [10]]

// 对象
let obj = {name: 'yd', hobby: {val: 'basketball'}};
let newObj = deepCopy(obj);
newObj.hobby.val = 'football';
console.log(obj, newObj); // {name: "yd", hobby: {val: 'basketball'}} {name: "yd", hobby: {val: 'football'}}

然后我们来测试下那些特殊的函数、日期类型。

let arr = [() => {}, { b: () => {} }, new Date()];
let newArr = deepCopy(arr);
console.log(newArr);

let obj = {a: () => {}, b: new Date()};
let newObj = deepCopy(obj);
console.log(newObj);

根据上图我们发现,函数是没有问题的,还是保留下来了,但是日期就完蛋了,变了个空对象,Why? 不要着急,是深拷贝函数deepCopy()出现了问题,我们改改。

function deepCopy(obj) {
  if (typeof obj !== 'object') return;
  let result = obj instanceof Array ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = typeof obj[key] === 'object' && !(obj[key] instanceof Date) ? deepCopy(obj[key]) : obj[key];
    }
  }
  return result;
}

这下正常了,明白了吧? 嘿嘿~ 不明白的自己悟吧,本章就差不多这样子啦,拜拜咯。
(当然递归只是一种方式,网上一搜各种各样实现深拷贝的方式,绝对优雅,感兴趣可以慢慢研究一下。)