一文搞定浅拷贝和深拷贝

132 阅读4分钟

前言

JS 的数据类型主要分为两类:基础数据类型和引用数据类型。

基础类型的数据直接存放在栈内存中;引用类型的数据存放堆内存中,栈内存中存放该数据的内存地址(指针)。

let a = 'first';
let arr1 = [1, 2, '3'];
let obj = { a: 'second', b: 16, c: a, d: arr1};
a = 'first1';
console.log(obj)  // { a: 'second', b: 16, c: 'first', d: [1, 2, "3"]}

arr1.push('44');  // 修改数组 arr1 的值
// arr1 的值改变后,obj中的属性 d 的值也发生了变化
console.log(obj)  // { a: 'second', b: 16, c: 'first', d: [1, 2, "3", "44"]}
// 直接修改 obj.d
obj.d.push(55)
// 导致 arr1 的值也发生了变化
console.log(arr1);  // [1, 2, "3", "44", 55]

改变引用类型数据的值时候,凡是引用该值的变量,也会发生改变。

看了上面的例子,就会发现,假如我们的变量指向同一个内存地址,修改它的值,就会影响到所有引用它的变量。 为了解决上面的问题,我们就需要对值做浅拷贝shallow clone)或者深拷贝(deep clone)。

什么是浅拷贝深拷贝

浅拷贝深拷贝主要是针对Object(包含函数、RegexDate)和Array的。

浅拷贝只拷贝指向某个对象的内存地址(指针),而不是复制对象本身,新旧对象共享一个内存地址,修改任意一个都会影响到另外一个。 浅拷贝会创建一个新的对象,对原始对象的属性进行逐一的拷贝。如果原始对象的属性是基础类型的话,拷贝就是属性的值;如果原始对象的属性是引用类型的话,拷贝的是就是内存地址,因此修改后会相互影响。

深拷贝会额外创造一个一模一样的对象,新旧对象不共享同一个内存地址,修改不会相互影响。

浅拷贝的实现

浅拷贝只能实现一层的拷贝,无法进行深层次的拷贝

对象浅拷贝

1. 展开运算符

let obj = {
  a: '',
  b: null,
  c: undefined,
  d: ['1', 2, 3, { a: 12 }],
};
let shallowCloneObj = {...obj};
console.log(shallowCloneObj)

2. Object.assign()

let obj = {
  a: '',
  b: null,
  c: undefined,
  d: ['1', 2, 3, { a: 12 }],
};
let shallowCloneObj = Object.assign(obj);
console.log(shallowCloneObj)

3. for..in 遍历

function shallowClone(obj) {
  if (!isObject(obj)) return obj;

  const cloneObj = Array.isArray(obj) ? [] : {};

  for (let prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      cloneObj[prop] = obj[prop];
    }
  }
  return cloneObj;
};

4. 依次赋值

let obj = {
  a: '',
  d: ['1', 2, 3, { a: 12 }],
};
let shallowCloneObj = {};
shallowCloneObj.a = obj.a
shallowCloneObj.d = obj.d

数组浅拷贝

Array.prototype 中的一些能够返回一个新数组的方法,都可以看做是浅拷贝

1. Array.prototype.slice()

let arr = [1, 2, 3, 4];

console.log(arr.slice(0))

2. Array.prototype.concat()

let arr = [1, 2, 3, 4];
console.log(arr.concat([]))

3. Array.prototype.map()

let arr = [1, 2, 3, 4];
console.log(arr.map((ele) => ele))

3. Array.prototype.filter()

let arr = [1, 2, 3, 4];
console.log(arr.filter(() => true))

4. 展开运算符

let arr = [1, 2, 3, 4];
console.log([...arr])

5. 依次赋值

值比较简单的时候可以使用

let arr = [1, 2];
let shallowCloneArr = [arr[0], arr[1]]

手写实现浅拷贝

// 判断是否是对象
const isObject = (obj) => typeof obj === 'object' && obj !== null;

const shallowClone = (obj) => {
  if (!isObject(obj)) return obj;

  const cloneObj = Array.isArray(obj) ? [] : {};

  for (let prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      cloneObj[prop] = obj[prop];
    }
  }
  return cloneObj;
}

深拷贝的实现

JSON.stringfy()

JSON.stringfy() 是前端很常用的深拷贝方式,把对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,然后用 JSON.parse()JSON 字符串生成一个新的对象。

let obj = {
      a: '',
      b: null,
      c: undefined, // undefined
      d: ['1', 2, 3, { a: 12 }],
      e: {m: 5, n: '66'},
      m: new Date(),  // Date
      n: NaN,
      f: function fn(){ console.log('11') }, // 函数
      r: new RegExp('/d'), // RegExp
      s: Symbol(66), // Symbol
      [Symbol('test')]: 1,
    }

    // 定义不可枚举的属性
    Object.defineProperty(obj, 'innumerable', { enumerable: false, value: 'innumerable'});

    console.log(JSON.parse(JSON.stringify(obj)));

    /** 结果如下
    a: ""
    b: null
    d: (4) ["1", 2, 3, {…}]
    e: {m: 5, n: "66"}
    m: "2021-09-09T11:42:05.771Z",
    n: null
    r: {}
  **/

但是有个问题需要注意:

  1. 拷贝的对象的值中如果有 函数undefinedSymbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失
  2. 如果是 RegExp 对象,会变成 {}
  3. 如果是 Date 会转为字符串
  4. NaN会转为 null
  5. 不可枚举的属性(enumerable: false),无法拷贝

lodash.cloneDeep()

源码:

lodash.cloneDeep()

手写递归实现

function cloneDeep(target) {
  // 判断是否为对象
  if (!typeof targe === 'object') {
    return target;
  }
  // 定义一个空的对象
  let newObj = Array.isArray(target) ? [] : {};

  for(let key in target) {
    newObj[key] = typeof target[key] === 'object' ?  cloneDeep(target[key]) : target[key]
  }
  return newObj;
}

思考

赋值和浅拷贝的不同之处

  1. 如果是基础类型数据的话,可以说赋值和浅拷贝一样的,
  2. 如果是引用类型数据的话,赋值赋的其实是该对象的在栈中的地址,而不是堆中的数据,两个数据相互影响,任何一个发生变化,另外一个也会变化;浅拷贝会创建一个新的对象,对原始对象的属性进行逐一的拷贝。如果原始对象的属性是基础类型的话,拷贝就是属性的值;如果原始对象的属性是引用类型的话

结语

如果这篇文章帮到了你,欢迎点赞👍和关注⭐️。

文章如有错误之处,希望在评论区指正🙏🙏。