大厂面试题分享 面试题库
前后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库 web前端面试题库 VS java后端面试题库大全
引子
通过本文可以学习到深拷贝的三种写法的实现思路与性能差异
首先,我们要理解什么是深拷贝,以及为什么要实现深拷贝
深拷贝是什么
通俗来讲,深拷贝就是深层的拷贝一个变量值。
为什么要实现深拷贝
因为在拷贝引用值时,由于复制一个变量只是将其指向要复制变量的引用内存地址,他们并没有完全的断开,而使用就可以实现深拷贝将其完全拷贝为两个单独的存在,指向不同的内存地址。
如何实现深拷贝
一行实现
let deepClone = JSON.parse(JSON.stringify(obj)) 复制代码
这种是最简单的实现方法,但缺点是无法拷贝 Date()
或是RegExp()
。
简单实现
function deepClone(obj) { // 判断是否是对象 if (typeof obj !== 'object') return obj // 判断是否是数组 如果是数组就返回一个新数组 否则返回一个新对象 var newObj = obj instanceof Array ? [] : {}; // 遍历obj for (var key in obj) { // 将key值拷贝,再层层递进拷贝对象的值 newObj[key] = deepClone(obj[key]); } // 返回最终拷贝完的值 return newObj; } 复制代码
对于普通的值(如数值、字符串、布尔值)和常见的引用类型(如对象和数组),这个写法完全够用。
但因为少了对 Date()
和 RegExp()
这些引用类型的特殊处理,这个写法一样不够完备。
普通版
function deepClone(origin, target) { // 判断target是否传入,如果未传入则创建一个{} let tar = target || {}; // 遍历origin对象 for (var key in origin) { // 判断是否origin的自有属性 if (origin.hasOwnProperty(key)) { // 如果值是对象并且不为null,递归调用 if (typeof origin[key] === 'object' && origin[key] !== null) { // 根据值是否为数组创建初始化对象或数组 tar[key] = Array.isArray(origin[key]) ? [] : {}; // 递归调用 deepClone(origin[key], tar[key]); } else { // 如果是简单类型,直接复制值 tar[key] = origin[key]; } } } // 返回最终拷贝完的值 return tar; } 复制代码
这个深拷贝方法通过判断属性的值类型,实现了对 对象、数组 以及 Date、RegExp 等引用类型对象的递归拷贝,同时也考虑了拷贝基本类型值的情况,能够满足大多数场景的要求。
最终版
为什么还有最终版?
上面的案例,可以应对一般场景。
但是对于有两个对象相互拷贝的场景,会导致循环的无限递归,造成死循环!
Uncaught RangeError: Maximum call stack size exceeded
场景:
如何解决无限递归的问题?
首先我们要了解 WeakMap: WeakMap 的键名所引用的对象都是弱引用,不会被垃圾回收机制考虑,所以当对象只被WeakMap引用时,其所占用的内存会被垃圾回收。
而通过 WeakMap 记录已经拷贝过的对象,能防止循环引用导致的无限递归。
代码
实现简述:利用 WeakMap()
在属性遍历完绑定,并在每次循环时获取当前键名,如果存在则返回数据,不存在则拷贝。
function deepClone(origin, hashMap = new WeakMap()) { // 判断是否是对象 if (origin == undefined || typeof origin !== 'object') return origin; // 判断是否是Date/RegExp类型 if (origin instanceof Date) return new Date(origin); if (origin instanceof RegExp) return new RegExp(origin); // 判断是否已经克隆过此对象, 如果是直接返回 const hashKey = hashMap.get(origin); if (hashKey) return hashKey; // *利用原型构造器获取新的对象 如: [], {} const target = new origin.constructor(); // 将对象存入map hashMap.set(origin, target); // 循环遍历当前层数据 for (let k in origin) { // 判断当前属性是否为引用类型 if (origin.hasOwnProperty(k)) { target[k] = deepClone(origin[k], hashMap); } } return target; } 复制代码
我们再来看一下使用最新版后的两个对象互相拷贝:
可以看到,通过使用 WeakMap 记录已经拷贝的对象,有效防止循环引用导致的栈溢出错误,是功能最完备的深拷贝实现。
总结
深拷贝可以完全拷贝一个对象,生成两个独立的且相互不影响的对象。
明白各种深拷贝实现的思路和性能差异,可以在不同场景选用最优的方案。