深入了解浅拷贝与深拷贝

2022-12-20 0 1,000

深入了解浅拷贝与深拷贝

慢板

坚信点进去的老师啥对浅复本与深复本有很大的介绍,这儿就无须多约勒,看完这首诗,期望能增进你对深复本的认知。

正则表达式

讲起复本,就不得已提出诉讼 js 的正则表达式了,即使深复本和浅复本的核心理念就是不同的正则表达式在缓存中储存的地方性相同。

ECMAScript 基本上正则表达式

最捷伊 ECMAScript 国际标准表述了 8 种正则表达式,当中 7 中是基本上正则表达式,它是:Boolean、Null、Undefined、Number、String、BigInt、Symbol。

基本上正则表达式都是储存在栈(stack)缓存中,栈具备科天料的特征,基本上正则表达式占用空间小、大小不一一般来说,透过按值来出访,归属于被频密采用的统计数据。

大部份的是基本上类别值这类是难以被出现改变的。可能将有的是人能有疑点,我整天修正数组等基本上类别值,还并非出现了出现改变么,只不过他们对数组展开操作方式后,回到的都是捷伊数组,并没修正原本的统计数据。

let a = 1;let b = a;b = 2;console.log(a, b); // 1 2复制代码基本上正则表达式的赋值,赋值后两个变量互不影响,b复制的是a的原始值,它储存在独立的的栈空间中,因此修正了b的值,a的值不会受到影响,大家能查看下图更清晰的介绍。

深入了解浅拷贝与深拷贝

ECMAScript 引用正则表达式

引用正则表达式 Object,像 Array、Function、Date…等都归属于 Object,它的值都是对象。

引用正则表达式存放在堆缓存中,能直接展开出访和修正。

引用正则表达式占据空间大、大小不一不一般来说,存放在栈中会有性能的问题。引用正则表达式在栈中保存了一份指针,该指针指向对应的统计数据在堆中的起始地址,当解释器寻找引用值时,会首先检索其在栈中的地址,透过地址从堆中获得统计数据。

let obj = { name: 烟花渲染离别 };let obj2 = obj;obj2.name = 七宝;console.log(obj.name); // 七宝console.log(obj2.name); // 七宝复制代码引用类别的赋值,在栈中复制了一份引用类别的地址指针,两个变量指向的还是同一个对象,所以修正了obj2.name,obj.name也会出现出现改变,这种出现改变有时候并并非他们所期望的,这时候就需要拿出他们的秘技:浅复本和深复本。

深入了解浅拷贝与深拷贝

浅复本

浅复本就是将源对象的属性复本一份,如果属性时基本上类别值,直接复本基本上类别值,如果属性是引用类别,则复本的是该引用类别在堆中的地址。下面介绍几种常用的浅复本方法:

展开运算符 …

个人常用的浅复本就是 …展开运算符了,展开运算符是es6的新特性,坚信大家都已经很介绍了,不介绍的能前往阮一峰大神的ECMAScript 6 入门查看。

let obj = { name: 烟花渲染离别 };let obj2 = { …obj };obj2.name = 七宝;console.log(obj.name); // 烟花渲染离别console.log(obj2.name); // 七宝复制代码Object.assign()

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

我一般是在需要合并两个对象成为一个新对象时采用这个方法。

let obj = { name: 烟花渲染离别 };let obj2 = Object.assign({}, obj);obj2.name = 七宝;console.log(obj.name); // 烟花渲染离别console.log(obj2.name); // 七宝复制代码concat和slice

这两个方法常用来复本数组。

let arr = [1, 2];let arr2 = arr.concat();arr.push(3);console.log(arr); // [1, 2, 3]console.log(arr2); // [1, 2]复制代码let arr = [1, 2];let arr2 = arr.slice();arr.push(3);console.log(arr); // [1, 2, 3]console.log(arr2); // [1, 2]复制代码浅复本的问题

有了浅复本后,为什么还需要深复本呢?自然是即使浅复本是有缺陷的,如果复本的对象中属性有引用类别值的话,浅复本就不能达到预期的完全复制隔离的效果了,下面来看个例子:

