深拷贝和浅拷贝

73 阅读11分钟

深拷贝与浅拷贝

浅拷贝

拷贝的是对象的指针,修改内容会互相影响

如果属性是基本类型,拷贝的就是基本类型的值

如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

简单赋值

即简单的将一个对象赋值给另外一个对象

 let obj1 = {
     a: 1,
     b: 2
 }
 let obj2 = obj1
 obj2.b = 3
 console.log(obj1)  // { a: 1, b: 3 }
 console.log(obj1 === obj2) //true

Object.assign

Object.assign() 只复制属性值,如果属性值是一个对象的话,那么只会引用这个对象的指针

 let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
 let obj2 = Object.assign({}, obj1);
 obj2.person.name = "wade";
 obj2.sports = 'football'
 console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

loadash_.clone方法

 var objects = [{ 'a': 1 }, { 'b': 2 }];
  
 var shallow = _.clone(objects);
 console.log(shallow[0] === objects[0]); //true

展开运算符...

assign一样,对于属性值是对象的属性,他只会引用其属性值的指针

 let obj1 = {
     a: 1,
     c: {
         d: 4
     }
 }
 ​
 let obj2 = {...obj1}
 console.log(obj1 === obj2)  //false
 console.log(obj2.c === obj1.c);     //true

concat方法

concat() 方法用于合并两个或多个数组

此方法不会更改现有数组,而是返回一个新数组

 let arr = [1, 3, {
     username: 'kobe'
 }];
 let arr2 = arr.concat();    
 arr2[2].username = 'wade';
 console.log(arr); //[ 1, 3, { username: 'wade' } ]

slice方法

slice() 方法返回一个新的数组对象

这一对象是一个由 beginend 决定的原数组的浅拷贝(包括 begin,不包括end),原始数组不会被改变

 let arr = [1, 3, {
     username: ' kobe'
 }];
 let arr3 = arr.slice();
 arr3[2].username = 'wade'
 console.log(arr); // [ 1, 3, { username: 'wade' } ]

深拷贝

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

使用JSON.stringify

lodash_.cloneDeep方法

手写

基础

首先,我们先写出一个能够实现浅拷贝的函数

思路:就是使用循环将原对象上的属性一个一个添加到新的克隆对象上

 function clone(target) {
     //创建一个克隆的对象
     let cloneTarget = {}
     //使用for...in把所有需要克隆的属性都添加到cloneTarget中
     for(const key in target){
         cloneTarget[key] = target[key]
     }
     //返回克隆对象
     return cloneTarget
 }

在深拷贝中,我们需要解决的第一个问题就是需要考虑拷贝的深度,我们目前只是拷贝到第一层,所以我们可以采用递归的方式来解决

  • 如果是原始数据,则直接返回即可
  • 如果是引用类型,则需要创建一个新的对象,遍历需要克隆的对象,添加属性值,如果又遇到引用类型则递归即可
 function clone(target) {
     //判断是不是引用类型,是的话则需要递归生成新对象
     if(typeof target === 'object'){
         //创建一个克隆的对象
         let cloneTarget = {}
         //使用for...in把所有需要克隆的属性都添加到cloneTarget中
         for(const key in target){
             //对每一个属性值进行检查,是引用类型则进一步递归遍历处理
             cloneTarget[key] = clone(target[key])
         }
         //返回克隆对象
         return cloneTarget
     }else{
         //是原始类型则直接返回
         return target
     }
 }

考虑数组

现在的代码只适用于对象,但是数组与对象同样都是引用数据类型,所以可以用同样的思路实现数组深拷贝

思路:将克隆出来的产物cloneTarget根据数据类型产出即可

 function clone(target) {
     if(typeof target === 'object'){
         //创建一个克隆的对象或数组
         let cloneTarget = Array.isArray(target) ? [] : {}
         for(const key in target){
             cloneTarget[key] = clone(target[key])
         }
         return cloneTarget
     }else{
         return target
     }
 }

循环引用

这个问题也是存在于JSON.stringify()这个方法中

