面试如何写出一个满意的深拷贝(适合初级前端)

前言

已经有不少关于深拷贝与浅拷贝的文章,为何本身还要写一遍呢💯javascript

学习就比如是座大山,人们沿着不一样的路爬山,分享着本身看到的风景。你不必定能看到别人看到的风景,体会到别人的心情。只有本身去爬山,才能看到不同的风景,体会才更加深入。java

分享一个不错的思惟导图👇git

深拷贝
深拷贝

经过文本的总结,但愿能够明白:github

  • 什么是深拷贝/浅拷贝,他们与赋值有什么区别
  • 深拷贝/浅拷贝实现方式有哪些

本章节直接从拷贝开始提及,对于基本数据类型,引用数据类型以前的区别,能够看看上面的思惟导图👆web

或者看看我以前的章节补一补基础,有写的不对的地方欢迎指出!面试


引用数据类型拷贝

对于引用数据类型的话,细分能够分为下面三个方面segmentfault

  • 赋值
  • 浅拷贝
  • 深拷贝

赋值

引用类型的赋值是传址。只是改变指针的指向,例如,引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,所以二者之间操做互相有影响。例如:数组

var a = {}; // a保存了一个空对象的实例
var b = a; // a和b都指向了这个空对象  a.name = 'jozo'; console.log(a.name); // 'jozo' console.log(b.name); // 'jozo'  b.age = 22; console.log(b.age);// 22 console.log(a.age);// 22  console.log(a == b);// true 复制代码
拷贝2
拷贝2

这样子的状况,会致使a和b指向同一份数据,对其中一个进行修改数据的话,会影响到另一个,实际开发中,这不是咱们预期中的结果,这会照成某种程度上的bug。编辑器

那么咱们如何不让相互之间产生影响呢?一种简单的办法就是拷贝一份a变量的数据,因此根据拷贝的层次不一样能够分为浅拷贝和深拷贝,浅拷贝的话知识进行一层拷贝,深拷贝的话是无限层次的拷贝!函数

咱们先来实现一个浅拷贝

let shallowClone = source => {
 let target = {}  for(let i in source) {  if( source.hasOwnProperty(i) )  target[i] = source[i];  }  return target  }  let demo = {  b:{  c : {  }  }  }  let demo2 = shallowClone(demo)  let demo3 = demo;  console.log(demo3 === demo ) // true  console.log(demo2.b.c === demo.b.c ) // true  console.log(demo2.b === demo.b ) // true  console.log(demo2 === demo ) // false 复制代码

demo3 = demo 赋值的话,是地址的赋值,也就是说指向同一个对象,那么不是咱们想要的结果,咱们来看看shallowClone函数,这个是浅拷贝的一种实现方式,那么demo2变量应该就是实现了一层的拷贝,正如20行效果,demo2变量是在堆中开了一个新内存,因此二者指向不一样对象,demo2.b === demo.b 为 true 说明 这就是浅拷贝效果,简单的拷贝一层,那么咱们是否是能够递归的思想去完成深拷贝呢?


浅拷贝的实现方式

Object.assign()

Object.assign() 方法用于将全部可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

let demo = {
 name : 'dayday',  book : {  title : 'Do you really Know JS',  price : "45"  }  }  let clone_demo = Object.assign({}, demo)  console.log(clone_demo);  demo.name = 'new name'  demo.book.price = '100'  console.log(clone_demo.name,clone_demo.book.price);  // dayday 100 复制代码

修改上面代码demo变量以后,对象clone_demo基本属性没有改变,可是修改demo对象中book引用属性时,对象clone_demo相应位置属性值也发生改变,一样的接下来展开运算符也是同样效果👇

展开运算符...

let demo = {
 name : 'dayday',  book : {  title : 'Do you really Know JS',  price : "45"  }  }  let clone_demo = {...demo}  console.log(clone_demo);  demo.name = 'new name'  demo.book.price = '100'  console.log(clone_demo.name,clone_demo.book.price);  // dayday 100 复制代码

咱们能够看到展开运算… 效果跟Object.assign() 效果是同样的。

Array.prototype.slice()

