前端面试题JavaScript- 如何深拷贝一个对象

在实际开发中,我们经常会遇到原对象不能修改,但是又需要用到里面的数据,比如原对象里面的值是一个数组,我们要将他拼接成一个字符串。
这个时候如果可以深拷贝一个对象,用拷贝出来的这个对象进行操作就会非常方便,这也是前端面试中,经常会遇到的题,可以考验候选人,对于js的知识是否掌握的过硬。

对象的拷贝,可以分为深拷贝和浅拷贝。

目前自己总结的对象拷贝的方法:

  1. 直接赋值 obj2 = obj1
  2. Object对象上的:Object.assign() 方法
  3. JSON 对象上的:JSON.parse(JSON.stringify(obj)) 方法
  4. for in 循环遍历对象的每一个键
  5. 更复杂的自定义方法
  6. 第三方库:比如 lodash 的cloneDeep

浅拷贝

很多情况我们只要实现对象的浅拷贝就好了,并不需要真的进行深拷贝,但是在进行浅拷贝的时候,一定是经过思考的,比如我们就想用里面的简单数据类型而已,不会对里面的引用值进行修改,否则运行结果可能会在你的意料之外。

一、直接赋值 obj2 = obj1

由于对象的值是存在内存的堆中的,直接赋值方法,新的对象指向的是原对象在内存中的地址而已,这个时候,改变新对象的属性值,会通过引用地址去找到内存中的真正的值。导致原对象的值也就改变了,这个直接赋值的方法,真的就是原对象多了个名字而已,就好像这个人有个原名字叫 李雷,你又给他起了个名字叫 小雷。

二、 Object对象上的:Object.assign() 方法

MDN上这样介绍Object.assign(),'Object.assign() 方法用于将所有可枚举的属性的值从一个或多个源对象复制到目标对象。它将返回目标对象',好吧,并看不出是深拷贝还是浅拷贝,我们来测试一下

let srcObj = {'name': 'lilei', 'age': '20'};
let copyObj2 = Object.assign({}, srcObj, {'age': '21'});
copyObj2.age = '23';
console.log('srcObj', srcObj); //{ name: 'lilei', age: '22' }
看起来好像是深拷贝了,那其实这里let copyObj2 = Object.assign({}, srcObj, {'age': '21'}); 我们把srcObj 给了一个新的空对象。同样目标对象为 {},我们再来测试下:

srcObj = {'name': '明', grade: {'chi': '50', 'eng': '50'} };
copyObj2 = Object.assign({}, srcObj);
copyObj2.name = '红';
copyObj2.grade.chi = '60';
console.log('新 objec srcObj', srcObj); // { name: '明', grade: { chi: '60', eng: '50' } }
从例子中可以看出,改变复制对象的name 和 grade.chi ,源对象的name没有变化,但是grade.chi却被改变了。因此我们可以看出Object.assign()拷贝的只是属性值,假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。
也就是说,对于Object.assign()而言, 如果对象的属性值为简单类型(string, number),通过Object.assign({},srcObj);得到的新对象为‘深拷贝’;如果属性值为对象或其它引用类型,那对于这个对象而言其实是浅拷贝的。这是Object.assign()特别值得注意的地方。
多说一句,Object.assign({}, src1, src2); 对于scr1和src2之间相同的属性是直接覆盖的,如果属性值为对象,是不会对对象之间的属性进行合并的。

三、JSON 对象上的:JSON.parse(JSON.stringify(obj)) 方法

摘选
JSON.parse(JSON.stringify(obj))一般用来实现深拷贝:
1. JSON.parse()将对象序列化(JSON字符串);
2. JSON.string()实现反序列化(还原)js对象
复制代码
注意一、obj里面有时间对象,则JSON.stringiify后再JSON.parse的结果,时间将只是字符串的形式。而不是时间对象

    var test = {
        name: 'a',
        date: [new Date(1536627600000), new Date(1540047600000)],
    }
    let b;
    b = JSON.parse(JSON.stringify(test))
    
    1. console.log(b)
        // b = {
        //      name: "a",
        //      data: [
        //          "2018-09-11T01:00:00.000Z",
        //          "2018-10-20T15:00:00.000Z"
        //          ]
        //  }
        (typeof b.date[0])  =>  string
    
    2. console.log(test)  
        // test = {
        //      name: "a",
        //      data: [
        //          "Tue Sep 11 2018 09:00:00 GMT+0800 (中国标准时间)",
        //          "Sat Oct 20 2018 23:00:00 GMT+0800 (中国标准时间)",
        //      ]
        // }
        (typeof test.date[0])   =>  object

注意二、如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象

        name: "a",
        date: new RegExp('\w+')
    }
    const copyed = JSON.parse(JSON.stringify(test));
    test.name = 'test';
    console.error('ddd', test, copyed)
    
    1. console.log(copyed)
        // copyed = {
        //      name: "a",
        //      data: {}
        //  }
    
    2. console.log(test)  
        // test = {
        //      name: "test",
        //      data: new RegExp('\w+')
        // }

注意三、如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;

    const test = {
        name: 'a',
        date: function foo(){
            console.log('haha')
        },
        a: undefined
    }
    const copyed = JSON.parse(JSON.stringify(test))
    
    1. console.log(copyed)
        // copyed = {
        //      name: "a"
        //  }
    
    2. console.log(test)  
        // test = {
        //      name: "test",
        //      date: function foo(){
        //          console.log('haha')
        //      },
        //      a: undefined
        // }

注意四、如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null

        // test = {
        //      name: "test",
        //      date: null   // 即 NaN返回null
        // }

注意五、JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的,则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;

    function Person(name) {
        this.name = name;
        // console.log(name)
    }
    const liai = new Person('liai');
    const test = {
        name: 'a',
        date: liai,
    };
    const copyed = JSON.parse(JSON.stringify(test));
    test.name = 'test'
    console.log(test, copyed)
    
    1. console.log(copyed)
        // copyed = {
        //      name: "a",
        //      data: {name: "liai"}
        //  }
    
    2. console.log(test)  
        // test = {
        //      name: "test",
        //      data: Person{name: "liai"}
        // }

复制代码
注意六、如果对象中存在循环引用的情况也无法正确实现深拷贝;

四、for in 循环遍历对象的每一个键

简单的for in也是只能实现对象的一级,简单数据的(字符串 、数字、布尔值、对空(Null)、未定义(Undefined)、Symbol)浅拷贝,复杂数据比如 值里面存的是一个对象,function 、array 那就无法深度拷贝。

  • 我们可以用递归 + for in 来实现一个日常开发中可以足够用的浅拷贝
(1)//自定义的复制方法

function clone(obj) {
var copy = {};
for (var attr in obj) {
copy[attr] = typeof(obj[attr])==='object' ? clone(obj[attr]) : obj[attr];
}
return copy;
}

//测试样例
var a = {v1:1, v2:2};
var b = clone(a);
b.v1 = 3;
console.log("对象a:",a);
console.log("对象b:",b);

(2)也可以直接给 Object 增加个 clone 方法,其内部实现原来同上面是一样的。

//自定义的复制方法
Object.prototype.clone = function() {
var copy = (this instanceof Array) ? [] : {};
for (var attr in this) {
if (this.hasOwnProperty(attr)){
copy[attr] = typeof(this[attr])==='object' ? clone(this[attr]) : this[attr];
}
}
return copy;
};

//测试样例
var a = {v1:1, v2:2};
var b = a.clone();
b.v1 = 3;
console.log("对象a:",a);
console.log("对象b:",b);

深拷贝

前面四个方法都无法实现深拷贝啊,那自己实现一个深拷贝真的有那么难吗?

五、 更复杂的自定义方法
function  deepClone(data) {      
        const type = this.judgeType(data);      
        let obj;      
        if (type === 'array') {
            obj = [];
        } else if (type === 'object') {
            obj = {};
        } else {    
            // 不再具有下一层次
            return data;
        }      
        if (type === 'array') {        // eslint-disable-next-line
            for (let i = 0, len = data.length; i < len; i++) {
                obj.push(this.deepClone(data[i]));
            }
        } else if (type === 'object') {        // 对原型上的方法也拷贝了....
            // eslint-disable-next-line
            for (const key in data) {
                obj[key] = this.deepClone(data[key]);
            }
        }      
        return obj;
    }
    function  judgeType(obj) {  
        // tostring会返回对应不同的标签的构造函数
        const toString = Object.prototype.toString;      
        const map = {
            '[object Boolean]': 'boolean',
            '[object Number]': 'number',
            '[object String]': 'string',
            '[object Function]': 'function',
            '[object Array]': 'array',
            '[object Date]': 'date',
            '[object RegExp]': 'regExp',
            '[object Undefined]': 'undefined',
            '[object Null]': 'null',
            '[object Object]': 'object',
        };      
        if (obj instanceof Element) {
            return 'element';
        }      
        return map[toString.call(obj)];
    }

作者:MepJia
链接:https://juejin.im/post/5df0545ee51d4557ed542146
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

六、第三方库:比如 lodash 的cloneDeep,

不想自己写就用这个,100多M的库,不用的担心,这是开发依赖而已,只在开发的时候用到这个库,上线时候会编译掉的。
官网:https://lodash.com/

  • 如果用jQuery
    jQuery 自带的 extend 方法可以用来实现对象的复制。
var a = {v1:1, v2:2};
var b = {};
$.extend(b,a);
b.v1 = 3;
console.log("对象a:",a);
console.log("对象b:",b);

如果还有其他更好的深度克隆的实现,欢迎留言交流,我也会继续查找资料。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容