关于前端:教你实现一个深浅拷贝

6次阅读

共计 7020 个字符,预计需要花费 18 分钟才能阅读完成。

浅拷贝的原理与实现

对于浅拷贝的定义咱们能够初步了解为:

本人创立一个新的对象,来承受你要从新复制或援用的对象值。如果对象属性是根本的数据类型,复制的就是根本类型的值给新对象;但如果属性是援用数据类型,复制的就是内存中的地址,如果其中一个对象扭转了这个内存中的地址,必定会影响到另一个对象。

上面我总结了一些 JavaScript 提供的浅拷贝办法,一起来看看哪些办法能实现上述定义所形容的过程。

办法一:object.assign

object.assign 是 ES6 中 object 的一个办法,该办法能够用于 JS 对象的合并等多个用处,其中一个用处就是能够进行浅拷贝。该办法的第一个参数是拷贝的指标对象,前面的参数是拷贝的起源对象(也能够是多个起源)。

object.assign 的语法为:Object.assign(target, …sources)

object.assign 的示例代码如下:

let target = {};
let source = {a: { b: 1} };
Object.assign(target, source);
console.log(target); // {a: { b: 1} };

从下面的代码中能够看到,通过 object.assign 咱们确实简略实现了一个浅拷贝,“target”就是咱们新拷贝的对象,上面再看一个和下面不太一样的例子。

let target = {};
let source = {a: { b: 2} };
Object.assign(target, source);
console.log(target); // {a: { b: 10} }; 
source.a.b = 10; 
console.log(source); // {a: { b: 10} }; 
console.log(target); // {a: { b: 10} };

从下面代码中咱们能够看到,首先通过 Object.assign 将 source 拷贝到 target 对象中,而后咱们尝试将 source 对象中的 b 属性由 2 批改为 10。通过控制台能够发现,打印后果中,三个 target 里的 b 属性都变为 10 了,证实 Object.assign 临时实现了咱们想要的拷贝成果。

然而应用 object.assign 办法有几点须要留神:

  • 它不会拷贝对象的继承属性;
  • 它不会拷贝对象的不可枚举的属性;
  • 能够拷贝 Symbol 类型的属性。

能够简略了解为:Object.assign 循环遍历原对象的属性,通过复制的形式将其赋值给指标对象的相应属性,来看一下这段代码,以验证它能够拷贝 Symbol 类型的对象。

let obj1 = {a:{ b:1}, sym:Symbol(1)}; 

Object.defineProperty(obj1, 'innumerable' ,{
    value:'不可枚举属性',
    enumerable:false
});

let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1);
console.log('obj2',obj2);

咱们来看一下控制台打印的后果,如下图所示。

从下面的样例代码中能够看到,利用 object.assign 也能够拷贝 Symbol 类型的对象,然而如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的扭转也会影响后者的第二层属性的值,阐明其中仍旧存在着拜访独特堆内存的问题,也就是说这种办法还不能进一步复制,而只是实现了浅拷贝的性能。

办法二:扩大运算符形式

咱们也能够利用 JS 的扩大运算符,在结构对象的同时实现浅拷贝的性能。

扩大运算符的语法为:let cloneObj = {…obj};

代码如下所示。

/* 对象的拷贝 */

let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj)  //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj)  //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; // 跟 arr.slice() 是一样的成果

扩大运算符 和 object.assign 有同样的缺点,也就是实现的浅拷贝的性能差不多,然而如果属性都是根本类型的值,应用扩大运算符进行浅拷贝会更加不便。

办法三:concat 拷贝数组

数组的 concat 办法其实也是浅拷贝,所以连贯一个含有援用类型的数组时,须要留神批改原数组中的元素的属性,因为它会影响拷贝之后连贯的数组。不过 concat 只能用于数组的浅拷贝,应用场景比拟局限。代码如下所示。

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

办法四:slice 拷贝数组

slice 办法也比拟有局限性,因为它仅仅针对数组类型。slice 办法会返回一个新的数组对象,这一对象由该办法的前两个参数来决定原数组截取的开始和完结工夫,是不会影响和扭转原始数组的。

slice 的语法为:arr.slice(begin, end);

咱们来看一下 slice 怎么应用,代码如下所示。

let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);  //[1, 2, { val: 1000} ]

从下面的代码中能够看出,这就是浅拷贝的限度所在了——它只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝将无能为力。因而深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝。