let obj = { name: 烟花渲染离别, hobby: [看动漫] };let obj2 = { …obj };obj2.name = 七宝;console.log(obj.name); // 烟花渲染离别console.log(obj2.name); // 七宝obj.hobby.push(打球);console.log(obj.hobby); // [看动漫, 打球]console.log(obj2.hobby); // [看动漫, 打球]console.log(obj.hobby === obj2.hobby); // true复制代码能看到浅复本后,obj.hobby的修正影响到了obj2.hobby,根据他们上面引用类别的赋值,他们能大胆推测,浅复本复本的是hobby的指针。同样画个图方便大家认知。

深入了解浅拷贝与深拷贝

既然浅复本有这种问题,那他们肯定想要避免这个问题,怎么去避免这个问题呢?这就要用到我下面要讲的深复本了。

深复本

深复本,顾名思义就是比浅复本能够更深层级的复本,它能够将复本过程中遇到的引用类别都新开辟一块地址复本对应的统计数据,这样就能避免子对象共享同一份缓存的问题了。

JSON.parse(JSON.stringify())

let obj = { name: 烟花渲染离别, hobby: [看动漫] };let obj2 = JSON.parse(JSON.stringify(obj));obj.hobby.push(打球);console.log(obj.hobby); // [看动漫, 打球]console.log(obj2.hobby); // [看动漫]复制代码基于JSON.stringify将对象先转成数组,再透过JSON.parse将数组转成对象,此时对象中每个层级的堆缓存都是新开辟的。

这种方法虽然简单,但它还有几个缺陷:

不能解决循环引用的问题难以复本特殊对象,比如:RegExp、BigInt、Date、Set、Map等手写深复本

既然利用js内置的JSON.parse + JSON.stringify方法展开深复本有缺陷的话,那他们就自己动手实现一个深复本吧。

实现深复本之前思考下他们思考下应该怎么去实现,只不过核心理念就是:浅复本 + 递归。

对于基本上正则表达式,他们直接复本即可对于引用正则表达式,则需要展开递归复本。他们先动手实现一个功能类似JSON.parse(JSON.stringify())的简单深复本,能对对象和数组展开深复本

