【译】如何在JavaScript中复制Object

2020-06-28 11:07:10 浏览数 (1)

原文地址:How to Copy Objects in JavaScriptundefined作者: Scott Robinson 日期: 2019-04-17

介绍

不管在什么编程语言中,复制一个对象的值而不是它的引用都是一个十分常见的工作。复值对象的值和复制对象的引用的区别在与通过复制值可以得到两个有着相同值或数据,但是毫不相干的对象,复制引用意味着得到的两个对象在内存中指向相同的数据块。当objet A和object B都引用自相同的底层数据时,只要你操作object A,就会修改到object B。

在这篇文章我会介绍几种在JavaScript中复制对象值的方法,我会向你演示如何利用第三方库实现对象值的复制,也会提供一个自己实现的复制函数。

注意:由于Node.js运行在V8引擎中,以下给出的复制方法也可以在Node.js中执行。

第三方库

有好几种很受欢迎的库都是函数式的风格,接下来几节中将会介绍到。独自编写这些代码并不容易,能用到这些库是非常有帮助的。

Lodash

Lodash根据不同的使用场景提供了好几种复制对象的方法。

最通用的方法是clone(),它实现了对象的浅拷贝,把对象最为参数传入就可以得到复制的值:

代码语言:javascript复制
const _ = require('lodash');

let arrays = {first: [1, 2, 3], second: [4, 5, 6]};  
let copy = _.clone(arrays);  
console.log(copy);  
代码语言:javascript复制
{ first: [ 1, 2, 3 ], second: [ 4, 5, 6 ] }

浅拷贝意味着顶层object(或者array、buffer、map等)被复制了,但是对象里的object(深层)只是复制了它引用。见下面代码:

代码语言:javascript复制
const _ = require('lodash');

let arrays = {first: [1, 2, 3], second: [4, 5, 6]};  
let copy = _.clone(arrays);  
console.log(copy.first === arrays.first);  
代码语言:javascript复制
true 

如果你希望每一层都复制object的值,可以使用cloneDeep()代替:

代码语言:javascript复制
const _ = require('lodash');

let arrays = {first: [1, 2, 3], second: [4, 5, 6]};  
let copy = _.cloneDeep(arrays);  
console.log(copy);  
代码语言:javascript复制
{ first: [ 1, 2, 3 ], second: [ 4, 5, 6 ] }

这个方法利用递归复制了object每一层的值。运行之前的等式,我们发现原始数组和复制后的数组将不再相等。

代码语言:javascript复制
const _ = require('lodash');

let arrays = {first: [1, 2, 3], second: [4, 5, 6]};  
let copy = _.cloneDeep(arrays);  
console.log(copy.first === arrays.first);  
代码语言:javascript复制
false

Lodash还提供了其他的复制方法,包括cloneWith()cloneDeepWith()。这两个函数都接受一个叫做customizer的定制函数,用来复制值。

如果你希望加入一些自定义的复制逻辑你可以传递一个函数给Lodash。举个例子,这里有个包含了一写Date对象的object,你希望在复制的时候把它们转换成时间戳,可以这样做:

代码语言:javascript复制
const _ = require('lodash');

let tweet = {  
    username: '@ScottWRobinson',
    text: 'I didn't actually tweet this',
    created_at: new Date('December 21, 2018'),
    updated_at: new Date('January 01, 2019'),
    deleted_at: new Date('February 28, 2019'),
};
let tweetCopy = _.cloneDeepWith(tweet, (val) => {  
    if (_.isDate(val)) {
        return val.getTime();
    }
});
console.log(tweetCopy);  
代码语言:javascript复制
{ username: '@ScottWRobinson',
  text: 'I didn't actually tweet this',
  created_at: 1545372000000,
  updated_at: 1546322400000,
  deleted_at: 1551333600000 }

正如你所见,唯一发生变化的数据是Date对象,它现在已经变成了Unix时间戳。

Underscore

Underscore的clone()方法同Lodash的clone()几乎是一样的,它提供对象的浅拷贝。

代码语言:javascript复制
const _ = require('underscore');

let arrays = {first: [1, 2, 3], second: [4, 5, 6]};  
let copy = _.clone(arrays);  
console.log(copy.first === arrays.first);  
代码语言:javascript复制
true

不幸的是Underscore没有提供深拷贝对象的方法,你只能自己实现这部分逻辑(使用下文提到的方案)。

自定义方案

就像我之前提到的,因为在JavaScript中复制对象问题需要处理很多情况(以及棘手的边界情况),这对于独自承担来说会是一项挑战。

使用JSON方法

使用JSON.stringifyJSON.parse方法是一个常用的解决方案:

代码语言:javascript复制
let arrays = {first: [1, 2, 3], second: [4, 5, 6]};  
let copy = JSON.parse(JSON.stringify(arrays));  
console.log(copy);  
代码语言:javascript复制
{ first: [ 1, 2, 3 ], second: [ 4, 5, 6 ] }

组合使用JSON.stringifyJSON.parse会返回一个对象的深拷贝,对于那些易转换成JSON的对象非常好用。

再一次用上面的方法验证:

代码语言:javascript复制
console.log(copy.first === arrays.first);  
代码语言:javascript复制
false

如果你知道你的对象很容易序列化,那么这可能是一个不错的解决方案。

从头编写自己的函数

出于某种原因以上解决方案不能满足需求时,你不得不自己编写一个复制方法。

因为我不相信自己正确实现了一个完整的复制方法(读者将我的代码复制到他们的生产环境时存在风险的),我从这个gist中复制了一个函数,该函数以递归方式复制对象并且覆盖了很多在JavaScript运行中遇到的数据类型。

代码语言:javascript复制
function clone(thing, opts) {  
    var newObject = {};
    if (thing instanceof Array) {
        return thing.map(function (i) { return clone(i, opts); });
    } else if (thing instanceof Date) {
        return new Date(thing);
    } else if (thing instanceof RegExp) {
        return new RegExp(thing);
    } else if (thing instanceof Function) {
        return opts && opts.newFns ? new Function('return '   thing.toString())() : thing;
    } else if (thing instanceof Object) {
        Object.keys(thing).forEach(function (key) { newObject[key] = clone(thing[key], opts); });
        return newObject;
    } else if ([ undefined, null ].indexOf(thing) > -1) {
        return thing;
    } else {
        if (thing.constructor.name === 'Symbol') {
            return Symbol(thing.toString().replace(/^Symbol(/, '').slice(0, -1));
        }
        return thing.__proto__.constructor(thing);
    }
}

这个函数先处理特定数据类型(如数组,正则表达式,函数等),然后再处理其他数据类型(如数字,字符串,布尔值等),它通过thing的constructor来复制值。如果thing是一个对象,那么它会递归地调用自己的子属性。

查看并测试上面代码中全部数据类型和边缘情况,保证他们都被测试验证。

总结

理论上看起来很简单,但实际上用JavaScript复制对象并不简单。幸运的是,已经有很多的解决方案,比如Lodash中的cloneDeep,也可以是内置的JSON方法。如果处于某些原因,这些都不使用了,只要做过了全面的测试你也可以编写自己的复制方法。

0 人点赞