您好,欢迎访问一九零五行业门户网

一文带你详细了解JavaScript中的深拷贝

网上有很多关于深拷贝的文章,但是质量良莠不齐,有很多都考虑得不周到,写的方法比较简陋,难以令人满意。本文旨在完成一个完美的深拷贝,大家看了如果有问题,欢迎一起补充完善。
评价一个深拷贝是否完善,请检查以下问题是否都实现了:
基本类型数据是否能拷贝?
键和值都是基本类型的普通对象是否能拷贝?
symbol作为对象的key是否能拷贝?
date和regexp对象类型是否能拷贝?
map和set对象类型是否能拷贝?
function对象类型是否能拷贝?(函数我们一般不用深拷贝)
对象的原型是否能拷贝?
不可枚举属性是否能拷贝?
循环引用是否能拷贝?
怎样?你写的深拷贝够完善吗?
深拷贝的最终实现这里先直接给出最终的代码版本,方便想快速了解的人查看,当然,你想一步步了解可以继续查看文章余下的内容:
function deepclone(target) {    const map = new weakmap()        function isobject(target) {        return (typeof target === 'object' && target ) || typeof target === 'function'    }    function clone(data) {        if (!isobject(data)) {            return data        }        if ([date, regexp].includes(data.constructor)) {            return new data.constructor(data)        }        if (typeof data === 'function') {            return new function('return ' + data.tostring())()        }        const exist = map.get(data)        if (exist) {            return exist        }        if (data instanceof map) {            const result = new map()            map.set(data, result)            data.foreach((val, key) => {                if (isobject(val)) {                    result.set(key, clone(val))                } else {                    result.set(key, val)                }            })            return result        }        if (data instanceof set) {            const result = new set()            map.set(data, result)            data.foreach(val => {                if (isobject(val)) {                    result.add(clone(val))                } else {                    result.add(val)                }            })            return result        }        const keys = reflect.ownkeys(data)        const alldesc = object.getownpropertydescriptors(data)        const result = object.create(object.getprototypeof(data), alldesc)        map.set(data, result)        keys.foreach(key => {            const val = data[key]            if (isobject(val)) {                result[key] = clone(val)            } else {                result[key] = val            }        })        return result    }    return clone(target)}
1. javascript数据类型的拷贝原理先看看js数据类型图(除了object,其他都是基础类型):
在javascript中,基础类型值的复制是直接拷贝一份新的一模一样的数据,这两份数据相互独立,互不影响。而引用类型值(object类型)的复制是传递对象的引用(也就是对象所在的内存地址,即指向对象的指针),相当于多个变量指向同一个对象,那么只要其中的一个变量对这个对象进行修改,其他的变量所指向的对象也会跟着修改(因为它们指向的是同一个对象)。如下图:
2. 深浅拷贝深浅拷贝主要针对的是object类型,基础类型的值本身即是复制一模一样的一份,不区分深浅拷贝。这里我们先给出测试的拷贝对象,大家可以拿这个obj对象来测试一下自己写的深拷贝函数是否完善:
// 测试的obj对象const obj = {    // =========== 1.基础数据类型 ===========    num: 0, // number    str: '', // string    bool: true, // boolean    unf: undefined, // undefined    nul: null, // null    sym: symbol('sym'), // symbol    bign: bigint(1n), // bigint    // =========== 2.object类型 ===========    // 普通对象    obj: {        name: '我是一个对象',        id: 1    },    // 数组    arr: [0, 1, 2],    // 函数    func: function () {        console.log('我是一个函数')    },    // 日期    date: new date(0),    // 正则    reg: new regexp('/我是一个正则/ig'),    // map    map: new map().set('mapkey', 1),    // set    set: new set().add('set'),    // =========== 3.其他 ===========    [symbol('1')]: 1  // symbol作为key};// 4.添加不可枚举属性object.defineproperty(obj, 'innumerable', {    enumerable: false,    value: '不可枚举属性'});// 5.设置原型对象object.setprototypeof(obj, {    proto: 'proto'})// 6.设置loop成循环引用的属性obj.loop = obj
obj对象在chrome浏览器中的结果:
2.1 浅拷贝浅拷贝: 创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址所指向的对象,肯定会影响到另一个对象。
首先我们看看一些浅拷贝的方法(详细了解可点击对应方法的超链接):
方法使用方式注意事项
object.assign() object.assign(target, ...sources)
说明:用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。 1.不会拷贝对象的继承属性;
2.不会拷贝对象的不可枚举的属性;
3.可以拷贝 symbol 类型的属性。
展开语法 let objclone = { ...obj }; 缺陷和object.assign()差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。
array.prototype.concat()拷贝数组 const new_array = old_array.concat(value1[, value2[, ...[, valuen]]]) 浅拷贝,适用于基本类型值的数组
array.prototype.slice()拷贝数组 arr.slice([begin[, end]]) 浅拷贝,适用于基本类型值的数组
这里只列举了常用的几种方式,除此之外当然还有其他更多的方式。注意,我们直接使用=赋值不是浅拷贝,因为它是直接指向同一个对象了,并没有返回一个新对象。
手动实现一个浅拷贝:
function 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;    }}// 测试const shallowcloneobj = shallowclone(obj)shallowcloneobj === obj  // false,返回的是一个新对象shallowcloneobj.arr === obj.arr  // true,对于对象类型只拷贝了引用
从上面这段代码可以看出,利用类型判断(查看typeof),针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性(for...in语句以任意顺序遍历一个对象的除symbol以外的可枚举属性,包含原型上的属性。查看for…in),基本就可以手工实现一个浅拷贝的代码了。
2.2 深拷贝深拷贝:创建一个新的对象,将一个对象从内存中完整地拷贝出来一份给该新对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。
看看现存的一些深拷贝的方法:
方法1:json.stringify()json.stringfy() 其实就是将一个 javascript 对象或值转换为 json 字符串,最后再用 json.parse() 的方法将json 字符串生成一个新的对象。(点这了解:json.stringfy()、json.parse())
使用如下:
function deepclone(target) { if (typeof target === 'object' && target !== null) { return json.parse(json.stringify(target)); } else { return target; }}// 开头的测试obj存在bigint类型、循环引用,json.stringfy()执行会报错,所以除去这两个条件进行测试const clonedobj = deepclone(obj)// 测试clonedobj === obj // false,返回的是一个新对象clonedobj.arr === obj.arr // false,说明拷贝的不是引用
浏览器执行结果:
从以上结果我们可知json.stringfy() 存在以下一些问题:
执行会报错:存在bigint类型、循环引用。
拷贝date引用类型会变成字符串。
键值会消失:对象的值中为function、undefined、symbol 这几种类型,。
键值变成空对象:对象的值中为map、set、regexp这几种类型。
无法拷贝:不可枚举属性、对象的原型链。
补充:其他更详细的内容请查看官方文档:json.stringify()
由于以上种种限制条件,json.stringfy() 方式仅限于深拷贝一些普通的对象,对于更复杂的数据类型,我们需要另寻他路。
方法2:递归基础版深拷贝手动递归实现深拷贝,我们只需要完成以下2点即可:
对于基础类型,我们只需要简单地赋值即可(使用=)。
对于引用类型,我们需要创建新的对象,并通过遍历键来赋值对应的值,这个过程中如果遇到 object 类型还需要再次进行遍历。
function deepclone(target) { if (typeof target === 'object' && target) { let cloneobj = {} for (const key in target) { // 遍历 const val = target[key] if (typeof val === 'object' && val) { cloneobj[key] = deepclone(val) // 是对象就再次调用该函数递归 } else { cloneobj[key] = val // 基本类型的话直接复制值 } } return cloneobj } else { return target; }}// 开头的测试obj存在循环引用,除去这个条件进行测试const clonedobj = deepclone(obj)// 测试clonedobj === obj // false,返回的是一个新对象clonedobj.arr === obj.arr // false,说明拷贝的不是引用
浏览器执行结果:
该基础版本存在许多问题:
不能处理循环引用。
只考虑了object对象,而array对象、date对象、regexp对象、map对象、set对象都变成了object对象,且值也不正确。
丢失了属性名为symbol类型的属性。
丢失了不可枚举的属性。
原型上的属性也被添加到拷贝的对象中了。
如果存在循环引用的话,以上代码会导致无限递归,从而使得堆栈溢出。如下例子:
const a = {}const b = {}a.b = bb.a = adeepclone(a)
对象 a 的键 b 指向对象 b,对象 b 的键 a 指向对象 a,查看a对象,可以看到是无限循环的:
对对象a执行深拷贝,会出现死循环,从而耗尽内存,进而报错:堆栈溢出
如何避免这种情况呢?一种简单的方式就是把已添加的对象记录下来,这样下次碰到相同的对象引用时,直接指向记录中的对象即可。要实现这个记录功能,我们可以借助 es6 推出的 weakmap 对象,该对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。(weakmap相关见这:weakmap)
针对以上基础版深拷贝存在的缺陷,我们进一步去完善,实现一个完美的深拷贝。
方法3:递归完美版深拷贝对于基础版深拷贝存在的问题,我们一一改进:
存在的问题改进方案
1. 不能处理循环引用 使用 weakmap 作为一个hash表来进行查询
2. 只考虑了object对象 当参数为 date、regexp 、function、map、set,则直接生成一个新的实例返回
3. 属性名为symbol的属性
4. 丢失了不可枚举的属性 针对能够遍历对象的不可枚举属性以及 symbol 类型,我们可以使用 reflect.ownkeys()
注:reflect.ownkeys(obj)相当于[...object.getownpropertynames(obj), ...object.getownpropertysymbols(obj)]
4. 原型上的属性 object.getownpropertydescriptors()设置属性描述对象,以及object.create()方式继承原型链
代码实现:
function deepclone(target) { // weakmap作为记录对象hash表(用于防止循环引用) const map = new weakmap() // 判断是否为object类型的辅助函数,减少重复代码 function isobject(target) { return (typeof target === 'object' && target ) || typeof target === 'function' } function clone(data) { // 基础类型直接返回值 if (!isobject(data)) { return data } // 日期或者正则对象则直接构造一个新的对象返回 if ([date, regexp].includes(data.constructor)) { return new data.constructor(data) } // 处理函数对象 if (typeof data === 'function') { return new function('return ' + data.tostring())() } // 如果该对象已存在,则直接返回该对象 const exist = map.get(data) if (exist) { return exist } // 处理map对象 if (data instanceof map) { const result = new map() map.set(data, result) data.foreach((val, key) => { // 注意:map中的值为object的话也得深拷贝 if (isobject(val)) { result.set(key, clone(val)) } else { result.set(key, val) } }) return result } // 处理set对象 if (data instanceof set) { const result = new set() map.set(data, result) data.foreach(val => { // 注意:set中的值为object的话也得深拷贝 if (isobject(val)) { result.add(clone(val)) } else { result.add(val) } }) return result } // 收集键名(考虑了以symbol作为key以及不可枚举的属性) const keys = reflect.ownkeys(data) // 利用 object 的 getownpropertydescriptors 方法可以获得对象的所有属性以及对应的属性描述 const alldesc = object.getownpropertydescriptors(data) // 结合 object 的 create 方法创建一个新对象,并继承传入原对象的原型链, 这里得到的result是对data的浅拷贝 const result = object.create(object.getprototypeof(data), alldesc) // 新对象加入到map中,进行记录 map.set(data, result) // object.create()是浅拷贝,所以要判断并递归执行深拷贝 keys.foreach(key => { const val = data[key] if (isobject(val)) { // 属性值为 对象类型 或 函数对象 的话也需要进行深拷贝 result[key] = clone(val) } else { result[key] = val } }) return result } return clone(target)}// 测试const clonedobj = deepclone(obj)clonedobj === obj // false,返回的是一个新对象clonedobj.arr === obj.arr // false,说明拷贝的不是引用clonedobj.func === obj.func // false,说明function也复制了一份clonedobj.proto // proto,可以取到原型的属性
详细的说明见代码中的注释,更多测试希望大家自己动手尝试验证一下以加深印象。
在遍历 object 类型数据时,我们需要把 symbol 类型的键名也考虑进来,所以不能通过 object.keys 获取键名或 for...in 方式遍历,而是通过reflect.ownkeys()获取所有自身的键名(getownpropertynames 和 getownpropertysymbols 函数将键名组合成数组也行:[...object.getownpropertynames(obj), ...object.getownpropertysymbols(obj)]),然后再遍历递归,最终实现拷贝。
浏览器执行结果:
可以发现我们的cloneobj对象和原来的obj对象一模一样,并且修改cloneobj对象的各个属性都不会对obj对象造成影响。其他的大家再多尝试体会哦!
【相关推荐:javascript视频教程、编程视频】
以上就是一文带你详细了解javascript中的深拷贝的详细内容。
其它类似信息

推荐信息