1. 浅拷贝

浅拷贝是指,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。

浅拷贝的方法

(1)直接赋值

  1. let arr1 = [1,2,3];
  2. let arr2 = arr1;
  3. new2[0] = 0;
  4. console.log(arr1); // [0, 2, 3]
  5. console.log(arr1); // [0, 2, 3]
  6. console.log(arr1 === arr2); // true

(2)Object.assign()

object.assign 是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法接受的第一个参数是目标对象,其余参数是源对象,用法:Object.assign(target, source_1, ···),该方法可以实现浅拷贝,也可以实现一维对象的深拷贝。

注意:**

  • 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。
  • 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。
  • 因为nullundefined 不能转化为对象,所以第一个参数不能为nullundefined,会报错。
  • 它不会拷贝对象的继承属性,不会拷贝对象的不可枚举的属性,可以拷贝 Symbol 类型的属性。

    1. let target = {a: 1};
    2. let object2 = {b: 2};
    3. let object3 = {c: 3};
    4. Object.assign(target,object2,object3);
    5. console.log(target); // {a: 1, b: 2, c: 3}

    (3)扩展运算符

    使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。语法:let cloneObj = { ...obj };

    1. let obj1 = {a:1,b:{c:1}}
    2. let obj2 = {...obj1};
    3. obj1.a = 2;
    4. console.log(obj1); //{a:2,b:{c:1}}
    5. console.log(obj2); //{a:1,b:{c:1}}
    6. obj1.b.c = 2;
    7. console.log(obj1); //{a:2,b:{c:2}}
    8. console.log(obj2); //{a:1,b:{c:2}}

    扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。

    (4)数组方法实现数组浅拷贝

    1)Array.prototype.slice

    slice()方法是JavaScript数组的一个方法,这个方法可以从已有数组中返回选定的元素:用法:array.slice(start, end),该方法不会改变原始数组。该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。

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

    2)Array.prototype.concat

    concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。

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

    (5)手写实现浅拷贝

    根据以上对浅拷贝的理解,实现一个浅拷贝的大致思路分为两点:

  • 对基础类型做一个最基本的一个拷贝;

  • 对引用类型开辟一个新的存储,并且拷贝一层对象属性。
    1. // 浅拷贝的实现;
    2. function shallowCopy(object) {
    3. // 只拷贝对象
    4. if (!object || typeof object !== "object") return;
    5. // 根据 object 的类型判断是新建一个数组还是对象
    6. let newObject = Array.isArray(object) ? [] : {};
    7. // 遍历 object,并且判断是 object 的属性才拷贝
    8. for (let key in object) {
    9. if (object.hasOwnProperty(key)) {
    10. newObject[key] = object[key];
    11. }
    12. }
    13. return newObject;
    14. }

    2. 深拷贝

    深拷贝是指,对于简单数据类型直接拷贝他的值,对于引用数据类型,在堆内存中开辟一块内存用于存放复制的对象,并把原有的对象类型数据拷贝过来,这两个对象相互独立,属于两个不同的内存地址,修改其中一个,另一个不会发生改变。

    (1)Object.assign()

    Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
    1. let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
    2. let obj2 = Object.assign({}, obj1);
    3. obj2.person.name = "wade";
    4. obj2.sports = 'football'
    5. console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

    (2)JSON.stringify()

    JSON.parse(JSON.stringify(obj))是目前比较常用的深拷贝方法之一,它的原理就是利用JSON.stringifyjs对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象。

这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。

  1. let obj1 = { a: 0,
  2. b: {
  3. c: 0
  4. }
  5. };
  6. let obj2 = JSON.parse(JSON.stringify(obj1));
  7. obj1.a = 1;
  8. obj1.b.c = 1;
  9. console.log(obj1); // {a: 1, b: {c: 1}}
  10. console.log(obj2); // {a: 0, b: {c: 0}}

使用该方法时,需要注意以下几点:

  • 无法拷贝不可枚举的属性;
  • 无法拷贝对象的原型链;
  • 拷贝 RegExp 引用类型会变成空对象;
  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;

无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。

(3)函数库lodash的_.cloneDeep方法

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

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

(4)手写实现深拷贝函数

  1. function clone(source) {
  2. //判断source是不是对象
  3. if (source instanceof Object == false) return source;
  4. //判断source是对象还是数组
  5. let target = Array.isArray(source) ? [] : {};
  6. for (let i in source) {
  7. if (source.hasOwnProperty(i)) {
  8. //判断数据i的类型
  9. if (typeof source[i] === 'object') {
  10. target[i] = clone(source[i]);
  11. } else {
  12. target[i] = source[i];
  13. }
  14. }
  15. }
  16. return target;
  17. }
  18. console.log(clone({b: {c: {d: 1}}})); // {b: {c: {d: 1}}})

虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringfy 一样,还是有一些问题没有完全解决,例如:

  • 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;
  • 这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;
  • 对象的属性里面成环,即循环引用没有解决。

    3. 解决递归爆栈

    我们使用递归的方法对数据进行拷贝,但是这也会出现一个问题,递归的深度的深度太深就会引发栈内存的溢出,我们使用下面的方法来解决递归爆栈的问题:将待拷贝的对象放入栈中,循环直至栈为空。

    1. function cloneLoop(x) {
    2. const root = {};
    3. // 栈
    4. const loopList = [
    5. {
    6. parent: root,
    7. key: undefined,
    8. data: x,
    9. }
    10. ];
    11. while(loopList.length) {
    12. // 深度优先
    13. const node = loopList.pop();
    14. const parent = node.parent;
    15. const key = node.key;
    16. const data = node.data;
    17. // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
    18. let res = parent;
    19. if (typeof key !== 'undefined') {
    20. res = parent[key] = {};
    21. }
    22. for(let k in data) {
    23. if (data.hasOwnProperty(k)) {
    24. if (typeof data[k] === 'object') {
    25. // 下一次循环
    26. loopList.push({
    27. parent: res,
    28. key: k,
    29. data: data[k],
    30. });
    31. } else {
    32. res[k] = data[k];
    33. }
    34. }
    35. }
    36. }
    37. return root;
    38. }

    这样我们就解决了递归爆栈的问题,但是循环引用的问题依然存在。

    4. 解决循环引用

    举例:当a对象的中的某属性值为a对象,这样就会造成循环引用。
    我们使用暴力破解的方法来解决循环引用的问题。
    思路:引入一个数组uniqueList用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在uniqueList中了,如果在的话就不执行拷贝逻辑了

    1. function cloneForce(x) {
    2. const uniqueList = []; // 用来去重
    3. let root = {};
    4. const loopList = [
    5. {
    6. parent: root,
    7. key: undefined,
    8. data: x,
    9. }
    10. ];
    11. while(loopList.length) {
    12. const node = loopList.pop();
    13. const parent = node.parent;
    14. const key = node.key;
    15. const data = node.data;
    16. // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
    17. let res = parent;
    18. if (typeof key !== 'undefined') {
    19. res = parent[key] = {};
    20. }
    21. // 数据已经存在
    22. let uniqueData = find(uniqueList, data);
    23. if (uniqueData) {
    24. parent[key] = uniqueData.target;
    25. continue;
    26. }
    27. // 数据不存在
    28. // 保存源数据,在拷贝数据中对应的引用
    29. uniqueList.push({
    30. source: data,
    31. target: res,
    32. });
    33. for(let k in data) {
    34. if (data.hasOwnProperty(k)) {
    35. if (typeof data[k] === 'object') {
    36. loopList.push({
    37. parent: res,
    38. key: k,
    39. data: data[k],
    40. });
    41. } else {
    42. res[k] = data[k];
    43. }
    44. }
    45. }
    46. }
    47. return root;
    48. }
    49. //find函数用来遍历uniqueList
    50. function find(arr, item) {
    51. for(let i = 0; i < arr.length; i++) {
    52. if (arr[i].source === item) {
    53. return arr[i];
    54. }
    55. }
    56. return null;
    57. }

    5. 总结

  • 浅拷贝:浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用 Object.assign 和展开运算符来实现。

  • 深拷贝:深拷贝相对浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用 JSON 的两个函数来实现,但是由于 JSON 的对象格式比 js 的对象格式更加严格,所以如果属性值里边出现函数或者 Symbol 类型的值时,会转换失败 ```javascript // 浅拷贝的实现;

function shallowCopy(object) { // 只拷贝对象 if (!object || typeof object !== “object”) return;

// 根据 object 的类型判断是新建一个数组还是对象 let newObject = Array.isArray(object) ? [] : {};

// 遍历 object,并且判断是 object 的属性才拷贝 for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = object[key]; } }

return newObject; }

// 深拷贝的实现;

function deepCopy(object) { if (!object || typeof object !== “object”) return;

let newObject = Array.isArray(object) ? [] : {};

for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = typeof object[key] === “object” ? deepCopy(object[key]) : object[key]; } }

return newObject; } ```