循环引用也就是自身引用了自身,例如:

 const obj = {a: 1, b: 2}
 obj.obj = obj   //自身引用了自身

此时如果用我们之前的函数去拷贝这样一个对象,那么就会出现爆栈的情况

 Uncaught RangeError: Maximum call stack size exceeded

解决思路:可以先开辟一块内存空间存储当前对象和拷贝对象的对应关系需要拷贝对象的时候,可以先去存储空间中找如果有这个对象则直接返回,如果没有则继续拷贝

现在这个存储空间,由于存储的是键值对关系的集合,所以我们可以考虑使用**Map数据结构**,拷贝对象之前先在这个Map数据结构中寻找即可

  • 找到:直接返回
  • 找不到:将当前对象和克隆对象存储起来,然后继续克隆
 function clone(target, map = new Map()) {
     if(typeof target === 'object'){
         let cloneTarget = Array.isArray(target) ? [] : {}
         //检查该对象有没有被克隆过
         if(map.get(target)){
             //如果被克隆过则直接返回该对象
             return target
         }
         //如果没有的话则需要新建键值对存储起来
         map.set(target, cloneTarget)
         for(const key in target){
             //并且把当前的map数据结构传递给下一个递归过程
             cloneTarget[key] = clone(target[key], map)
         }
         return cloneTarget
     }else{
         return target
     }
 }

到这里,我们的代码已经能够解决循环引用的问题了,那么现在我们要对其优化一下

我们使用了Map数据结构进行存储,我们可以改用WeakMap数据结构代替Map

WeakMap的特点:

WeakMap对象是一组键/值对的集合,其中的键是弱引用的

其键必须是对象,而值可以是任意的

弱引用:

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收

如果我们现在创建一个对象{},那么只有我们手动将其赋值为null的时候,才会被垃圾回收机制进行回收

而弱引用对象,垃圾回收机制则会自动帮我们回收

所以当我们拷贝的对象十分庞大的时候,使用Map会造成很大的额外消耗,需要我们手动清除其属性才能释放这块内存,而使用WeakMap则可以自动帮我们回收垃圾

性能优化

在遍历中,我们使用了for...in循环,但实际上,for...in循环的效率是最低的,而**while循环和for循环的效率最高**,所以此处采用while循环

for...in循环效率最低:

  • for...in不仅会遍历数组的元素,而且还会去每一个元素的原型上面寻找属性
  • for...in还要判断一个属性是不是枚举属性,是的话则不遍历
  • for...in还会要求按特定的顺序输出属性,先数字,再按字典顺序输出string属性(不会获取Symbol属性)

首先,我们新建一个遍历函数去模拟for...in循环

 function forEach(array, iteratee) {
     //设置索引
     let index = -1
     //获取数组长度
     const length = array.length
     //使用while循环代替for...in
     while (++index < length) {
         //由于for...in能自动提取出键,所以我们要模拟他的功能
         //此处我们传入一个回调函数即可,参数将数组的项和索引传进入
         iteratee(array[index], index)
     }
     //将原数组返回
     return array
 }

然后,修改clone函数中的循环逻辑即可

 function clone(target, map = new WeakMap()) {
     if (typeof target === 'object') {
         //记录传入的是数组还是对象
         const isArray = Array.isArray(target)
         let cloneTarget = isArray ? [] : {}
         if (map.get(target)) {
             return target
         }
         map.set(target, cloneTarget)
         //如果是对象,要为他创建对应的keys集合
         const keys = isArray ? undefined : Object.keys(target)
         //使用我们的forEach去代替for...in优化性能,将属性拷贝到克隆对象中
         forEach(keys || target, (value, key) => {
             //如果是对象的话,我们需要指定键为value,不然此处遍历出来的是数字
             if (keys) key = value
             //对每一个属性值进行检查,是引用类型则进一步递归遍历处理
             //并且把当前的map数据结构传递给下一个递归过程
             cloneTarget[key] = clone(target[key], map)
         })
         return cloneTarget
     } else {
         return target
     }
 }

