Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【再学前端】深拷贝与浅拷贝 #139

Open
Samgao0312 opened this issue Jul 6, 2022 · 0 comments
Open

【再学前端】深拷贝与浅拷贝 #139

Samgao0312 opened this issue Jul 6, 2022 · 0 comments

Comments

@Samgao0312
Copy link
Owner

Samgao0312 commented Jul 6, 2022

经常会听到有人说:

  • 简单描述JS中浅拷贝、深拷贝的区别,并列举深拷贝的几种方式?
  • 什么是深拷贝?什么是浅拷贝?
  • 你能写个深拷贝函数吗?

好吧......以免被别人鄙视,今天好好总结梳理下这部分知识。。

1、什么是深拷贝和浅拷贝?分别有哪些方式?

深拷贝和浅拷贝是只针对 Object 和 Array 这样的引用类型的。如下图所示:
image

  • 浅拷贝
    只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
    浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

常见的浅拷贝方法有:

  1. Object.assign()
  2. Es6的 ... 运算符。
  • 深拷贝
    会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
    深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

常见的深拷贝方法有:

JSON.parse(JSON.stringfy(目标对象))


2、赋值和浅拷贝的区别

2.1 赋值

我们先看一下下面这段代码:

let obj1 = {
  name: 'G-Dragon',
  age: 30,
  language: [1, [2, 3], [4, 5]]
}
let obj2 = obj1;
obj2.name = 'Louis';
obj2.language[1] = ['二', '三'];
console.log(obj1)
console.log(obj2)

输出:

{ name: 'Louis',
  age: 30,
  language: [ 1, [ '二', '三' ], [ 4, 5 ] ] }
{ name: 'Louis',
  age: 30,
  language: [ 1, [ '二', '三' ], [ 4, 5 ] ] }

结论:
当把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

2.2 浅拷贝

function shallowCopy(obj) {
  const result = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = obj[key]
    }
  }
  return result;
}
let obj1 = {
  name: 'G-Dragon',
  age: 30,
  language: [1, [2, 3], [4, 5]]
}
let obj2 = shallowCopy(obj1);
obj2.name = 'Louis';
obj2.language[1] = ['二', '三'];
console.log(obj1)
console.log(obj2)

输出:

{ name: 'G-Dragon',
  age: 30,
  language: [ 1, [ '二', '三' ], [ 4, 5 ] ] }
{ name: 'Louis',
  age: 30,
  language: [ 1, [ '二', '三' ], [ 4, 5 ] ] }

结论:
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。


3、赋值 vs 浅拷贝 vs 深拷贝

和原始数据类型是否指向同一个对象 第一层数据为基本数据类型 原始数据中包含子对象
赋值 改变使原数据一同改变 改变使原数据类型一同改变
浅拷贝 改变不会使原数据一同改变 改变使原数据类型一同改变
深拷贝 改变不会使原数据一同改变 改变不会使原数据一同改变

4、浅拷贝的实现方式

4.1 for/in遍历

let target = {
  name: 'G-Dragon',
  age: 30,
  friend: [
    {
      name: 'JiangNan',
      age: 31,
    },
  ],
  sayName: () => {
    return 'Hi, I am ' + this.name
  }
};


let shallowCopy = (obj) => {
  // 检查传入的参数是还是数组
  const result = Array.isArray(obj) ? [] : {};
  for(let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = obj[key];
    }
  }
  return result;
}
let t1 = shallowCopy(target);
let t2 = shallowCopy(target)

console.log(t1);
console.log(t2)
console.log('-------------')
t1.friend.push({
  name: '小刘医生',
  age: 27
})
console.log(t1);
console.log(t2)

上面代码输出为:

{ name: 'G-Dragon',
  age: 30,
  friend: [ { name: 'JiangNan', age: 31 } ],
  sayName: [Function: sayName] }
{ name: 'G-Dragon',
  age: 30,
  friend: [ { name: 'JiangNan', age: 31 } ],
  sayName: [Function: sayName] }

-------------

{ name: 'G-Dragon',
  age: 30,
  friend: [ { name: 'JiangNan', age: 31 }, { name: '小刘医生', age: 27 } ],
  sayName: [Function: sayName] }
{ name: 'G-Dragon',
  age: 30,
  friend: [ { name: 'JiangNan', age: 31 }, { name: '小刘医生', age: 27 } ],
  sayName: [Function: sayName] }

结论:
shallowCopy方法只拷贝了对象的一层,这只是一种浅拷贝。其实还有一些原生的方法也是只拷贝一层,比如 Object.assign... 扩展运算符

let newObj = Object.assign({}, target); // 这是一层的浅拷贝
let newObj = {...target};  // 这也是一层的浅拷贝

4.2 Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

let obj = {
  a: {
    a: 'justin',
    b: 50
  }
}
let initObj = Object.assign({}, obj)
console.log(initObj)  //{ a: { a: 'justin', b: 50 } }
initObj.a.a = 'justin_2'
console.log(obj.a.a)  //justin_2

4.3 Array.prototype.concat()

let arr = [1, 3, {
  user_name: 'G-Dragon'
}];
let arr2 = arr.concat();
console.log(arr2)  //[ 1, 3, { user_name: 'G-Dragon' } ]
arr2[2].user_name = 'wa~ wo~';
console.log(arr2) //[ 1, 3, { user_name: 'wa~ wo~' } ]
console.log(arr) //[ 1, 3, { user_name: 'wa~ wo~' } ], 表明修改新对象会改到原对象

补充:Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。

原数组的元素会按照下述规则拷贝:

  • 如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
  • 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

5、深拷贝的实现方式

5.1 JSON.parse(JSON.stringify())

let arr = [1, 3, {
  user_name: 'G-Dragon'
}, function() {}];
let arr2 = JSON.parse(JSON.stringify(arr));
arr2[2].user_name = '孙悟空';
console.log(arr)   //[ 1, 3, { user_name: 'G-Dragon' }, [Function] ]
console.log(arr2)  //[ 1, 3, { user_name: '孙悟空' }, null ]

原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse() 把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

缺点: 该方法虽然可以实现 ArrayObject 的深拷贝,但不能处理函数。这是因为 JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数。

5.2 手写递归

原理:遍历Object、Array,知道里面东都市基本数据类型,然后再去复制,就是深度拷贝。

有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。关于这块如有疑惑,请仔细阅读ConardLi大佬如何写出一个惊艳面试官的深拷贝?这篇文章。

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== 'object') return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);

另一种不太严谨的实现:

function deepClone(obj){
    let objClone = Array.isArray(obj)?[]:{};
    if(obj && typeof obj==="object"){
        for(key in obj){
            //判断是否为自身属性
            if(obj.hasOwnProperty(key)){
                //判断ojb子元素是否为对象,如果是,递归复制
                if(obj[key]&&typeof obj[key] ==="object"){
                    objClone[key] = deepClone(obj[key]);
                }else{
                    //如果不是,简单复制
                    objClone[key] = obj[key];
                }
            }
        }
    }
    return objClone;
}

5.3 函数库lodash

该函数库有提供 _.cloneDeep用来做Deep Copy;

let _ = require('lodash');
let obj1 = {
  a: 1,
  b: {
    f: {
      g: 1
    }
  },
  c: [1, 2, 3]
}
let obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f)  //false

6、参考阅读

相关文章:【积点成势】写一个函数实现深拷贝

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant