前端面试题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之间相同的属性是直接覆盖的,如果属性值为对象,是不会对对象之间的属性进行合并的。

  • 源于Object.assign() 方法——原作者地址:javascript对象的浅拷贝、深拷贝和Object.assign方法浅析
三、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"}
        // }

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

  • 关于JSON.parse(JSON.stringify(obj))方法原作者地址:https://juejin.im/post/5df0545ee51d4557ed542146
四、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);

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

你可能感兴趣的:(前端面试题JavaScript- 如何深拷贝一个对象)