ionisObject(target) {const type = typeof target;return target !== null && (type === object || type === function);}functiondeepClone(target) {if (!isObject(target)) return target; // 复本基本上类别值let cloneTarget = Array.isArray(target) ? [] : {}; // 判断复本的是否是数组Object.keys(target).forEach(key => { cloneTarget[key] = deepClone(target[key]); // 递归复本属性 });return cloneTarget;}let obj = { name: 烟花渲染离别, hobby: [看动漫] };let obj2 = deepClone(obj);obj2.name = 七宝;console.log(obj.name); // 烟花渲染离别console.log(obj2.name); // 七宝obj.hobby.push(打球);console.log(obj.hobby); // [看动漫, 打球]console.log(obj2.hobby); // [看动漫]复制代码

深入了解浅拷贝与深拷贝

能看到基本上实现了JSON.parse(JSON.stringify())的深复本功能,但是他们都知道这种方法的缺陷,那他们继续完善深复本方法。

处理循环引用

什么是循环引用呢?简单来说就是自己内部引用了自已,和递归的自己调用自己有点像,来看个例子吧:

let obj = { name: 烟花渲染离别 };obj.info = obj;console.log(obj);复制代码

深入了解浅拷贝与深拷贝

如果采用上面的深复本的话,即使没处理循环引用,就会导致info属性一直递归复本,递归死循环导致栈缓存溢出。

如何处理循环引用呢?他们能开辟一个空间储存要复本过的对象,当复本当前对象时,先去储存空间查找该对象是否被复本过,如果复本过,直接回到该对象,如果没复本过就继续复本。

functiondeepClone(target, cache = newWeakSet()) { if (!isObject(target)) return target; // 复本基本上类别值if (cache.has(target)) return target; // 如果之前已经复本过该对象,直接回到该对象 cache.add(target); // 将对象添加缓存let cloneTarget = Array.isArray(target) ? [] : {}; // 判断复本的是否是数组Object.keys(target).forEach(key => { cloneTarget[key] = deepClone(target[key], cache); // 递归复本属性,将缓存传递 });return cloneTarget;}复制代码这儿采用了WeakSet收集复本对象,WeakSet中的对象都是弱引用的,垃圾回收机制不考虑WeakSet对该对象的引用。如果他们复本的对象很大的时候,采用Set会导致很大的缓存消耗,需要他们手动清除Set中的统计数据才能释放缓存,而WeakSet则不会有这样的问题。

处理键是 Symbol 类别

Symbol 值作为键名,难以被Object.keys()、Object.getOwnPr

let symbol = Symbol(我是独一无二的值);let obj = { name: 烟花渲染离别, [symbol]: };const obj2 = deepClone(obj);console.log(obj2); // { name: “烟花渲染离别” }复制代码

深入了解浅拷贝与深拷贝

能看到,深复本后并没拿到Symbol属性的,他们可eys()来拿到对象的大部份属性。

functiondeepClone(target, cache = newWeakSet()) {if (!isObject(target)) return target; // 复本基本上类别值if (cache.has(target)) return target; cache.add(target);let cloneTarget = Array.isArray(target) ? [] : {}; // 判断复本的是否是数组Reflect.ownKeys(target).forEach(key => { cloneTarget[key] = deepClone(target[key], cache); // 递归复本属性 });return cloneTarget;}let symbol = Symbol(我是独一无二的值);let obj = { name: 烟花渲染离别, [symbol]: };console.log(obj); // { name: “烟花渲染离别”, Symbol(我是独一无二的值): “” }const obj2 = deepClone(obj); console.log(obj2); // { name: “烟花渲染离别”, Symbol(我是独一无二的值): “” }复制代码处理其他引用类别值

上面只处理了数组和对

const arrayTag = [object Array]const objectTag = [object Object]const mapTag = [object Map]const setTag = [object Set]const regexpTag = [object RegExp]const boolTag = [object Boolean]const numberTag = [object Number]const stringTag = [object String]const symbolTag = [object Symbol]const dateTag = [object Date]const errorTag = [object Error]复制代码创建复本对象

拿到复本对象的构造函数,透过源对象的构造函数生成的对象能保留对象原型上的统计数据,如果采用{},则原型上的统计数据会丢失。

Boolean、Number、String、Date、Error他们能直接透过构造函数和原始统计数据创建一个捷伊对象。Object、Map、Set他们直接执行构造函数回到初始值,递归处理后续属性,即使它的属性能保存对象。Array、Symbol、RegExp展开特殊处理。functioninitCloneTargetByTag(target, tag) {const Ctor = target.constructor;switch (tag) {case boolTag:case dateTag:returnnew Ctor(+target);case numberTag:case stringTag:case errorTag:returnnew Ctor(target);case objectTag:case mapTag:case setTag:returnnew Ctor();case arrayTag:return cloneArray(target);case symbolTag:return cloneSymbol(target);case regexpTag:return cloneRegExp(target); }}functiondeepClone(target, cache = newWeakSet()) { …const tag = Object.prototype.toString.call(target);let cloneTarget = initCloneTargetByTag(target, tag); // 采用复本对象的构造方法创建对应类别的统计数据 …}复制代码初始化 Array

cloneArray 是为了兼容处理匹配正则时执行exec()后的回到结果,exec()方法会回到一个数组,当中包含了额外的index和input属性。

functioncloneArray(array) {const { length } = array;const result = new array.constructor(length);if (length && typeof array[0] === string && hasOwnProperty.call(array, index)) { result.index = array.index; result.input = array.input; }return result;}复制代码初始化 Symbol

functioncloneSymbol(symbol) {returnObject(Symbol.prototype.valueOf.call(symbol));}复制代码初始化 RegExp

functioncloneRegExp(regexp) {const reFlags = /\w*$/; // \w 用于匹配字母,数字或下划线字符,相当于[A-Za-z0-9_]const result = new regexp.constructor(regexp.source, reFlags.exec(regexp)); // 回到当前匹配的文本 result.lastIndex = regexp.lastIndex; // 下一次匹配的起始索引return result;}复制代码处理Map和Set

map和set有透过独有的是set、add方法设置值,单独处理。

functiondeepClone(target, cache = newWeakSet()) { …if (tag === mapTag) { target.forEach((value, key) => { cloneTarget.set(key, deepClone(value, map)); });return cloneTarget; }if (tag === setTag) { target.forEach(value => { cloneTarget.add(deepClone(value, map)); });return cloneTarget; } …}复制代码处理函数

事实上,他们直接采用同一个缓存地址的函数是没问题的,所以他们能直接回到该函数,lodash上也是这么处理的。

functiondeepClone(target, cache = newWeakSet()) { …if (tag === functionTag) {return target; } …}复制代码完整代码

const arrayTag = [object Array]const objectTag = [object Object]const mapTag = [object Map]const setTag = [object Set]const functionTag = [object Function];const boolTag = [object Boolean]const dateTag = [object Date]const errorTag = [object Error]const numberTag = [object Number]const regexpTag = [object RegExp]const stringTag = [object String]const symbolTag = [object Symbol]functioncloneArray(array) {const { length } = array;const result = new array.constructor(length);if (length && typeof array[0] === string && hasOwnProperty.call(array, index)) { result.index = array.index; result.input = array.input; }return result;}functioncloneSymbol(symbol) {returnObject(Symbol.prototype.valueOf.call(symbol));}functioncloneRegExp(regexp) {const reFlags = /\w*$/;const result = new regexp.constructor(regexp.source, reFlags.exec(regexp)); result.lastIndex = regexp.lastIndex;return result;}functioninitCloneTargetByTag(target, tag) {const Ctor = target.constructor;switch (tag) {case boolTag:case dateTag:returnnew Ctor(+target);case numberTag:case stringTag:case errorTag:returnnew Ctor(target);case objectTag:case mapTag:case setTag:returnnew Ctor();case arrayTag:return cloneArray(target);case symbolTag:return cloneSymbol(target);case regexpTag:return cloneRegExp(target); }}functionisObject(target) {const type = typeof target;return target !== null && (type === object || type === function);}functiondeepClone(target, cache = newWeakSet()) {if (!isObject(target)) return target; // 复本基本上类别值if (cache.has(target)) return target; cache.add(target);const tag = Object.prototype.toString.call(target);let cloneTarget = initCloneTargetByTag(target, tag); // 采用复本对象的构造方法创建对应类别的统计数据if (tag === mapTag) { target.forEach((value, key) => { cloneTarget.set(key, deepClone(value, map)); });return cloneTarget; }if (tag === setTag) { target.forEach(value => { cloneTarget.add(deepClone(value, map)); });return cloneTarget; }if (tag === functionTag) {return target; }Reflect.ownKeys(target).forEach(key => { cloneTarget[key] = deepClone(target[key], cache); // 递归复本属性 });return cloneTarget;}复制代码测试代码

写完了代码他们肯定得需要测试下代码是否能正常运行,下面来看看吧。

const map = newMap();map.set(烟花, 渲染离别);map.set(掘金, https://juejin.cn/user/2101921961223614);const set = newSet();set.add(set1);set.add(set2);let target = { arr: [1, 2, 3], bool: false, bool2: newBoolean(true), date: newDate(), empty: null, error: newError(), func: () => {console.log(我是函数); }, map, num: 1, num2: newNumber(1), obj: { children: { name: 我是子对象 } }, reg: /\w*$/, set, str: 烟花渲染离别, str2: newString(new String), symbol: Symbol(symbol), symbol: Object(Symbol(new Symbol)), undefined: undefined,};let cloneTarget = deepClone(target);console.log(cloneTarget);target.obj.children.name = 修正子对象;console.log(cloneTarget.obj.children.name);复制代码

深入了解浅拷贝与深拷贝

总结

深复本作为面试常考的题目,里面确实涉及到了很多细节:

考察你的递归能力考察处理循环引用,还能深入细致挖掘对weakSet、weakMap弱引用的介绍程度考察各种引用类别的处理,对正则表达式的掌握的程度考察Symbol作为对象属性的遍历处理等

举报/反馈

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务