zoukankan      html  css  js  c++  java
  • 【JavaScript】深拷贝和浅拷贝的总结(含义、区别及实现)

    前言:

    很多人以为深拷贝和浅拷贝是只出现在面试里的问题,其实不是的,在实际工作中,也常会遇到它。比如用于页面展示的数据状态,与需要传给后端的数据包中,有部分字段的值不一致的话,就需要在传参时根据接口文档覆写那几个字段的值。

    最常见的可能就是 status 这个参数了。界面上的展示需要 Boolean 值,而后端接口希望拿到的是 Number 值,1 或者 0。为了不影响展示效果,往往就需要深拷贝一下,再进行覆写,否则界面上就会因为某些值的变化,出现奇怪的现象。当然你也可以再加个变量联动,但是这个方法太笨了,不推荐使用(虽然我喜欢使用哈哈哈),利用深拷贝会让你方便得多。

    让我们先从赋值开始说起。

    赋值

    Javascript 的原始数据类型有这几种:Boolean、Null、Undefined、Number、String、Symbol(ES6)。它们的赋值很简单,且赋值后两个变量互不影响。

    let test1 = 'shijian';
    let test2 = test1;
    
    // test2: shijian
    
    test1 = 'shijian_change';
    
    // test2: shijian
    // test1: shijian_change

    另外的引用数据类型有:Object 和 Array。深拷贝与浅拷贝的出现,就与这两个数据类型有关。

    1 const obj = {a:1, b:2};
    2 const obj2 = obj;
    3 obj2.a = 3;
    4 console.log(obj.a); // 3

    依照赋值的思路,对 Object 引用类型进行拷贝,就会出问题。很多情况下,这不是我们想要的。这时,就需要用浅拷贝来实现了。

    浅拷贝

    什么是浅拷贝?可以这么理解:创建一个新的对象,把原有的对象属性值,完整地拷贝过来。其中包括了原始类型的值,还有引用类型的内存地址。

    让我们用 Object.assign 来改写一下上面的例子:

    1 const obj = {a:1, b:2};
    2 const obj2 = Object.assign({}, obj);
    3 obj2.a = 3;
    4 console.log(obj.a); // 1

    Ok,改变了 obj2 的 a 属性,但 obj 的 a 并没有发生变化,这正是我们想要的。

    可是,这样的拷贝还有瑕疵,再改一下例子:

    1 const arr = [{a:1,b:2}, {a:3,b:4}];
    2 const newArr = [].concat(arr);
     
    4 newArr.length = 1; // 为了方便区分,只保留新数组的第一个元素
    5 console.log(newArr); // [{a:1,b:2}]
    6 console.log(arr); // [{a:1,b:2},{a:3,b:4}]
     
    8 newArr[0].a = 123; // 修改 newArr 中第一个元素的a
    9 console.log(arr[0]); // {a: 123, b: 2},竟然把 arr 的第一个元素的 a 也改了

    oh,no!这不是我们想要的...

    经过一番查找,才发现:原来,对象的 Object.assign(),数组的 Array.prototype.slice() 和 Array.prototype.concat(),还有 ES6 的 扩展运算符,都有类似的问题,它们都属于 浅拷贝。这一点,在实际工作中处理数据的组装时,要格外注意。

    所以,浅拷贝应该这样定义:只拷贝第一层的原始类型值,和第一层的引用类型地址

    深拷贝

    我们当然希望当拷贝多层级的对象时,也能实现互不影响的效果。所以,深拷贝的概念也就油然而生了。

    深拷贝应该定义为:拷贝所有的属性值,以及属性地址指向的值的内存空间

    也就是说,当遇到对象时,就再新开一个对象,然后将第二层源对象的属性值,完整地拷贝到这个新开的对象中。

    按照浅拷贝的思路,很容易就想到了递归调用。所以,就自己封装了个深拷贝的方法:

    function deepClone(obj) {
        if(!obj && typeof obj !== 'object'){
            return;
        }
        var newObj= toString.call(obj) === '[object Array]' ? [] : {};
        for (var key in obj) {
            if (obj[key] && typeof obj[key] === 'object') {
                newObj[key] = deepClone(obj[key]);
            } else {
                newObj[key] = obj[key];
            }
        }
        return newObj;
    }

    刚才的例子再试试看:

    let arr = [{a:1,b:2}, {a:3,b:4}];
    let newArr = deepClone(arr);
    
    newArr.length = 1; // 为了方便区分,只保留新数组的第一个元素
    console.log(newArr); // [{a:1, b:2}]
    console.log(arr); // [{a:1, b:2}, {a:3, b:4}]
    
    newArr[0].a = 123; // 修改 newArr 中第一个元素的 a
    console.log(arr[0]); // {a:1, b:2}

    ok,这下搞定了。

    不过,这个方法貌似会存在 引用丢失 的的问题。比如这样:

    var b = {};
    var a = {a1: b, a2: b};
    
    a.a1 === a.a2 // true
    
    var c = clone(a);
    c.a1 === c.a2 // false

    如果我们的需求是,应该丢失引用,那就可以用这个方法。反之,就得想办法解决。

    一行代码的深拷贝

    当然,还有最简单粗暴的深拷贝方法,就是利用 JSON 了。像这样(接上面代码):

    let newArr2 = JSON.parse(JSON.stringify(arr));
    console.log(arr[0]); // {a:1, b:2}

    newArr2[0].a = 123; console.log(arr[0]); // {a:1, b:2}

    但是,JSON 内部用了递归的方式。数据一但过多,就会有递归爆栈的风险。

    深拷贝的终极方案

    在网上看到有位大神给出了深拷贝的终极方案,利用了“栈”的思想。

    function cloneForce(x) {
        // 用来去重
        const uniqueList = [];
    
        let root = {};
    
        // 循环数组
        const loopList = [
            {
                parent: root,
                key: undefined,
                data: x,
            }
        ];
    
        while(loopList.length) {
            // 深度优先
            const node = loopList.pop();
            const parent = node.parent;
            const key = node.key;
            const data = node.data;
    
            // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
            let res = parent;
            if (typeof key !== 'undefined') {
                res = parent[key] = {};
            }
    
            // 数据已经存在
            let uniqueData = uniqueList.find((item) => item.source === data );
            if (uniqueData) {
                parent[key] = uniqueData.target;
                // 中断本次循环
                continue;
            }
    
            // 数据不存在
            // 保存源数据,在拷贝数据中对应的引用
            uniqueList.push({
                source: data,
                target: res,
            });
    
            for(let k in data) {
                if (data.hasOwnProperty(k)) {
                    if (typeof data[k] === 'object') {
                        // 下一次循环
                        loopList.push({
                            parent: res,
                            key: k,
                            data: data[k],
                        });
                    } else {
                        res[k] = data[k];
                    }
                }
            }
        }
    
        return root;
    }

    其思路是:引入一个数组 uniqueList 用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在 uniqueList 中了,如果在的话就不执行拷贝逻辑了。

    这个方法是在解决递归爆栈问题的基础上,加以改进解决循环引用的问题。但如果你并不想保持引用,那就改用 cloneLoop(用于解决递归爆栈)即可。有兴趣的同学,可以前往 https://juejin.im/post/5bc1ae9be51d450e8b140b0c,查看更多的细节。

    总结

    所谓深拷贝与浅拷贝,指的是 Object 和 Array 这样的引用数据类型。

    浅拷贝,只拷贝第一层的原始类型值,和第一层的引用类型地址。

    深拷贝,拷贝所有的属性值,以及属性地址指向的值的内存空间。通过递归调用,或者 JSON 来做深拷贝,都会有一些问题。而 cloneForce 方法倒是目前看来最完美的解决方案了。

    在日常的工作中,我们要特别注意,对象的 Object.assign()数组的 Array.prototype.slice() 和 Array.prototype.concat(),还有 ES6 的 扩展运算符,都属于浅拷贝。当需要做数据组装时,一定要用深拷贝,以免影响界面展示效果。

     

  • 相关阅读:
    [Java] 计算两个日期之间的差(年 月 日)
    Javassist library is missing in classpath! Please add missed dependenc
    $_SERVER['SCRIPT_FILENAME'] 与 __FILE__ 区别
    内存管理一
    内存管理四
    内存管理二
    内存分配函数分类
    内存映像文件
    内存管理三
    到底有多少内存
  • 原文地址:https://www.cnblogs.com/shijianblog/p/12530046.html
Copyright © 2011-2022 走看看