测试用例:

 const target = {
     field1: 1 ,
     field2: undefined,
     field3: { child: 'child'},
     field4: [2, 4, 8],
     f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: {} } } } } } } } } } } },
 };
 target.target = target;

获取对象准确的类型

在上述的克隆函数中,我们只考虑了objectarray两种数据类型

而在判断object的时候,我们只使用了typeof去检查一个对象,但是这样的检查往往是不严谨的,因为没有考虑到nullfunction,所以我们要重新判断object

此处将判断object的代码提取出来,作为一个**isObject函数**

 function isObject(target) {
     //检查target的类型
     const type = typeof target
     //考虑null和function的情况
     return target !== null && (type === 'object' || type === 'function')
 }

然后再修改一下clone函数即可

 function clone(target, map = new WeakMap()) {
     //判断是不是引用类型,是的话则需要递归生成新对象
     if (isObject(target)) {
         /** 省略 */
     } else {
         //是原始类型则直接返回
         return target
     }
 }
 ​

考虑其他数据类型

我们可以使用toString方法来获取准确的引用类型

每一个引用类型都有toString方法,因为这个方法被每个Object对象继承,会返回[[object type]],但是很有可能这个方法在自定义对象中被覆盖,导致不能获取到对象类型type

既然如此,我们可以直接调用Object原型上的toString方法再修改一个this指向即可

我们将这个提取引用类型的逻辑提取出来:

 function getType(target) {
     //调用Object原型对象上的toString
     return Object.prototype.toString.call(target)
 }

这样每一个对象都可以正确的判断出他的类型,如下表:

调用结果
Object.prototype.toString.call(true)[object Boolean]
Object.prototype.toString.call(123)[object Number]
Object.prototype.toString.call('aaa')[object String]
Object.prototype.toString.call(null)[object Null]
Object.prototype.toString.call(undefined)[object undefined]
Object.prototype.toString.call(Symbol())[object Symbol]
Object.prototype.toString.call({})[object Object]
Object.prototype.toString.call(function(){})[object Function]
Object.prototype.toString.call([])[object Array]
Object.prototype.toString.call(new Error())[object Error]
Object.prototype.toString.call(new ReqExp())[object ReqExp]
Object.prototype.toString.call(Math)[object Math]
Object.prototype.toString.call(JSON)[object JSON]
Object.prototype.toString.call(window)[object golbal]

为了后续使用方便,我们抽取其中的一些常用数据类型:

 //可以继续遍历的类型
 const mapTag = '[object Map]'
 const setTag = '[object Set]'
 const arrayTag = '[object Array]'
 const objectTag = '[object Object]'
 ​
 //不可以继续遍历的类型
 const boolTag = '[object Boolean]'
 const dateTag = '[object Date]'
 const errorTag = '[object Error]'
 const numberTag = '[object Number]'
 const stringTag = '[object String]'
 const regexpTag = '[object RegExp]'
 const symbolTag = '[object Symbol]'

处理可继续遍历的类型

可继续遍历的类型:内存中还可以存储引用数据类型的数据

我们已经实现objectArray这两个可继续遍历的类型了,另外我们再实现以下MapSet这两个数据结构

现在我们就需要根据传入的target判断数据结构,然后再根据数据类型创建克隆对象

这里我们可以通过一个小技巧:使用constructor方式获取,就可以获取到它的构造函数,再使用这个构造函数进行创建实例,就可以创建出新的克隆对象,而且这个对象还可以保留原型对象上的数据

 function getInit(target){
     const Ctor = target.constructor
     return new Ctor()
 }

