8、探秘:深拷贝与浅拷贝

8、深拷贝与浅拷贝

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

深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

8.1 浅拷贝的实现

1. Object.assign()

首先可以通过 Object.assign 来解决这个问题,很多人认为这个函数是用来深拷贝的。其实并不是,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

let a = {
  age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

2. 函数库lodash的_.clone方法

该函数库也有提供_.clone用来做 Shallow Copy,后面我们会再介绍利用这个库实现深拷贝。

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

3. 展开运算符…

通过展开运算符 ... 来实现浅拷贝
展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。

let a = {
  age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1

let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }

4. Array.prototype.concat()
5. Array.prototype.slice()

利用数组API

let arr = [1, 3, {
    username: 'kobe'
    }];
let arr2 = arr.concat();
//let arr3 = arr.slice();
arr2[2].username = 'wade';
//arr3[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
8.2 深拷贝的实现

1.JSON.parse(JSON.stringify())

通常可以通过 JSON.parse(JSON.stringify(object)) 来解决
这是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

该方法的局限性在于:

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象

MDN-stringify

JSON.stringify() 方法将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。

console.log(JSON.stringify({ x: 5, y: 6 }));
// expected output: "{"x":5,"y":6}"

console.log(JSON.stringify([new Number(3), new String('false'), new Boolean(false)]));
// expected output: "[3,"false",false]"

console.log(JSON.stringify({ x: [10, undefined, function(){}, Symbol('')] }));
// expected output: "{"x":[10,null,null,null]}"

console.log(JSON.stringify(new Date(2006, 0, 2, 15, 4, 5)));
// expected output: ""2006-01-02T15:04:05.000Z""

MDN-parse

JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。提供可选的 reviver 函数用以在返回之前对所得到的对象执行变换(操作)。

const json = '{"result":true, "count":42}';
const obj = JSON.parse(json);

console.log(obj.count);
// expected output: 42

console.log(obj.result);
// expected output: true

2. 函数库lodash的_.cloneDeep方法

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

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

根据拷贝的层级不同可以分为浅拷贝和深拷贝,浅拷贝就是只进行一层拷贝,深拷贝就是无限层级拷贝

实现浅拷贝

//浅拷贝
function shallowClone(target) {
  let cloneTarget = {};
  for (const key in target) {
    cloneTarget[key] = target[key];
  }
  return cloneTarget;
}

实现深拷贝

深拷贝的问题其实可以分解成两个问题,浅拷贝+递归

//深拷贝
function deepClone(target) {
  if (typeof target === 'object') {
    let cloneTarget = {};
    for (const key in target) {
      cloneTarget[key] = deepClone(target[key]);
    }
    return cloneTarget;
  } else {
    return target;
  }
}

提升

解决上述深拷贝的问题

  • 没有对参数做检验,判断是否对象的逻辑不够严谨

首先我们需要抽象一个判断对象的方法,其实比较常用的判断对象的方法如下

//抽象一个判断对象的方法即可解决
function isObject(x) {
  return Object.prototype.toString.call(x) === '[object Object]'
}

function deepClone(target) {
  // 使用isObject对参数做检验,判断是否对象
  if (isObject(target)) {
    let cloneTarget = {};
    for (const key in target) {
      cloneTarget[key] = deepClone(target[key]);
    }
    return cloneTarget;
  } else {
    return target;
  }
}
  • 没有考虑数组
function isObject(x) {
  return Object.prototype.toString.call(x) === '[object Object]'
}

function deepClone(target) {
  // 使用isObject对参数做检验,判断是否对象
  if (isObject(target)) {
    // 考虑数组
    let cloneTarget = Array.isArray(target) ? [] : {};
    for (const key in target) {
      cloneTarget[key] = deepClone(target[key]);
    }
    return cloneTarget;
  } else {
    return target;
  }
}
//测试
const target = {
  field1: 1,
  field2: undefined,
  field3: {
    child: 'child'
  },
  field4: [2, 4, 8]
};
var cloneTarget = deepClone(target)
  • 无法解决循环引用
  • 解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系
  • 当需要拷贝当前对象时,先去存储空间map中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
  • 这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:

  • 检查map中有无克隆过的对象
    有 - 直接返回
    没有 - 将当前对象作为key,克隆对象作为value进行存储

  • 继续克隆

//代码
function isObject(x) {
  return Object.prototype.toString.call(x) === '[object Object]'
}

function deepClone(target, map = new Map()) {
  // 使用isObject对参数做检验,判断是否对象
  if (isObject(target)) {
    // 考虑数组
    let cloneTarget = Array.isArray(target) ? [] : {};
	//使用map解决循环引用问题
    if (map.get(target)) {
      return map.get(target);
    }
    map.set(target, cloneTarget);

    for (const key in target) {
      cloneTarget[key] = deepClone(target[key], map);
    }
    return cloneTarget;
  } else {
    return target;
  }
}

//测试用例
const target = {
  field1: 1,
  field2: undefined,
  field3: {
    child: 'child'
  },
  field4: [2, 4, 8]
};
target.target = target

var cloneTarget = deepClone(target)
console.log(cloneTarget)

输出:target属性,变为了一个Circular类型,即循环引用的意思。
在这里插入图片描述

  • 递归爆栈
参考文章
  1. 如何写出一个惊艳面试官的深拷贝?
  2. 深拷贝的终极探索(99%的人都不知道)
  3. 浅拷贝与深拷贝
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值