如何在javascript中进行深度克隆
如何在javascript中进行深度克隆
如何深度克隆JavaScript对象?
我知道有一些基于框架的函数,比如JSON.parse(JSON.stringify(o))
和$.extend(true, {}, o)
,但我不想使用那样的框架。
什么是最优雅或高效的创建深度克隆的方法。
我们关心边缘情况,比如克隆数组。不破坏原型链,处理自引用。
我们不关心支持复制DOM对象之类的,因为已经存在.cloneNode
。
由于我主要想在node.js
中使用深度克隆,因此使用V8引擎的ES5特性是可以接受的。
[编辑]
在有人建议之前,让我提到创建一个通过原型继承对象并克隆它之间存在明显差异。前者会弄乱原型链。
[进一步编辑]
阅读了您的回答后,我发现克隆整个对象是一场非常危险和困难的游戏。以闭包为基础的对象是一个很好的例子
var o = (function() { var magic = 42; var magicContainer = function() { this.get = function() { return magic; }; this.set = function(i) { magic = i; }; } return new magicContainer; }()); var n = clone(o); // 如何实现克隆以支持闭包
有没有办法编写一个克隆函数,它克隆对象,在克隆时具有相同的状态,但不能在不使用JS解析器的情况下更改o
的状态。
不再有真正需要这样的函数的现实需求了。这只是学术兴趣。
在JavaScript中如何进行深度克隆
在JavaScript中,深度克隆一个对象取决于你想要克隆的是什么。它是一个真正的JSON对象还是JavaScript中的任何对象?如果想要克隆任何对象,可能会遇到一些麻烦。什么麻烦?我将在下面解释,但首先,我们先来看一个代码示例,可以克隆对象字面量、任何基本类型、数组和DOM节点。
function clone(item) { if (!item) { return item; } // 检查null和undefined的值 var types = [ Number, String, Boolean ], result; // 如果是基本类型则进行标准化,比如new String('aaa')或者new Number('444') types.forEach(function(type) { if (item instanceof type) { result = type( item ); } }); if (typeof result == "undefined") { if (Object.prototype.toString.call( item ) === "[object Array]") { result = []; item.forEach(function(child, index, array) { result[index] = clone( child ); }); } else if (typeof item == "object") { // 判断是否为DOM节点 if (item.nodeType && typeof item.cloneNode == "function") { result = item.cloneNode( true ); } else if (!item.prototype) { // 判断是否为字面量 if (item instanceof Date) { result = new Date(item); } else { // 字面量对象 result = {}; for (var i in item) { result[i] = clone( item[i] ); } } } else { // 根据需求,保留引用或者创建新对象 if (false && item.constructor) { // 不建议这样做,为什么?请继续阅读下文 result = new item.constructor(); } else { result = item; } } } else { result = item; } } return result; } var copy = clone({ one : { 'one-one' : new String("hello"), 'one-two' : [ "one", "two", true, "four" ] }, two : document.createElement("div"), three : [ { name : "three-one", number : new Number("100"), obj : new function() { this.name = "Object test"; } } ] });
现在,让我们讨论一下在克隆真实对象时可能遇到的问题。我现在说的是通过类进行创建的对象,例如:
var User = function(){} var newuser = new User();
当然,你可以对它们进行克隆,这不是问题,每个对象都会暴露出构造函数属性,你可以使用它来克隆对象,但并不总是有效。你也可以对这些对象进行简单的for in
循环,但是同样会遇到麻烦。我在代码中还包含了克隆功能,但它被if(false)
语句排除在外。
那么,为什么克隆会很麻烦呢?首先,每个对象/实例可能都有一些状态。你永远无法确定你的对象是否有私有变量,如果是这样,在克隆对象时,你会破坏状态。
想象一下没有状态,那没问题。然后我们还有另一个问题。通过"constructor"方法进行克隆会带来另一个障碍。这是一个参数依赖的问题。你永远无法确定创建这个对象的人是否在构造函数中执行了某种操作,例如:
new User({ bike : someBikeInstance });
如果是这种情况,你就没那么幸运了,someBikeInstance可能是在某个上下文中创建的,而这个上下文对于克隆方法来说是未知的。
那么该怎么办呢?你仍然可以使用for in
解决方案,并像对待普通对象字面量一样对待这些对象,但也许最好的办法是根本不克隆这些对象,只传递对该对象的引用?
另一种解决方法是 - 你可以设定一个约定,所有需要克隆的对象应该自己实现这部分,并提供适当的API方法(比如cloneObject)。类似于DOM中的cloneNode方法。
你决定吧。
我自己也遇到了处理使用闭包隐藏状态的对象的困难。如何克隆一个对象以及它的整个状态,但同时确保克隆体无法通过自身改变原始状态。对result = new item.constructor();
不好的反驳是,根据构造函数和item对象,你应该能够解析构造函数中传递的任何参数。
如果对象使用闭包隐藏状态,那么你无法对它们进行克隆。这就是为什么叫做'闭包'的原因。正如nemisj在最后所说,最好的方法是如果可能的话,实现一个用于克隆(或序列化/反序列化)的API方法。
我有一种感觉,就是这种情况。尽管可能有人对此有一个非常聪明的解决方案。
false && item.constructor
会产生什么效果?这个if
是不是无用的?
从功能的角度来看,这个if
是'无用'的,因为它永远不会运行,但它在学术上的目的是显示一个可能尝试使用的假设实现,作者不建议使用这种方式,原因在后面的解释中已经解释过了。所以,是的,它每次评估条件时都会触发else
子句,但是存在这段代码是有原因的。
正常化Boolean
会失败,因为new Boolean(new Boolean(false)) => [Boolean: true]
。这样对于布尔值来说,返回item.valueOf()
而不是Boolean(item)
会更方便,因为Boolean(new Boolean(false))
是true
。
对于我来说,检查基本类型的部分不起作用。例如,"foo" instanceof String
是false
。
OP写道"处理自引用"...这个方法没有处理它。
如何在JavaScript中进行深度克隆
在JavaScript中,要进行深度克隆一个对象,最简单的方法是使用JSON.parse(JSON.stringify(objectToClone))
。这种方法非常简单,但是如果对象的值是函数,克隆之后函数会丢失,因为函数无法转换为JSON格式。此时,我们可以使用类似于Lodash中的cloneDeep
方法来进行克隆。
克隆函数的用例是什么?为什么不直接使用函数而要进行克隆呢?对于这个问题,暂时没有明确的答案。
需要注意的是,通过JSON.parse(JSON.stringify(objectToClone))
进行克隆时,日期对象会被转换为字符串。
有一个回答提到,如果克隆的对象包含函数,那么函数会丢失。但是根据问题的描述,提问者并不想使用像JSON.parse(JSON.stringify(objectToClone))
这样的框架。所以,这个回答并不正确。
另外,如果对象包含循环引用,使用这种方法进行克隆可能会导致错误。这种情况下,克隆过程会无限循环,最好的结果可能是出现错误,而不是使应用程序崩溃。
实际上,这个回答比其他回答更有帮助。对于提问者来说,这个回答更容易找到,因为他只关心问题的标题,而不是整个问题的内容。
另外,提问者表示不想使用像JSON
或$
这样的"框架"。但问题是,其中一个(jQuery)确实是一个框架,而JSON
是JavaScript的一部分。可能值得问一下提问者,但我的回答会是:"当你使用JSON
时,并不意味着你正在使用像jQuery这样的框架"。
要克隆一个对象,要么使用简单的方法JSON.parse(JSON.stringify(objectToClone))
,但会丢失函数,要么编写一个复杂的JavaScript递归函数进行深度克隆,但可能会在其他方面失败。
最后,有用户提到在他的Vue 3项目中,使用行数据时遇到了数据更改的问题,通过深度克隆解决了这个问题。
如何在JavaScript中进行深拷贝
在JavaScript中,使用JSON.parse(JSON.stringify())
的组合方法来深拷贝对象是一种低效的hack方法,因为它原本是用于JSON数据的。它不支持undefined
或function () {}
的值,当将Javascript对象转换成JSON字符串时,它会忽略它们(或将其置为null
)。
更好的解决方法是使用一个深拷贝函数。下面的函数可以深拷贝对象,并且不需要第三方库(如jQuery、LoDash等)。
function copy(aObject) { // 防止未定义的对象 // if (!aObject) return aObject; let bObject = Array.isArray(aObject) ? [] : {}; let value; for (const key in aObject) { // 防止自己引用父对象 // if (Object.is(aObject[key], aObject)) continue; value = aObject[key]; bObject[key] = (typeof value === "object") ? copy(value) : value; } return bObject; }
注意:这段代码可以检查简单的自我引用(取消注释// 防止自己引用父对象
部分),但是在可能的情况下,您还应避免创建具有自我引用的对象。请参阅:https://softwareengineering.stackexchange.com/questions/11856/whats-wrong-with-circular-references
除非aObject(或它包含的另一个对象)包含对自身的自我引用...stackoverflow!
可以提供一些“自我引用”的测试用例吗?
var o = { a:1, b:2 } ; o["oo"] = { c:3, m:o };
我喜欢这个解决方案。唯一需要修复的是处理null值:bObject[k] = (v === null) ? null : (typeof v === "object") ? copy(v) : v;
这个函数简单易懂,几乎可以处理所有情况。在JavaScript世界中,这已经是最完美的了。
我会把v === null
检查作为函数顶部的防护块,这样可以捕捉到初始传入null的情况,并且还可以阻止递归表达式变得更复杂。
请提供代码 - 我不知道如何将v === null
移到函数顶部。
这段代码有bug。在{a: [1, 2, 3]}
上使用它无法深拷贝数组属性a
。考虑使用github.com/pvorb/clone。
让我知道您发现了哪些其他bug。我会看看{a: [1, 2, 3]}
问题。这段代码的整个目的是让人们不需要安装第三方包。
我认为这正是git和npm的用处所在,而不是试图在一个Q&A网站上迭代一个代码片段。
根据我的测试,{a: [1, 2, 3]}
被正确地深拷贝了。
尽管没有打包,但您的代码仍然是开源的,并且仍然是一个依赖项。它在任何方面都不比您宣传的那些包更好。相反,它在更多情况下失败。而且它缺少真正的力量(和精神),这来自于两个主要因素:协作创作和分布式测试。如果我的团队中的某个人使用了您的代码,而不是经过良好测试的包,我会认为他们无视了我们团队的利益和客户的利益,通过承担不必要的风险而浪费我们的钱。
let uh = {}; uh.oh = uh; copy(uh); // => oh...
这是一个关于如何深拷贝的StackOverflow问题,我提供了一个简单的代码解决方案供其他人讨论和改进。这比告诉别人安装一个第三方包要好得多,这个包会为他们思考,并包含了数十到数百个未使用的函数,这些函数会引入它们自己的bug。
Déjàvu、ringø、tao - 我认为对象之间的自我引用并不被视为良好的实践,请参阅:softwareengineering.stackexchange.com/questions/11856/…。然而,如果子对象对父对象存在一个简单的引用,这段代码(带有“防止自我引用”检查)仍然会深拷贝这些结构-但会中断引用。我将尝试重新编写代码,以深拷贝所有具有循环引用的结构(并保留它们)。
无论自我引用是否是一个好的实践,在这里并不是问题的焦点。它在实际中被使用,并且是要求的一部分。您选择回答了这个问题。关于您对开源库的抱怨:lodash-es
是模块化的。因此,如果您只需要cloneDeep
,您可以只导入它。那个函数和您的版本一样是开源的。它们都不会“思考”。它们只是代码。它们要么按预期工作,要么不工作。根据OP的要求,您的版本并不工作。为什么不公开cloneDeep
的源代码?我保证它不会“为您思考”。很可能,它会让您思考和学习。
- OP说:“我知道有各种基于框架的函数,如JSON.parse(JSON.stringify(o))
和$.extend(true, {}, o)
,但我不想使用这样的框架。创建一个深拷贝的最优雅或最高效的方法是什么。我们关心边界情况,例如克隆数组,不破坏原型链,处理自我引用......现在应该没有真正需要这样的函数。这只是纯粹的学术兴趣。”
在我看来,问题在于这个问题及其答案被初级开发人员使用。他们只读问题的标题,复制粘贴答案的代码,而不阅读其他内容,并期望它能够工作。这就是为什么我反对您明显的建议不要使用经过良好测试的解决方案。我认为这个建议是有害的。而且,它也使您显得不理解使用开源软件的好处。此外,我很难相信您从未使用过实用程序库,正是因为不应该在每个项目中重新发明轮子。
-montague 这个函数也无法深拷贝树结构。因为子对象将引用它们的父对象,而父对象将引用子对象。根据您链接的页面,这个答案认为在树中,这样的循环引用并不是一个坏设计。 softwareengineering.stackexchange.com/a/11861/315637
你是对的。在图形、树等中的循环引用并不被认为是坏设计。然而,如果子对象对父对象存在一个简单的引用,这段代码(带有“防止自我引用”检查)仍然会深拷贝这些结构-但会中断引用。我将设法重新编写代码,以深拷贝所有具有循环引用的结构(并保留它们)。
如果您的初级开发人员只是复制粘贴而不思考,那是您让他们明确的工作,而不是stackoveflow的用户。此外,使用开源软件实际上相当于将别人的钥匙交给您的房子;如果东西丢了,您会责怪谁?无论是编写自己的代码还是接受使用别人的代码的风险,这是您的选择。tim-montague让其他开发人员更容易思考这个问题,但他们仍然必须选择自己思考。