通过复制所有自己的属性来克隆任何javascript对象

时间:2018-09-16 20:12:15

标签: javascript

如果我想克隆任何JavaScript对象(不为null),我想我可以使用Object.getOwnPropertyNames将其所有属性(可枚举和不可枚举)复制到一个新的空对象上。

但是我注意到Dojo工具包(https://davidwalsh.name/javascript-clone)提供的深度克隆功能的示例将RegExp,Date和Node对象视为特殊情况,而lodash.cloneDeep也具有很多逻辑,比简单地复制属性要复杂得多,包括拥有一些特殊情况,并且显然不支持所有类型的对象:(https://github.com/lodash/lodash/blob/master/.internal/baseClone.js)。

为什么仅复制对象属性是不够的?除了我不知道的属性外,JavaScript对象还有什么?

编辑:明确地说,我说的是 deep 克隆对象。抱歉造成混乱。

5 个答案:

答案 0 :(得分:1)

如果顶级属性是所有值对象(例如字符串和数字),则仅复制顶级属性对于对象的克隆就可以了。如果有任何引用对象,例如日期,数组或其他对象,那么您要做的就是将引用从一个对象复制到另一个对象。如果在克隆上更改参考对象,则将对原始对象进行突变。

https://stackblitz.com/edit/typescript-qmzgf7上查看我的克隆函数

如果它是一个数组,它将克隆该数组中的每个项目;如果它是一个日期,它将同时创建一个新日期;如果它是一个对象,则仅复制该属性就将克隆所有其他属性。

现在可以对克隆的对象进行突变,而不必担心它可能对原始对象产生的影响。

const clone = obj =>
  Array.isArray(obj)
    ? obj.map(item => clone(item))
    : obj instanceof Date
      ? new Date(obj.getTime())
      : (typeof obj === 'object') && obj
        ? Object.getOwnPropertyNames(obj).reduce((o, prop) => ({ ...o, [prop]: clone(obj[prop]) }), {})
        : obj;
        
let original = { prop1: "Original", objProp: { prop1: "Original" } };
let swallowCopy = { ...original };
let clonedObj = clone(original);

clonedObj.prop1 = "Changed";
clonedObj.objProp.prop1 = "Changed";

console.log(`Original objects properties are '${original.prop1}' and '${original.objProp.prop1}'`);

swallowCopy.prop1 = "Changed";
swallowCopy.objProp.prop1 = "Changed";

console.log(`Original objects properties are '${original.prop1}' and '${original.objProp.prop1}'`);

请注意,在对象属性浅表副本上修改属性是如何使原始属性也发生变化。

答案 1 :(得分:0)

在JS中克隆对象的最简单方法是使用...传播运算符。

假设您有这个对象:

const object = { foo: 1, bar: 2 }

要克隆它,只需声明:

const objectClone = {...object}

这会将原始对象中存在的所有属性及其值创建到克隆上。

现在的问题是,如果您有任何嵌套的对象,则将通过引用进行复制。假设原始对象是这个:

const student = { studentID: 1, tests: { test1: 90, test2: 95}}

如果使用spread运算符(或Object.assign,spread只是语法糖)创建该对象的副本,则嵌套对象实际上将指向原始对象内的对象!所以重复一遍:

const studentClone = {...student}

现在,您在克隆内编辑嵌套对象的属性:

studentClone.tests.test1 = 80

这将同时更改克隆对象和原始对象中的值,因为嵌套对象实际上只是指向内存中的1个对象。

现在,这些实用程序(如_.cloneDeep会执行的操作是遍历要克隆对象中的所有内部对象,然后重复该过程。从技术上讲,您可以自己执行此操作,但是您将无法轻松地对具有许多嵌套对象的对象执行此操作。像这样:

const studentClone = {...studentClone, tests: {...studentClone.tests}}

这将创建没有引用问题的新对象。

希望这对您有帮助!

编辑:当然,仅添加对象扩展仅适用于原型对象。每个实例化的对象(例如数组,Date对象等)都有自己的克隆方式。

可以通过[...array]类似地复制数组。它确实遵循关于引用的相同规则。对于日期,您只需将原始日期对象再次传递到Date构造函数中即可:

const clonedDate = new Date(date)

这是第三方实用程序将派上用场的地方,因为它们通常会处理大多数用例。

答案 2 :(得分:0)

This answer很好地解释了克隆普通JavaScript对象的两个问题:原型属性和循环引用。但是,要回答有关某些内置类型的问题,TL; DR的答案是,您无法以编程方式访问“内部”属性。

考虑:

let foo = [1, 2];
let bar = {};
Object.assign(bar, foo);
Object.setPrototypeOf(bar, foo.constructor.prototype); // aka Array.prototype
bar[0]; // 1
bar instanceof Array; // true
bar.map(x => x + 1); // [] ????

空数组?为什么?只是为了确保我们没有疯

foo.map(x => x + 1); // [2, 3]

map(和其他数组方法)无法工作的原因是,数组不仅仅是一个对象:它具有内部插槽属性,可用于放置您无法得到的东西视为JavaScript程序员。再举一个例子,每个JavaScript对象都有一个内部[[Class]]属性,该属性说明它是什么类型的对象。对我们来说幸运的是,规范中存在一个漏洞,可让我们间接访问它:好的Object.prototype.toString.call hack。因此,让我们来看看各种内容的含义:

 Object.prototype.toString.call(true);   // [object Boolean]
 Object.prototype.toString.call(3);      // [object Number]
 Object.prototype.toString.call({});     // [object Object]
 Object.prototype.toString.call([]);     // [object Array]
 Object.prototype.toString.call(null);   // [object Null]
 Object.prototype.toString.call(/\w/);   // [object RegExp]
 Object.prototype.toString.call(JSON);   // [object JSON]
 Object.prototype.toString.call(Math);   // [object Math]

让我们看看它对我们的foo和bar的提示:

 Object.prototype.toString.call(foo); // [object Array]
 Object.prototype.toString.call(bar); // [object Object] Doh!

无法将随机对象“转换”为Array ...或Date ...或HTMLElement ...或正则表达式。现在,实际上有克隆所有这些东西的方法,但是它们需要特殊的逻辑:您不能只复制属性,甚至不能设置原型,因为它们具有无法访问或直接复制的内部逻辑。

在通常的日常JavaScript编程中,我们不必对此太担心,这是库作者(或语言实现者)通常感兴趣的事情。我们每天的工作僵局只是使用一个库来覆盖边缘情况,并称之为一天。但是每隔一段时间,抽象就会使用泄漏和丑陋的气泡。但是,这很好地说明了为什么您可能应该使用经过考验的库而不是尝试自己使用库。

答案 3 :(得分:-1)

javascript中的对象包含字段和函数,并且每个字段都可以是另一个对象(如Date类型)。如果您复制日期字段,它将是引用类型分配。 示例:

Temperature_K

现在,如果我们这样更改“ obj2.myField”:

case class Node(key: String, value: String, var left: Node, var right: Node)

如您所见,obj1和obj2仍然链接。

复制日期字段的正确方法:

def find(key:String, tree:Node): Option[String] = {
    if(tree == null) {
        return None
    } else if (tree.key == key) {
        return Some(tree.value)
    }
    val checkLeft = find(key, tree.left)
    val checkRight = find(key, tree.right)
    if(checkLeft != None) {
        return checkLeft
    } else if(checkRight != None) {
        return checkRight
    }
    return None
}

答案 4 :(得分:-2)

大多数 native 对象(如您提到的-我不知道它们的正确命名;也许内置?)被视为“简单” ”:按属性复制Date对象没有意义。同时,它们都以某种方式可变。

... (reducer logic)
   case ADD_NEW_CUSTOMER:
      return {
        ...state,
        customersList: [...state.customersLits, action.payload]

因此,您应该明确处理这些例外情况(特殊情况),或者冒某些情况发生副作用的风险。