slice() 方法返回一个新的数组对象,这一对象是一个由 beginend(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。

let a = [0, "1", [2, 3]];
let b = a.slice(1); console.log(b); // ["1", [2, 3]]  a[1] = "99"; a[2][0] = 4; console.log(a); // [0, "99", [4, 3]]  console.log(b); // ["1", [4, 3]] 复制代码

能够看出,改变 a[1] 以后 b[0] 的值并无发生变化,但改变 a[2][0] 以后,相应的 b[1][0] 的值也发生变化。说明 slice() 方法是浅拷贝,相应的还有concat等,在工做中面对复杂数组结构要额外注意。


深拷贝实现方式

深拷贝会拷贝全部的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一块儿拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢而且花销较大。拷贝先后两个对象互不影响。

JSON.parse(JSON.stringify(obj))

let demo = {
 name : 'dayday',  book : {  title : 'Do you really Know JS',  price : "45"  }  }  let clone_demo = JSON.parse(JSON.stringify(demo))  console.log(clone_demo);  demo.name = 'new name'  demo.book.price = '100'  console.log(clone_demo.name,clone_demo.book.price);  // dayday 45 复制代码

彻底改变变量 demo 以后对 clone_demo 没有任何影响,这就是深拷贝的魔力。

一样的对于数组使用该方法也是能够达到深拷贝的。

注意的就是:
  • 会忽略undefined Symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
  • 不能正确处理 new Date()
  • 不能处理正则

对于undefined symbol 函数三种状况会直接忽略

let demo = {
 name : 'dayday',  h1 : undefined,  h2 : Symbol('dayday'),  h3 : function () {},  }  let clone_demo = JSON.parse(JSON.stringify(demo))  console.dir(clone_demo)  // { name : 'dayday' } 复制代码

循环引用状况下,会报错。

let obj = {
 a: 1,  b: {  c: 2,  d: 3  } } obj.a = obj.b; obj.b.c = obj.a;  let b = JSON.parse(JSON.stringify(obj)); // Uncaught TypeError: Converting circular structure to JSON 复制代码

new Date 状况下,转换结果不正确。

new Date();
// Wed Jul 01 2020 16:19:07 GMT+0800 (中国标准时间) {}  JSON.stringify(new Date()); // ""2020-07-01T08:19:19.860Z""  JSON.parse(JSON.stringify(new Date())); // "2020-07-01T08:19:35.569Z"  复制代码

解决方法转成字符串或者时间戳就行了

let date = (new Date()).valueOf();
// 1593591638596  JSON.stringify(date); // "1593591638596"  JSON.parse(JSON.stringify(date)); // 1593591638596 复制代码

正则状况下

let demo = {
 name: "daydaylee",  a: /'123'/ } console.log(demo); // {name: "daydaylee", a: /'123'/}  let clone_demo = JSON.parse(JSON.stringify(obj)); console.log(clone_demo); // {name: "daydaylee", a: {}} 复制代码

PS:为何会存在这些问题能够学习一下 JSON

除了上面介绍的深拷贝方法,经常使用的还有jQuery.extend()lodash.cloneDeep(),因为文章篇幅的问题,这里就很少介绍了,有兴趣的能够本身去了解了解

面试如何实现一个深拷贝

面试官叫你实现一个深拷贝的话,你只要记得浅拷贝+递归,浅拷贝的时候,去判断是否是一个对象就行的,是对象的话,就进行递归操做。

以前的简单浅拷贝:

let shallowClone = source => {
 let target = {}  for(let key in source) {  if(Object.prototype.hasOwnProperty.call(source, key)){  target[key] = typeof source[key] === 'object' ? shallowClone(source[key]) : source[key];  }  }  return target  }  let demo = {  name : 'dayday',  book : {  title : 'Do you really Know JS',  price : "45"  }  }  let clone_demo = shallowClone(demo);  console.log(clone_demo);  demo.name = 'new name'  demo.book.price = '100'  console.log(clone_demo.name,clone_demo.book.price)  // dayday 45 复制代码

写到这里,至少一个简单的深克隆实现了,可是仍是有些问题没有解决!

  • 没有考虑数组的写法
  • 对对象的判断逻辑不严谨,由于 typeof null === object
  • 没有对传入参数校验,好比传入null 应该返回 null 而不是 {}

首先的写一个兼容数组而且判断null方法的函数

let isObject = obj => typeof obj === 'object' && obj !== null ;
复制代码

那么进一步完善了深度拷贝的方法

// 保留数组 而且判断是否是null
 let isObject = obj => typeof obj === 'object' && obj !== null ;  let shallowClone2 = source => {   if(!isObject(source)) return source // 非对象返回自身  let target = Array.isArray(source) ? [] : {}  for(let key in source) {  if(Object.prototype.hasOwnProperty.call(source, key)){  target[key] = isObject(source[key]) ? shallowClone2(source[key]) : source[key];  }  }  return target  }  let demo = {  name : 'dayday',  book : {  title : 'Do you really Know JS',  price : "45"  },  h1 : null,  h2 : [1,2,3],  h3 : undefined  }  let clone_demo = shallowClone2(demo);  console.log(clone_demo);  demo.name = 'new name'  demo.book.price = '100'  demo.h2[1] = 'new data'  console.log(clone_demo.name,clone_demo.book.price)  // dayday 45  console.log(clone_demo);  // 修改demo值为能影响clone_demo 复制代码

这篇文章写的很好:深拷贝的终极探索(99%的人都不知道)

它还对深度拷贝有了新的优化,好比JSON.parse(JSON.stringify(obj))循环引用抛出异常的问题,作出了优化,那咱们试着去优化这个小问题。

  • 哈希表

对于循环检测的话,咱们可使用哈希检测的方法,好比设置一个数组或者是已经拷贝的对象,当检测到对象已经存在哈希表时,就取出该值🤭

let isObject = obj => typeof obj === 'object' && obj !== null;
 let shallowClone3 = (source, hash = new WeakMap()) => {   if (!isObject(source)) return source // 非对象返回自身  if (hash.has(source)) return hash.get(source) // 新增检测, 查哈希表  let target = Array.isArray(source) ? [] : {}  hash.set(source, target) // 设置哈希表值  for (let key in source) {  if (Object.prototype.hasOwnProperty.call(source, key)) {  target[key] = isObject(source[key]) ? shallowClone3(source[key], hash) : source[key]; // 传入哈希表  }  }  return target  }  let obj = {  a: 1,  b: {  c: 2,  d: 3  }  }  obj.a = obj.b;  obj.b.c = obj.a;  let clone_obj = shallowClone3(obj)  console.log(clone_obj) 复制代码

写完这段代码的话,至少面试实现一个这样子的深拷贝马马虎虎过的去,固然了仍是有不少的问题须要解决的:

  • 好比拷贝一个Symbol类型的值该这么解决
  • 这么解决递归爆栈问题

固然了有兴趣的读者能够深刻的了解呐🚀

总结

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

参考

JavaScript深拷贝的一些坑

面试题之如何实现一个深拷贝

深刻剖析 JavaScript 的深复制

MDN展开语法

MDN之Object.assign()

深拷贝的终极探索(99%的人都不知道)

本文使用 mdnice 排版