手工实现一个浅拷贝

依据以上对浅拷贝的了解,如果让你本人实现一个浅拷贝,大抵的思路分为两点:

  1. 对根底类型做一个最根本的一个拷贝;
  2. 对援用类型开拓一个新的存储,并且拷贝一层对象属性。

那么,围绕着这两个思路,请你跟着我的操作,本人来实现一个浅拷贝吧,代码如下所示。

const shallowClone = (target) => {if (typeof target === 'object' && target !== null) {const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {if (target.hasOwnProperty(prop)) {cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {return target;}
}

从下面这段代码能够看出,利用类型判断,针对援用类型的对象进行 for 循环遍历对象属性赋值给指标对象的属性,根本就能够手工实现一个浅拷贝的代码了。

那么理解了实现浅拷贝代码的思路,接下来咱们再看看深拷贝是怎么实现的。

深拷贝的原理和实现

浅拷贝只是创立了一个新的对象,复制了原有对象的根本类型的值,而援用数据类型只拷贝了一层属性,再深层的还是无奈进行拷贝。深拷贝则不同,对于简单援用数据类型,其在堆内存中齐全开拓了一块内存地址,并将原有的对象齐全复制过去寄存。

这两个对象是互相独立、不受影响的,彻底实现了内存上的拆散。总的来说,深拷贝的原理能够总结如下:

将一个对象从内存中残缺地拷贝进去一份给指标对象,并从堆内存中开拓一个全新的空间寄存新对象,且新对象的批改并不会扭转原对象,二者实现真正的拆散。

当初原理你晓得了,那么怎么去实现深拷贝呢?我也总结了几种办法分享给你。

办法一:乞丐版(JSON.stringify)

JSON.stringify() 是目前开发过程中最简略的深拷贝办法,其实就是把一个对象序列化成为 JSON 的字符串,并将对象外面的内容转换成字符串,最初再用 JSON.parse() 的办法将 JSON 字符串生成一个新的对象。示例代码如下所示。

let obj1 = {a:1, b:[1,2,3] }
let str = JSON.stringify(obj1);let obj2 = JSON.parse(str);console.log(obj2);   //{a:1,b:[1,2,3]} 
obj1.a = 2;obj1.b.push(4);
console.log(obj1);   //{a:2,b:[1,2,3,4]}
console.log(obj2);   //{a:1,b:[1,2,3]}

从下面的代码能够看到,通过 JSON.stringify 能够初步实现一个对象的深拷贝,通过扭转 obj1 的 b 属性,其实能够看出 obj2 这个对象也不受影响。

然而应用 JSON.stringify 实现深拷贝还是有一些中央值得注意,我总结下来次要有这几点:

  • 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,通过 JSON.stringify 序列化之后的字符串中这个键值对会隐没;
  • 拷贝 Date 援用类型会变成字符串;
  • 无奈拷贝不可枚举的属性;
  • 无奈拷贝对象的原型链;
  • 拷贝 RegExp 援用类型会变成空对象;
  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的后果会变成 null;
  • 无奈拷贝对象的循环利用,即对象成环 (obj[key] = obj)。

针对这些存在的问题,你能够尝试着用上面的这段代码亲自执行一遍,来看看如此简单的对象,如果用 JSON.stringify 实现深拷贝会呈现什么状况。

function Obj() {this.func = function () {alert(1) }; 
  this.obj = {a:1};
  this.arr = [1,2,3];
  this.und = undefined; 
  this.reg = /123/; 
  this.date = new Date(0); 
  this.NaN = NaN;
  this.infinity = Infinity;
  this.sym = Symbol(1);
} 

let obj1 = new Obj();

Object.defineProperty(obj1,'innumerable',{ 
  enumerable:false,
  value:'innumerable'
});

console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);

通过下面这段代码能够看到执行后果如下图所示。

应用 JSON.stringify 办法实现深拷贝对象,尽管到目前为止还有很多无奈实现的性能,然而这种办法足以满足日常的开发需要,并且是最简略和快捷的。而对于其余的也要实现深拷贝的,比拟麻烦的属性对应的数据类型,JSON.stringify 临时还是无奈满足的,那么就须要上面的几种办法了。

办法二:根底版(手写递归实现)

上面是一个实现 deepClone 函数封装的例子,通过 for in 遍历传入参数的属性值,如果值是援用类型则再次递归调用该函数,如果是根底数据类型就间接复制,代码如下所示。

let obj1 = {
  a:{b:1}
}

function deepClone(obj) {let cloneObj = {}
  for(let key in obj) {                 // 遍历
    if(typeof obj[key] ==='object') {cloneObj[key] = deepClone(obj[key])  // 是对象就再次调用该函数递归
    } else {cloneObj[key] = obj[key]  // 根本类型的话间接复制值
    }

  }
  return cloneObj

}

let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2);   //  {a:{b:1}}

尽管利用递归能实现一个深拷贝,然而同下面的 JSON.stringify 一样,还是有一些问题没有齐全解决,例如:

  1. 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;
  2. 这种办法只是针对一般的援用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的援用类型并不能正确地拷贝;
  3. 对象的属性外面成环,即循环援用没有解决。

这种根底版本的写法也比较简单,能够应答大部分的利用状况。然而你在面试的过程中,如果只能写出这样的一个有缺点的深拷贝办法,有可能不会通过。

所以为了“援救”这些缺点,上面我带你一起看看改良的版本,以便于你能够在面试种呈现出更好的深拷贝办法,博得面试官的青眼。

办法三:改进版(改良后递归实现)

针对下面几个待解决问题,我先通过四点相干的实践通知你别离应该怎么做。

  1. 针对可能遍历对象的不可枚举属性以及 Symbol 类型,咱们能够应用 Reflect.ownKeys 办法;
  2. 当参数为 Date、RegExp 类型,则间接生成一个新的实例返回;
  3. 利用 Object 的 getOwnPropertyDescriptors 办法能够取得对象的所有属性,以及对应的个性,顺便联合 Object 的 create 办法创立一个新对象,并继承传入原对象的原型链;
  4. 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱援用类型,能够无效避免内存透露(你能够关注一下 Map 和 weakMap 的要害区别,这里要用 weakMap),作为检测循环援用很有帮忙,如果存在循环,则援用间接返回 WeakMap 存储的值。

对于第 4 点的 WeakMap,这里我不进行过多的科普解说了,你如果不分明能够本人再通过相干材料理解一下。我也常常在给人面试中看到有人应用 WeakMap 来解决循环援用问题,然而很多解释都是不够清晰的。

当你不太理解 WeakMap 的真正作用时,我倡议你不要在面试中写出这样的代码,如果只是死记硬背,会给本人挖坑的。因为你写的每一行代码都是须要通过三思而行并且十分清晰明确的,这样你能力经得住面试官的斟酌。

当然,如果你在思考到循环援用的问题之后,还能用 WeakMap 来很好地解决,并且向面试官解释这样做的目标,那么你所展现的代码,以及你对问题思考的全面性,在面试官眼中应该算是合格的了。

那么针对下面这几个问题,咱们来看下改良后的递归实现的深拷贝代码应该是什么样子的,如下所示。

const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {if (obj.constructor === Date) 
  return new Date(obj)       // 日期对象间接返回一个新的日期对象
  if (obj.constructor === RegExp)
  return new RegExp(obj)     // 正则对象间接返回一个新的正则对象
  // 如果循环援用了就用 weakMap 来解决
  if (hash.has(obj)) return hash.get(obj)
  let allDesc = Object.getOwnPropertyDescriptors(obj)
  // 遍历传入参数所有键的个性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
  // 继承原型链
  hash.set(obj, cloneObj)
  for (let key of Reflect.ownKeys(obj)) {cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
  }
  return cloneObj
}

// 上面是验证代码

let obj = {
  num: 0,
  str: '',
  boolean: true,
  unf: undefined,
  nul: null,
  obj: {name: '我是一个对象', id: 1},
  arr: [0, 1, 2],
  func: function () { console.log('我是一个函数') },
  date: new Date(0),
  reg: new RegExp('/ 我是一个正则 /ig'),
  [Symbol('1')]: 1,
};

Object.defineProperty(obj, 'innumerable', {enumerable: false, value: '不可枚举属性'}
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj    // 设置 loop 成循环援用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)

咱们看一下后果,cloneObj 在 obj 的根底上进行了一次深拷贝,cloneObj 里的 arr 数组进行了批改,并未影响到 obj.arr 的变动,如下图所示。

从这张截图的后果能够看出,改进版的 deepClone 函数曾经对根底版的那几个问题进行了改良。

正文完
 0