资讯专栏INFORMATION COLUMN

「前端面试题系列9」浅拷贝与深拷贝的含义、区别及实现(文末有岗位内推哦~)

caige / 2111人阅读

摘要:深拷贝与浅拷贝的出现,就与这两个数据类型有关。这时,就需要用浅拷贝来实现了。数据一但过多,就会有递归爆栈的风险。这个方法是在解决递归爆栈问题的基础上,加以改进解决循环引用的问题。但如果你并不想保持引用,那就改用用于解决递归爆栈即可。

前言

这是前端面试题系列的第 9 篇,你可能错过了前面的篇章,可以在这里找到:

数组去重(10 种浓缩版)

JavaScript 中的事件机制(从原生到框架)

理解函数的柯里化

ES6 中箭头函数的用法

this 的原理以及用法

伪类与伪元素的区别及实战

如何实现一个圣杯布局?

今日头条 面试题和思路解析

面试的时候,我经常会问候选人深拷贝与浅拷贝的问题。因为它可以考察一个人的很多方面,比如基本功,逻辑能力,编码能力等等。

另外在实际工作中,也常会遇到它。比如用于页面展示的数据状态,与需要传给后端的数据包中,有部分字段的值不一致的话,就需要在传参时根据接口文档覆写那几个字段的值。

最常见的可能就是 status 这个参数了。界面上的展示需要 Boolean 值,而后端同学希望拿到的是 Number 值,1 或者 0。为了不影响展示效果,往往就需要深拷贝一下,再进行覆写,否则界面上就会因为某些值的变化,出现奇怪的现象。

至于为什么会这样,下文会讲到。马上开始今天的主题,让我们先从赋值开始说起。

赋值

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

let test1 = "chao";
let test2 = test1;

// test2: chao

test1 = "chao_change";

// test2: chao
// test1: chao_change

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

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

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

浅拷贝

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

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

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

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

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

const arr = [{a:1,b:2}, {a:3,b:4}];
const newArr = [].concat(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: 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 内部用了递归的方式。数据一但过多,就会有递归爆栈的风险。

// Maximum call stack size exceeded
深拷贝的终极方案

有位大佬给出了深拷贝的终极方案,利用了“栈”的思想。

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(用于解决递归爆栈)即可。有兴趣的同学,可以前往 深拷贝的终极探索(90%的人都不知道),查看更多的细节。

总结

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

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

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

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

岗位内推

莉莉丝游戏招 中高级前端工程师 啦!!!

你玩过《小冰冰传奇([刀塔传奇])》么?你玩过《剑与家园》么?

你想和 薛兆丰老师 成为同事么?有兴趣的同学,可以 关注下面的公众 号加我微信 详聊哈~

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

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

相关文章

  • 「读懂源码系列3」lodash 是如何实现拷贝(上)

    摘要:上对位运算的解释是它经常被用来创建处理以及读取标志位序列一种类似二进制的变量。位运算,常用于处理同时存在多个布尔选项的情形。掩码中的每个选项的值都是的幂,位运算是位的。位运算,说白了就是直接对某个数据在内存中的二进制位,进行运算操作。 showImg(https://segmentfault.com/img/bVbrC56?w=2208&h=1242); 前言 上一篇文章 「前端面试题...

    flyer_dev 评论0 收藏0
  • 前端面试 -- JavaScript (一)

    摘要:前言前两天总结了一下方面的面试题传送门,今天翻看了一些面试中常见的几个问题只是一部分,会持续更新,分享给有需要的小伙伴,欢迎关注如果文章中有出现纰漏错误之处,还请看到的小伙伴留言指正,先行谢过以下有哪些数据类型种原始数据类型布尔表示一个逻辑 前言 前两天总结了一下HTML+CSS方面的面试题 (传送门),今天翻看了一些 JavaScript 面试中常见的几个问题(只是一部分,会持续更新...

    junnplus 评论0 收藏0
  • 2018大厂高级前端面试汇总

    摘要:面试的公司分别是阿里网易滴滴今日头条有赞挖财沪江饿了么携程喜马拉雅兑吧微医寺库宝宝树海康威视蘑菇街酷家乐百分点和海风教育。 (关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导) 本人于7-8月开始准备面试,过五关斩六将,最终抱得网易归,深深感受到高级前端面试的套路。以下是自己整理的面试题汇总,不敢藏私,统统贡献出来。 面试的公司分...

    zzir 评论0 收藏0
  • 「读懂源码系列4」lodash 是如何实现拷贝(下)

    摘要:用于检测自己是否在自己的原型链上如果是函数,则取出该函数的原型对象否则,取出对象的原型对象其中,的判断,是为了确定的类型是对象或数组。相当于,而的构造函数是一个函数对象。 showImg(https://segmentfault.com/img/bVbq2N1?w=640&h=437); 前言 接着上一篇文章 lodash 是如何实现深拷贝的(上),今天会继续解读 _.cloneDee...

    zombieda 评论0 收藏0

发表评论

0条评论

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