资讯专栏INFORMATION COLUMN

JavaScript对象深拷贝/浅拷贝遇到的坑和解决方法

atinosun / 3123人阅读

摘要:在以上讨论和研究结束后,同学向我推荐了一个库,测试了一下该库存在方法,实现深拷贝更为完整和精致,前文问题均没有在该方法内被发现,在这里提一波。

如果本文对您有任何帮助或者您有任何想要提出的意见或问题,请在本文下方回复,诚挚欢迎各位参与讨论,望各位不吝指教。
原载自己的小博客 JavaScript对象拷贝遇到的坑和解决方法 | 手柄君的小阁,所以无耻地算原创吧

近期参与某集训,JavaScript,遇到一对象拷贝问题,得到需求:
给一个对象,请编写一个函数,使其可以拷贝一个对象,返回这个拷贝得到的新对象:
举例如下:

function clone(obj){
    //DO SOMETHING
    return newObject; //返回拷贝得到的新对象
}

首先想到解法如下:

> ES6解构赋值(浅拷贝):
function clone(obj){
    return {...obj};
}

得到新对象为原始对象浅拷贝,即属性Key一致,值如果是数或者字符串则值传递,否则为地址传递,即Value引用和源对象一致,可根据下方运行测试:

var a = {a:1, b:2, c:3, d:[0, 1, 2]}
var b = clone(a);
console.log(b.d[1]); //1
b.d[1] = 2;
console.log(b.d[1]); //2
console.log(a.d[1]); //2

对复制后的对象中包含的数组或者对象进行编辑,影响了源对象,这显然不是我们想要的结果,但是在对象内不包含数组或对象时,该方法不失为一个快速创建对象拷贝的实用方法。
在ES6中,Object提供了一个 assign() 方法,也可以实现相同效果

> ES6 Object.assign()(浅拷贝):
function clone(obj){
    return Object.assign({},obj);
}

运行效果和前一种方式基本一致,根据MDN描述,Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,允许至少两个参数,第一个参数为拷贝的目标对象,在方法执行结束后会被返回,其余参数将作为拷贝来源。
前面两种方法均为浅拷贝,那么对于对象内包含对象或数组的对象,我们该怎样拷贝呢?
我们的老师提供了一种方法如下,缺陷稍后再谈

> For...in遍历并递归(深拷贝):
function clone(obj) {
    var newobj = obj.constructor === Array ? [] : {};
    if (typeof obj !== "object") {
        return obj;
    } else {
        for (var i in obj) {
            newobj[i] = typeof obj[i] === "object" ? clone(obj[i]) : obj[i];
        }
    }
    return newobj;
}

同样使用前文中的测试数据:

var a = {a:1, b:2, c:3, d:[0, 1, 2]}
var b = clone(a);
console.log(b.d[1]); //1
b.d[1] = 2;
console.log(b.d[1]); //2
console.log(a.d[1]); //1

可见该方法可以正确地对对象进行深拷贝,并根据参数类型为数组或对象进行进行判断并分别处理,但是该方法有一定缺陷:

1,在存在Symbol类型属性key时,无法正确拷贝,可以尝试以下测试数据:

var sym = Symbol();
var a = {a:1, b:2, c:3, d:[0, 1, 2], [sym]:"symValue"}
var b = clone(a);
b.d[1] = 2;
console.log(b.d[1]); //2
console.log(a.d[1]); //1
console.log(a[sym]); //"symValue"
console.log(b[sym]); //undefined

可以发现拷贝得到的对象b,不存在Symbol类型对象为属性名的属性。
那么可以发现,问题主要出在For...in遍历属性无法获得Symbol类型Key导致,那么有什么方法可以遍历到这些呢?
在ES6中Reflect包含的静态方法ownKeys() 可以获取到这些key,根据MDN描述,这个方法获取到的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。

那么使用ES6解构赋值和Reflect.ownKeys() 组合使用,改写上文函数,得到:

> ES6解构赋值 & Reflect.ownKeys() 遍历并递归(深拷贝):
function clone(obj) {
    var newobj = obj.constructor === Array ? [...obj] : {...obj};
    if (typeof obj !== "object") {
        return obj;
    } else {
        Reflect.ownKeys(newobj).forEach(i => {
            newobj[i] = typeof obj[i] === "object" ? clone(obj[i]) : obj[i];
        });
    }
    return newobj;
}

运行相同的测试语句:

var sym = Symbol();
var a = {a:1, b:2, c:3, d:[0, 1, 2], [sym]:"symValue"}
var b = clone(a);
b.d[1] = 2;
console.log(b.d[1]); //2
console.log(a.d[1]); //1
console.log(a[sym]); //"symValue"
console.log(b[sym]); //"symValue"
b[sym] = "newValue";
console.log(a[sym]); //"symValue"
console.log(b[sym]); //"newValue"

可以发现Symbol类型的key也被正确拷贝并赋值了,但是该方法依然有一定问题,如下:

2,在对象内部存在环时,堆栈溢出,尝试运行以下测试语句:

var a = { info: "a", arr: [0, 1, 2] };
var b = { data: a, info: "b", arr: [3, 4, 5] };
a.data = b;
var c = clone(a); //Error: Maximum call stack size exceeded. 报错:堆栈溢出

解决这个的方法稍后再讲,但目前来看已有的两种深拷贝方法足够平时使用,接下来正好提一下,ES5.1中包含的JSON对象,使用该对象亦可对对象进行深拷贝,会遇到的问题和第一种深拷贝方式一样,无法记录Symbol为属性名的属性,另外只能包含能用JSON字符串表示的数据类型,实现代码如下:

> JSON对象转义(深拷贝):
function clone(obj) {
    return JSON.parse(JSON.stringify(obj);
}

JSON.stringify() 首先将对象序列化为字符串,再由JSON.parse() 反序列化为对象,形成新的对象。
回到前面提到的问题2,如果对象内包含环,怎么办,我的实现思路为使用两个对象作为类似HashMap,记录源对象的结构,并在每层遍历前检查对象是否已经被拷贝过,如果是则重新指向到拷贝好的对象,防止无限递归。实现代码如下(配有注释):

> Map记录并递归(深拷贝)

/**
 * 深拷贝(包括Symbol)
 * @param {Object} obj
 */
function clone(obj) {
    const map = {}; //空对象,记录源对象
    const mapCopy = {}; //空对象,记录拷贝对象
    /**
     * 在theThis对象中,查找e对象的key,如果找不到,返回false
     * @param {Object} e 要查找的对象
     * @param {Object} theThis 在该对象内查找
     * @returns {symbol | boolean}
     */
    function indexOfFun(e, theThis) {
        let re = false;
        for (const key of Reflect.ownKeys(theThis)) {
            if (e === theThis[key]) {
                re = key;
                break;
            }
        }
        return re;
    }
    /**
     * 在Map对象中,查找e对象的key
     * @param {Object} e 
     */
    const indexOfMap = e => indexOfFun(e, map);
    /**
     * 在Map中记录obj对象内所有对象的地址
     * @param {Object} obj 要被记录的对象
     */
    function bindMap(obj) {
        map[Symbol()] = obj;
        Reflect.ownKeys(obj).forEach(key => {
            //当属性类型为Object且还没被记录过
            if (typeof obj[key] === "object" && !indexOfMap(obj[key])) {
                bindMap(obj[key]); //记录这个对象
            }
        });
    }
    bindMap(obj);
    /**
     * 拷贝对象
     * @param {Object} obj 要被拷贝的对象
     */
    function copyObj(obj) {
        let re;//用作返回
        if (Array.isArray(obj)) {
            re = [...obj]; //当obj为数组
        } else {
            re = { ...obj }; //当obj为对象
        }
        mapCopy[indexOfMap(obj)] = re; //记录新对象的地址
        Reflect.ownKeys(re).forEach(key => { //遍历新对象属性
            if (typeof re[key] === "object") { //当属性类型为Object
                if (mapCopy[indexOfMap(re[key])]) { //当属性已经被拷贝过
                    re[key] = mapCopy[indexOfMap(re[key])]; //修改属性指向到先前拷贝好的对象
                } else {//当属性还没有被拷贝
                    re[key] = copyObj(re[key]); //拷贝这个对象,并将属性指向新对象
                }
            }
        });
        return re; //返回拷贝的新对象
    }
    return copyObj(obj); //执行拷贝并返回
}

运行前面的测试语句:

var a = { info: "a", arr: [0, 1, 2] };
var b = { data: a, info: "b", arr: [3, 4, 5] };
a.data = b;

var c = clone(a);
c.info = "c";
c.data.info = "d";
console.log(a.info); //"a"
console.log(a.data.info); //"b"
console.log(c.info); //"c"
console.log(c.data.info); //"d"

得到该函数可以正确地拷贝带环对象。

在以上讨论和研究结束后,同学向我推荐了一个库 lodash,测试了一下该库存在 _.cloneDeep() 方法,实现深拷贝更为完整和精致,前文问题均没有在该方法内被发现,在这里提一波。

如果本文对您有任何帮助或者您有任何想要提出的意见或问题,请在本文下方回复,诚挚欢迎各位参与讨论,望各位不吝指教。
本文原载于https://www.bysb.net/3113.html

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107470.html

相关文章

  • JavaScript拷贝拷贝

    摘要:引用数据类型是存放在堆内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。栈和堆的区别其实浅拷贝和深拷贝的主要区别就是数据在内存中的存储类型不同。这里,对存在子对象的对象进行拷贝的时候,就是深拷贝了。 数据类型 在开始拷贝之前,我们从JavaScript的数据类型和内存存放地址讲起。数据类型分为基本数据类型 和引用数据类型 基本数据类型主要包括undefin...

    娣辩孩 评论0 收藏0
  • JavaScript拷贝实现的方法

    摘要:相信人很多学习的过程中都踩了深拷贝和浅拷贝的坑,深拷贝和浅拷贝的区别我就不再赘述了,今天我来写一下我自己实现深拷贝的各种方法。中的深拷贝也是用类似方法实现。 相信人很多学习js的过程中都踩了深拷贝和浅拷贝的坑,深拷贝和浅拷贝的区别我就不再赘述了,今天我来写一下我自己实现深拷贝的各种方法。 比较简单的拷贝方式可以借用浏览器的Json对象去实现,先把对象转化为json字符串,在解析回对...

    Vicky 评论0 收藏0
  • 记录一下最近在学的拷贝

    摘要:两者享有相同的引用。深拷贝这个问题通常可以通过来解决。深浅拷贝也可以使用的方法,注意使用合并返回值 前言 最近写代码经常用到深浅拷贝,从一开始的闷头使用渐渐想要深究其理,这篇文章记录一下我的认为,有所不足,恭请指正 我们可以先看看一个常遇到的一个小问题 let a = { age:1 } let b = a a.age = 2 console.log(b.age) //2 ...

    cpupro 评论0 收藏0
  • 「前端面试题系列9」拷贝拷贝的含义、区别及实现(文末有岗位内推哦~)

    摘要:深拷贝与浅拷贝的出现,就与这两个数据类型有关。这时,就需要用浅拷贝来实现了。数据一但过多,就会有递归爆栈的风险。这个方法是在解决递归爆栈问题的基础上,加以改进解决循环引用的问题。但如果你并不想保持引用,那就改用用于解决递归爆栈即可。 前言 这是前端面试题系列的第 9 篇,你可能错过了前面的篇章,可以在这里找到: 数组去重(10 种浓缩版) JavaScript 中的事件机制(从原生到...

    caige 评论0 收藏0
  • JavaScript中的拷贝拷贝

    摘要:所以,深拷贝是对对象以及对象的所有子对象进行拷贝实现方式就是递归调用浅拷贝对于深拷贝的对象,改变源对象不会对得到的对象有影响。 为什么会有浅拷贝与深拷贝什么是浅拷贝与深拷贝如何实现浅拷贝与深拷贝好了,问题出来了,那么下面就让我们带着这几个问题去探究一下吧! 如果文章中有出现纰漏、错误之处,还请看到的小伙伴多多指教,先行谢过 以下↓ 数据类型在开始了解 浅拷贝 与 深拷贝 之前,让我们先...

    546669204 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<