修改克隆函数,加上处理mapset的逻辑

 function clone(target, map = new WeakMap()) {
     //判断是不是引用类型,不是的话则直接返回
     if (!isObject(target)) return target
     const isArray = Array.isArray(target)
     //获取当前数据的详细类型
     const type = getType(target)
     //创建克隆对象
     let cloneTarget
     //检查是不是可以继续遍历的类型
     if (deepTag.includes(type)) {
         //创建对应的数据类型实例赋值给克隆对象
         cloneTarget = getInit(target)
     }
     if (map.get(target)) {
         return target
     }
     map.set(target, cloneTarget)
 ​
     //克隆Set数据结构
     if (type === setTag) {
         target.forEach(value => {
             //往克隆对象中添加每一个值,并检查是不是引用数据类型
             cloneTarget.add(clone(value))
         })
         //返回克隆的Set数据
         return cloneTarget
     }
 ​
     //克隆Map数据结构
     if (type === mapTag) {
         target.forEach((value, key) => {
             //往克隆对象中添加每一个值,并检查是不是引用数据类型
             cloneTarget.set(key, clone(value))
         })
         //返回克隆的Map数据
         return cloneTarget
     }
 ​
     const keys = isArray ? undefined : Object.keys(target)
     forEach(keys || target, (value, key) => {
         if (keys) key = value
         cloneTarget[key] = clone(target[key], map)
     })
     return cloneTarget
 }

测试用例:

 const map = new Map([['aaa', 'AAA'], ['bbb', 'BBB']])
 const set = new Set(['ccc', 'ddd'])
 const target = {
     field1: 1,
     field2: undefined,
     field3: { child: 'child' },
     field4: [2, 4, 8],
     empty: null,
     map,
     set,
 }
 ​
 const obj = clone(target)
 target.map.set('hhh', '哈哈哈')
 target.set.add('hhh')
 console.log(obj, target);

处理不可继续遍历的类型

剩余的类型我们将其处理为不可继续遍历的类型:

 function cloneOtherType(target, type) {
     const Ctor = target.constructor
     switch (type){
         case boolTag:
         case numberTag:
         case stringTag:
         case errorTag:
         case dateTag:
             return new Ctor(target)
         default:
             return null
     }
 }

克隆函数

虽然克隆函数并没有什么实际的应用场景,两个对象使用同一个地址中的函数也是没有问题的,所以一般不用处理

在函数库lodash库中就没有对其做处理,而是直接返回

但是如果我们一定要克隆一个函数,也不是没有办法的

首先,函数分为两种:普通函数和箭头函数,而两者可以用有无prototype属性来区分

先处理普通函数:使用正则取出函数体和参数,然后使用new Function()构建即可

再处理箭头函数:直接使用eval函数即可

 function cloneFunction(func){
     //获取函数体的正则判断
     const bodyReg = /(?<={)([\s\S]*)(?=})/
     //获取参数的正则判断
     const paramReg = /(?<=().+(?=))/
     //将函数转化成字符串
     const funcString = func.toString()
     //判断有没有prototype,有的话则为普通函数
     if(func.prototype){
         //提取参数
         const param = paramReg.exec(funcString)
         //提取函数体
         const body = bodyReg.exec(funcString)
         //如果有函数体
         if(body){
             //有参数则处理参数在构建函数,没有则直接构建
             if(param){
                 const paramArr = param[0].split(',')
                 return new Function(...paramArr, body[0])
             }else{
                 return new Function(body[0])
             }
         }else{
             //没有函数体则返回null
             return null
         }
     }else{
         //处理箭头函数
         return eval(funcString)
     }
 }

最终版本

 //可以继续遍历的类型
 const mapTag = '[object Map]'
 const setTag = '[object Set]'
 const arrayTag = '[object Array]'
 const objectTag = '[object Object]'
 const deepTag = [mapTag, setTag, arrayTag, objectTag]
 ​
 //不可以继续遍历的类型
 const boolTag = '[object Boolean]'
 const dateTag = '[object Date]'
 const errorTag = '[object Error]'
 const numberTag = '[object Number]'
 const stringTag = '[object String]'
 const regexpTag = '[object RegExp]'
 const symbolTag = '[object Symbol]'
 const funcTag = '[object Function]'
 ​
 //遍历函数,模拟for...in
 function forEach(array, iteratee) {
     //设置索引
     let index = -1
     //获取数组长度
     const length = array.length
     //使用while循环代替for...in
     while (++index < length) {
         //由于for...in能自动提取出键,所以我们要模拟他的功能
         //此处我们传入一个回调函数即可,参数将数组的项和索引传进入
         iteratee(array[index], index)
     }
     //将原数组返回
     return array
 }
 ​
 //判断target的类型是不是object
 function isObject(target) {
     //检查target的类型
     const type = typeof target
     //考虑null和function的情况
     return target !== null && (type === 'object' || type === 'function')
 }
 ​
 //获取数据类型
 function getType(target) {
     //调用Object原型对象上的toString
     return Object.prototype.toString.call(target)
 }
 ​
 //创建克隆对象
 function getInit(target) {
     //获取初始化数据target的构造函数
     const Ctor = target.constructor
     //构造新的实例
     return new Ctor()
 }
 ​
 //处理不可继续遍历的类型
 function cloneOtherType(target, type) {
     const Ctor = target.constructor
     switch (type){
         case boolTag:
         case numberTag:
         case stringTag:
         case errorTag:
         case dateTag:
             return new Ctor(target)
         case funcTag:
             return cloneFunction(target)
         default:
             return null
     }
 }
 ​
 //克隆函数
 function cloneFunction(func){
     //获取函数体的正则判断
     const bodyReg = /(?<={)([\s\S]*)(?=})/
     //获取参数的正则判断
     const paramReg = /(?<=().+(?=))/
     //将函数转化成字符串
     const funcString = func.toString()
     //判断有没有prototype,有的话则为普通函数
     if(func.prototype){
         //提取参数
         const param = paramReg.exec(funcString)
         //提取函数体
         const body = bodyReg.exec(funcString)
         //如果有函数体
         if(body){
             //有参数则处理参数在构建函数,没有则直接构建
             if(param){
                 const paramArr = param[0].split(',')
                 return new Function(...paramArr, body[0])
             }else{
                 return new Function(body[0])
             }
         }else{
             //没有函数体则返回null
             return null
         }
     }else{
         //处理箭头函数
         return eval(funcString)
     }
 }
 ​
 function clone(target, map = new WeakMap()) {
     //判断是不是引用类型,不是的话则直接返回
     if (!isObject(target)) return target
     //记录传入的是数组还是对象
     const isArray = Array.isArray(target)
     //获取当前数据的详细类型
     const type = getType(target)
     //创建克隆对象
     let cloneTarget
     //检查是不是可以继续遍历的类型
     if (deepTag.includes(type)) {
         //创建对应的数据类型实例赋值给克隆对象
         cloneTarget = getInit(target)
     }else{
         //处理不可继续遍历的类型
         return cloneOtherType(target, type)
     }
     //检查该对象有没有被克隆过
     if (map.get(target)) {
         //如果被克隆过则直接返回该对象
         return target
     }
     //如果没有的话则需要新建键值对存储起来
     map.set(target, cloneTarget)
 ​
     /**
      * 克隆Set数据结构
      */
     if (type === setTag) {
         target.forEach(value => {
             //往克隆对象中添加每一个值,并检查是不是引用数据类型
             cloneTarget.add(clone(value))
         })
         //返回克隆的Set数据
         return cloneTarget
     }
 ​
     /**
      * 克隆Map数据结构
      */
     if (type === mapTag) {
         target.forEach((value, key) => {
             //往克隆对象中添加每一个值,并检查是不是引用数据类型
             cloneTarget.set(key, clone(value))
         })
         //返回克隆的Map数据
         return cloneTarget
     }
 ​
     /**
      * 克隆数组和对象
      */
     //如果是对象,要为他创建对应的keys集合
     const keys = isArray ? undefined : Object.keys(target)
     //使用我们的forEach去代替for...in优化性能,将属性拷贝到克隆对象中
     forEach(keys || target, (value, key) => {
         //如果是对象的话,我们需要指定键为value,不然此处遍历出来的是数字
         if (keys) key = value
         //对每一个属性值进行检查,是引用类型则进一步递归遍历处理
         //并且把当前的map数据结构传递给下一个递归过程
         cloneTarget[key] = clone(target[key], map)
     })
     //返回克隆对象或数组
     return cloneTarget
 }

\