ChatGPT解决这个技术问题 Extra ChatGPT

如何正确克隆 JavaScript 对象?

我有一个对象 x。我想将它复制为对象 y,这样对 y 的更改就不会修改 x。我意识到复制从内置 JavaScript 对象派生的对象会导致额外的、不需要的属性。这不是问题,因为我正在复制我自己的文字构造对象之一。

如何正确克隆 JavaScript 对象?

对于 JSON,我使用 mObj=JSON.parse(JSON.stringify(jsonObject));
我真的不明白为什么没有人建议Object.create(o),它可以满足作者的所有要求?
var x = { deep: { key: 1 } }; var y = Object.create(x); x.deep.key = 2; 执行此操作后,y.deep.key 也将为 2,因此 Object.create 不能用于克隆...
@r3wt 这不起作用...请仅在对解决方案进行基本测试后发布..

V
Vitaly Zdanevich

2022 年更新

有一个名为 structured cloning 的新 JS 标准。它适用于所有浏览器:

const clone = structuredClone(object);

旧答案

对 JavaScript 中的任何对象执行此操作都不会简单或直接。您将遇到错误地从对象原型中获取属性的问题,这些属性应该留在原型中而不是复制到新实例中。例如,如果您将 clone 方法添加到 Object.prototype,正如一些答案所描述的,您将需要明确跳过该属性。但是,如果在 Object.prototype 中添加了其他附加方法,或者其他中间原型,您不知道怎么办?在这种情况下,您将复制不应复制的属性,因此您需要使用 hasOwnProperty 方法检测不可预见的非本地属性。

除了不可枚举的属性之外,当您尝试复制具有隐藏属性的对象时,您还会遇到更棘手的问题。例如,prototype 是函数的隐藏属性。此外,对象的原型由属性 __proto__ 引用,该属性也是隐藏的,不会被迭代源对象属性的 for/in 循环复制。我认为 __proto__ 可能特定于 Firefox 的 JavaScript 解释器,并且在其他浏览器中可能有所不同,但你明白了。并非所有事物都是可枚举的。如果您知道它的名称,您可以复制隐藏的属性,但我不知道有什么方法可以自动发现它。

寻求优雅解决方案的另一个障碍是正确设置原型继承的问题。如果您的源对象的原型是 Object,那么只需使用 {} 创建一个新的通用对象即可,但如果源的原型是 Object 的某个后代,那么您将丢失该原型的其他成员您使用 hasOwnProperty 过滤器跳过了哪些,或者哪些在原型中,但一开始就无法枚举。一种解决方案可能是调用源对象的 constructor 属性来获取初始复制对象,然后复制属性,但是您仍然不会获得不可枚举的属性。例如,Date 对象将其数据存储为隐藏成员:

function clone(obj) {
    if (null == obj || "object" != typeof obj) return obj;
    var copy = obj.constructor();
    for (var attr in obj) {
        if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr];
    }
    return copy;
}

var d1 = new Date();

/* Executes function after 5 seconds. */
setTimeout(function(){
    var d2 = clone(d1);
    alert("d1 = " + d1.toString() + "\nd2 = " + d2.toString());
}, 5000);

d1 的日期字符串将比 d2 晚 5 秒。使一个 Date 与另一个相同的方法是调用 setTime 方法,但这是特定于 Date 类的。我认为这个问题没有万无一失的通用解决方案,尽管我很乐意犯错!

当我不得不实现一般的深度复制时,我最终妥协,假设我只需要复制一个普通的 ObjectArrayDateStringNumberBoolean。最后 3 种类型是不可变的,所以我可以执行浅拷贝而不用担心它会改变。我进一步假设 ObjectArray 中包含的任何元素也将是该列表中的 6 个简单类型之一。这可以通过如下代码来完成:

function clone(obj) {
    var copy;

    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;

    // Handle Date
    if (obj instanceof Date) {
        copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    // Handle Array
    if (obj instanceof Array) {
        copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = clone(obj[i]);
        }
        return copy;
    }

    // Handle Object
    if (obj instanceof Object) {
        copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
        }
        return copy;
    }

    throw new Error("Unable to copy obj! Its type isn't supported.");
}

只要对象和数组中的数据形成树形结构,上述函数就可以充分适用于我提到的 6 种简单类型。也就是说,对象中对相同数据的引用不超过一个。例如:

// This would be cloneable:
var tree = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "right" : null,
    "data"  : 8
};

// This would kind-of work, but you would get 2 copies of the 
// inner node instead of 2 references to the same copy
var directedAcylicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
directedAcyclicGraph["right"] = directedAcyclicGraph["left"];

// Cloning this would cause a stack overflow due to infinite recursion:
var cyclicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
cyclicGraph["right"] = cyclicGraph;

它不能处理任何 JavaScript 对象,但它可能足以满足许多目的,只要你不认为它只适用于你扔给它的任何东西。


这是缺少符号键和符号值。如今,使用 Object.getOwnPropertyDescriptors 更好。
structuredCloneonly 75% compatible globally
在 Nodejs 中,structuredClone(object) 适用于节点 v17.0.0 及更高版本。
@JoshuaDavid 的更新,目前 82.57% 的浏览器都支持。
T
TOPKAT

如果您不在对象中使用 Date、函数、未定义、正则表达式或 Infinity,则一个非常简单的一行是 JSON.parse(JSON.stringify(object))

const a = { string: 'string', number: 123, bool: false, nul: null, date: new Date(), // 字符串化 undef: undefined, // 丢失 inf: Infinity, // 强制为 'null' } 控制台.log(a); console.log(typeof a.date); // 日期对象 const clone = JSON.parse(JSON.stringify(a));控制台.log(克隆); console.log(typeof clone.date); // .toISOString() 的结果

这适用于包含对象、数组、字符串、布尔值和数字的所有类型的对象。

另请参阅在向工作人员发送消息或从工作人员发送消息时使用的 this article about the structured clone algorithm of browsers。它还包含深度克隆功能。


有时最好的答案是最简单的。天才。
有帮助,但是在比较包含其他对象的对象时,当两个完全相等的对象不被视为相等时,我遇到了意外的行为。使用 JSON.stringify(x) == JSON.stringify(JSON.parse(JSON.stringify(a))) 来修复它。出于某种原因,作为字符串进行比较在比较时可以正常工作,否则无法匹配。
@AgustinL.Lacuara 您无法比较 JS 中的复杂数据类型。 a={};b={}; a==bfalse。但是在 a=b 之后它变成了 true,因为它不仅相同而且是同一个对象。
做这项工作,但是,这违背了任何良好的编程习惯。在巴西,我们称之为“冈比亚拉”
b
buræquete

使用 jQuery,您可以使用 extend 浅拷贝

var copiedObject = jQuery.extend({}, originalObject)

copiedObject 的后续更改不会影响 originalObject,反之亦然。

或者制作一个深拷贝:

var copiedObject = jQuery.extend(true, {}, originalObject)

m
mikemaccana

在 ECMAScript 6 中有 Object.assign 方法,它将所有可枚举自身属性的值从一个对象复制到另一个对象。例如:

var x = {myProp: "value"};
var y = Object.assign({}, x); 

但请注意,这是一个浅拷贝 - 嵌套对象仍被复制为引用。


E
Efren

MDN

如果你想要浅拷贝,使用 Object.assign({}, a)

对于“深度”复制,使用 JSON.parse(JSON.stringify(a))

不需要外部库,但您需要检查 browser compatibility first


i
itpastorn

有很多答案,但没有一个提到 ECMAScript 5 中的 Object.create,它诚然没有给你一个精确的副本,而是将源设置为新对象的原型。

因此,这不是问题的确切答案,但它是一种单线解决方案,因此很优雅。它最适合两种情况:

这种继承是有用的(呃!)源对象不会被修改的地方,因此这两个对象之间的关系不成问题。

例子:

var foo = { a : 1 };
var bar = Object.create(foo);
foo.a; // 1
bar.a; // 1
foo.a = 2;
bar.a; // 2 - prototype changed
bar.a = 3;
foo.a; // Still 2, since setting bar.a makes it an "own" property

为什么我认为这个解决方案更好?它是原生的,因此没有循环,没有递归。但是,较旧的浏览器将需要一个 polyfill。


这是原型继承,而不是克隆。这些是完全不同的事情。新对象没有任何它自己的属性,它只是指向原型的属性。克隆的目的是创建一个新对象,它不引用另一个对象中的任何属性。
C
Community

一种在一行代码中克隆 Javascript 对象的优雅方法

Object.assign 方法是 ECMAScript 2015 (ES6) 标准的一部分,可以满足您的需要。

var clone = Object.assign({}, obj);

Object.assign() 方法用于将所有可枚举自身属性的值从一个或多个源对象复制到目标对象。

Read more...

支持旧浏览器的 polyfill:

if (!Object.assign) {
  Object.defineProperty(Object, 'assign', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: function(target) {
      'use strict';
      if (target === undefined || target === null) {
        throw new TypeError('Cannot convert first argument to object');
      }

      var to = Object(target);
      for (var i = 1; i < arguments.length; i++) {
        var nextSource = arguments[i];
        if (nextSource === undefined || nextSource === null) {
          continue;
        }
        nextSource = Object(nextSource);

        var keysArray = Object.keys(nextSource);
        for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
          var nextKey = keysArray[nextIndex];
          var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
          if (desc !== undefined && desc.enumerable) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
      return to;
    }
  });
}

这只会执行一个浅的“克隆”
我很难理解 objA = objB; 会导致各种头痛。这似乎已经解决了这个问题,至少现在......
t
tgogos

互联网上的大多数解决方案都存在几个问题。所以我决定进行跟进,其中包括为什么不应该接受已接受的答案。

起始情况

我想深拷贝 Javascript Object 及其所有子级及其子级等。但由于我不是一个普通的开发者,我的 Object普通 propertiescircular structures 甚至 nested objects

因此,让我们先创建一个 circular structure 和一个 nested object

function Circ() {
    this.me = this;
}

function Nested(y) {
    this.y = y;
}

让我们将所有内容放在一个名为 aObject 中。

var a = {
    x: 'a',
    circ: new Circ(),
    nested: new Nested('a')
};

接下来,我们要将 a 复制到名为 b 的变量中并对其进行变异。

var b = a;

b.x = 'b';
b.nested.y = 'b';

您知道这里发生了什么,因为如果不是,您甚至不会遇到这个好问题。

console.log(a, b);

a --> Object {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

b --> Object {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

现在让我们找到解决方案。

JSON

我尝试的第一次尝试是使用 JSON

var b = JSON.parse( JSON.stringify( a ) );

b.x = 'b';
b.nested.y = 'b';

不要在上面浪费太多时间,你会得到 TypeError: Converting circular structure to JSON

递归副本(接受的“答案”)

让我们看看接受的答案。

function cloneSO(obj) {
    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;

    // Handle Date
    if (obj instanceof Date) {
        var copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    // Handle Array
    if (obj instanceof Array) {
        var copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = cloneSO(obj[i]);
        }
        return copy;
    }

    // Handle Object
    if (obj instanceof Object) {
        var copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = cloneSO(obj[attr]);
        }
        return copy;
    }

    throw new Error("Unable to copy obj! Its type isn't supported.");
}

看起来不错吧?它是对象的递归副本,也可以处理其他类型,例如 Date,但这不是必需的。

var b = cloneSO(a);

b.x = 'b';
b.nested.y = 'b';

递归和 circular structures 不能很好地协同工作... RangeError: Maximum call stack size exceeded

本机解决方案

在与同事争吵后,我的老板问我们发生了什么,他在谷歌上搜索后找到了一个简单的解决方案。它称为 Object.create

var b = Object.create(a);

b.x = 'b';
b.nested.y = 'b';

该解决方案是前段时间添加到 Javascript 中的,甚至可以处理 circular structure

console.log(a, b);

a --> Object {
    x: "a",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

b --> Object {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

...你看,它不适用于内部的嵌套结构。

原生解决方案的 polyfill

与 IE 8 一样,旧版浏览器中有一个用于 Object.create 的 polyfill。它类似于 Mozilla 推荐的东西,当然,它并不完美,并且会导致与 本机解决方案相同的问题。

function F() {};
function clonePF(o) {
    F.prototype = o;
    return new F();
}

var b = clonePF(a);

b.x = 'b';
b.nested.y = 'b';

我已将 F 放在范围之外,因此我们可以看看 instanceof 告诉我们什么。

console.log(a, b);

a --> Object {
    x: "a",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

b --> F {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

console.log(typeof a, typeof b);

a --> object
b --> object

console.log(a instanceof Object, b instanceof Object);

a --> true
b --> true

console.log(a instanceof F, b instanceof F);

a --> false
b --> true

与本机解决方案相同的问题,但输出稍差一些。

更好(但不完美)的解决方案

在四处挖掘时,我发现了一个与这个问题类似的问题 (In Javascript, when performing a deep copy, how do I avoid a cycle, due to a property being "this"?),但有一个更好的解决方案。

function cloneDR(o) {
    const gdcc = "__getDeepCircularCopy__";
    if (o !== Object(o)) {
        return o; // primitive value
    }

    var set = gdcc in o,
        cache = o[gdcc],
        result;
    if (set && typeof cache == "function") {
        return cache();
    }
    // else
    o[gdcc] = function() { return result; }; // overwrite
    if (o instanceof Array) {
        result = [];
        for (var i=0; i<o.length; i++) {
            result[i] = cloneDR(o[i]);
        }
    } else {
        result = {};
        for (var prop in o)
            if (prop != gdcc)
                result[prop] = cloneDR(o[prop]);
            else if (set)
                result[prop] = cloneDR(cache);
    }
    if (set) {
        o[gdcc] = cache; // reset
    } else {
        delete o[gdcc]; // unset again
    }
    return result;
}

var b = cloneDR(a);

b.x = 'b';
b.nested.y = 'b';

让我们看看输出......

console.log(a, b);

a --> Object {
    x: "a",
    circ: Object {
        me: Object { ... }
    },
    nested: Object {
        y: "a"
    }
}

b --> Object {
    x: "b",
    circ: Object {
        me: Object { ... }
    },
    nested: Object {
        y: "b"
    }
}

console.log(typeof a, typeof b);

a --> object
b --> object

console.log(a instanceof Object, b instanceof Object);

a --> true
b --> true

console.log(a instanceof F, b instanceof F);

a --> false
b --> false

要求是匹配的,但还有一些小问题,包括将 nestedinstancecirc 更改为 Object

共享叶子的树的结构不会被复制,它们将成为两个独立的叶子:

        [Object]                     [Object]
         /    \                       /    \
        /      \                     /      \
      |/_      _\|                 |/_      _\|  
  [Object]    [Object]   ===>  [Object]    [Object]
       \        /                 |           |
        \      /                  |           |
        _\|  |/_                 \|/         \|/
        [Object]               [Object]    [Object]

结论

最后一个使用递归和缓存的解决方案可能不是最好的,但它是对象的真实深层副本。它处理简单的 propertiescircular structuresnested object,但在克隆时会弄乱它们的实例。

jsfiddle


所以结论是避免这个问题:)
@mikus 直到有一个真正的规范,它不仅涵盖基本用例,是的。
对上面提供的解决方案进行了很好的分析,但作者得出的结论表明这个问题没有解决方案。
遗憾的是 JS 不包含原生克隆功能。
在所有最佳答案中,我觉得这接近正确的答案。
N
Nadeem Yasin

如果您对浅拷贝没问题,underscore.js 库有一个 clone 方法。

y = _.clone(x);

或者你可以像这样扩展它

copiedObject = _.extend({},originalObject);

谢谢。在 Meteor 服务器上使用此技术。
要快速开始使用 lodash,我建议学习 npm、Browserify 以及 lodash。我让克隆与 'npm i --save lodash.clone' 一起工作,然后是 'var clone = require('lodash.clone');'要让 require 工作,你需要类似 browserify 的东西。安装并了解它的工作原理后,您将在每次运行代码时使用“browserify yourfile.js > bundle.js;start chrome index.html”(而不是直接进入 Chrome)。这会将您的文件和 npm 模块所需的所有文件收集到 bundle.js 中。不过,您可能可以使用 Gulp 节省时间并自动执行此步骤。
A
Alireza

好的,假设你在下面有这个对象并且你想克隆它:

let obj = {a:1, b:2, c:3}; //ES6

或者

var obj = {a:1, b:2, c:3}; //ES5

答案主要取决于您使用的 ECMAscript,在 ES6+ 中,您可以简单地使用 Object.assign 进行克隆:

let cloned = Object.assign({}, obj); //new {a:1, b:2, c:3};

或像这样使用扩展运算符:

let cloned = {...obj}; //new {a:1, b:2, c:3};

但是如果你使用 ES5,你可以使用几个方法,但是 JSON.stringify,只要确保你不使用大量数据来复制,但在很多情况下它可能是一个方便的方式,像这样:

let cloned = JSON.parse(JSON.stringify(obj)); 
//new {a:1, b:2, c:3};, can be handy, but avoid using on big chunk of data over and over

您能否举例说明 big chunk of data 的含义? 100KB? 100MB?谢谢!
是的,@ user1063287,基本上更大的数据,性能更差......所以这真的取决于,而不是kb,mb或gb,更多的是你想要做多少次......而且它也行不通对于功能和其他东西......
Object.assign 制作浅拷贝(就像传播一样,@Alizera)
你不能在 es5 中使用 let :^) @Alireza
N
Nimantha

2020 年 7 月 6 日更新

有三 (3) 种方法可以在 JavaScript 中克隆对象。由于 JavaScript 中的对象是引用值,因此您不能简单地使用 = 进行复制。

方法是:

const food = { food: 'apple', drink: 'milk' }


// 1. Using the "Spread"
// ------------------

{ ...food }


// 2. Using "Object.assign"
// ------------------

Object.assign({}, food)


// 3. "JSON"
// ------------------

JSON.parse(JSON.stringify(food))

// RESULT:
// { food: 'apple', drink: 'milk' }

这可以作为参考总结。


这为这个问题增加了哪些新的/独特的信息?
JSON 方法将删除对象的任何方法
从一个对象创建一个字符串,然后将该字符串解析为另一个对象只是为了复制该对象是一种 Monty Python 的编程风格:-D
这仅适用于对象字面量和可以这样表示的对象,但不适用于您在 OO 语言中遇到的通用“对象”。这似乎是 OP 要求的,因此没关系,但它不是适用于每种对象的通用解决方案。
对于具有层次结构的对象,扩展运算符和 Object.assign 失败,即。嵌套对象。 JSON.parse/stringify 有效,但如上所述不复制方法。
T
Tim

一种特别不优雅的解决方案是使用 JSON 编码来制作没有成员方法的对象的深层副本。该方法是对您的目标对象进行 JSON 编码,然后通过对其进行解码,您可以获得所需的副本。您可以根据需要进行多次解码,制作尽可能多的副本。

当然,函数不属于 JSON,所以这只适用于没有成员方法的对象。

这种方法非常适合我的用例,因为我将 JSON blob 存储在键值存储中,当它们在 JavaScript API 中作为对象公开时,每个对象实际上都包含对象原始状态的副本,所以我们可以在调用者改变暴露的对象后计算增量。

var object1 = {key:"value"};
var object2 = object1;

object2 = JSON.stringify(object1);
object2 = JSON.parse(object2);

object2.key = "a change";
console.log(object1);// returns value

为什么函数不属于 JSON?我不止一次看到它们以 JSON 格式传输...
函数不是 JSON 规范的一部分,因为它们不是一种安全(或智能)的数据传输方式,而这正是 JSON 的用途。我知道 Firefox 中的本机 JSON 编码器只是忽略传递给它的函数,但我不确定其他人的行为。
@mark: { 'foo': function() { return 1; } } 是一个文字构造的对象。
@abarnert 函数不是数据。 “函数字面量”是用词不当——因为函数可以包含任意代码,包括赋值和各种“不可序列化”的东西。
m
musemind

您可以简单地使用 spread property 来复制没有引用的对象。但要小心(见评论),“副本”只是在最低的对象/数组级别。嵌套属性仍然是引用!

完整克隆:

let x = {a: 'value1'}
let x2 = {...x}

// => mutate without references:

x2.a = 'value2'
console.log(x.a)    // => 'value1'

使用二级引用克隆:

const y = {a: {b: 'value3'}}
const y2 = {...y}

// => nested object is still a references:

y2.a.b = 'value4'
console.log(y.a.b)    // => 'value4'

JavaScript 实际上本身并不支持深度克隆。使用效用函数。例如拉姆达:

http://ramdajs.com/docs/#clone


这不起作用...当 x 将是一个数组时,它可能会起作用,例如 x= [ 'ab','cd',...]
这可行,但请记住这是一个 SHALLOW 副本,因此对其他对象的任何深度引用仍然是引用!
部分克隆也可能以这种方式发生:const first = {a: 'foo', b: 'bar'}; const second = {...{a} = first}
P
Pavan Garre
const objClone = { ...obj };

请注意,嵌套对象仍被复制为引用。


感谢您提示嵌套对象仍被复制作为参考!调试我的代码时我几乎发疯了,因为我修改了“克隆”上的嵌套属性,但原来的被修改了。
这是 ES2016,而不是 2018,这个答案是 two years earlier
那么如果我也想要嵌套属性的副本怎么办
@SunilGarg 要复制嵌套属性,您可以使用 const objDeepClone = JSON.parse(JSON.stringify(obj));
T
The Red Pea

来自这篇文章:How to copy arrays and objects in Javascript by Brian Huisman:

Object.prototype.clone = function() {
  var newObj = (this instanceof Array) ? [] : {};
  for (var i in this) {
    if (i == 'clone') continue;
    if (this[i] && typeof this[i] == "object") {
      newObj[i] = this[i].clone();
    } else newObj[i] = this[i]
  } return newObj;
};

这很接近,但不适用于任何对象。尝试用这个克隆一个 Date 对象。并非所有属性都是可枚举的,因此它们不会全部显示在 for/in 循环中。
像这样添加到对象原型对我来说破坏了 jQuery。即使我重命名为 clone2。
@iPadDeveloper2011 上面的代码有一个错误,它创建了一个名为“i”“(for i in this)”的全局变量,而不是“(for var i in this)”。我有足够的业力来编辑和修复它,所以我做到了。
@Calvin:这应该创建一个不可枚举的属性,否则“克隆”将出现在“for”循环中。
为什么 var copiedObj = Object.create(obj); 也不是一个好方法?
L
Lukas Jelinek

对于那些使用 AngularJS 的人,也有直接的方法来克隆或扩展这个库中的对象。

var destination = angular.copy(source);

或者

angular.copy(source, destination);

更多在 angular.copy documentation...


这是一个深拷贝仅供参考。
N
Nimantha
function clone(obj) {
    if(obj == null || typeof(obj) != 'object')
        return obj;    
    var temp = new obj.constructor(); 
    for(var key in obj)
        temp[key] = clone(obj[key]);    
    return temp;
}

这个答案非常接近,但并不完全正确。如果您尝试克隆 Date 对象,您将不会获得相同的日期,因为对 Date 构造函数的调用会使用当前日期/时间初始化新 Date。该值不可枚举,也不会被 for/in 循环复制。
不完美,但对于那些基本情况很好。例如,允许简单克隆可以是基本对象、数组或字符串的参数。
赞成使用 new 正确调用构造函数。接受的答案没有。
适用于节点一切!仍然留下参考链接
递归的想法很棒。但是如果值是数组,它会工作吗?
J
Jan Turoň

A.Levy 的回答几乎是完整的,这是我的一点贡献:有一种方法如何处理递归引用,请参阅这一行

if(this[attr]==this) copy[attr] = copy;

如果对象是 XML DOM 元素,我们必须使用 cloneNode 代替

if(this.cloneNode) return this.cloneNode(true);

受 A.Levy 详尽的研究和 Calvin 的原型制作方法的启发,我提供了以下解决方案:

Object.prototype.clone = function() {
  if(this.cloneNode) return this.cloneNode(true);
  var copy = this instanceof Array ? [] : {};
  for(var attr in this) {
    if(typeof this[attr] == "function" || this[attr]==null || !this[attr].clone)
      copy[attr] = this[attr];
    else if(this[attr]==this) copy[attr] = copy;
    else copy[attr] = this[attr].clone();
  }
  return copy;
}

Date.prototype.clone = function() {
  var copy = new Date();
  copy.setTime(this.getTime());
  return copy;
}

Number.prototype.clone = 
Boolean.prototype.clone =
String.prototype.clone = function() {
  return this;
}

另请参阅答案中的安迪·伯克 (Andy Burke) 的注释。


Date.prototype.clone = function() {return new Date(+this)};
K
Kamil Kiełczewski

表现

今天 2020.04.30 我在 MacOs High Sierra v10.13.6 上在 Chrome v81.0、Safari v13.1 和 Firefox v75.0 上执行所选解决方案的测试。

我专注于复制 DATA 的速度(具有简单类型字段的对象,而不是方法等)。解AI只能做浅拷贝,解JU可以做深拷贝。

浅拷贝的结果

解决方案 {...obj} (A) 在 chrome 和 firefox 上最快,在 safari 上中等速度

基于 Object.assign (B) 的解决方案在所有浏览器上都很快

jQuery (E) 和 lodash (F,G,H) 解决方案中等/相当快

解决方案 JSON.parse/stringify(K) 相当慢

解决方案 D 和 U 在所有浏览器上都很慢

https://i.stack.imgur.com/mCte1.png

深拷贝的结果

解决方案 Q 在所有浏览器上最快

jQuery (L) 和 lodash (J) 中等速度

解决方案 JSON.parse/stringify(K) 相当慢

解决方案 U 在所有浏览器上最慢

lodash (J) 和解决方案 U crash on Chrome for 1000 level deep object

https://i.stack.imgur.com/KxBqv.png

细节

对于选择的解决方案:A B C(my) D E F G H I J K L M N O P Q R S T U,我执行 4 次测试

浅小:具有 10 个非嵌套字段的对象 - 您可以在此处运行它

浅大:具有 1000 个非嵌套字段的对象 - 您可以在此处运行它

deep-small:具有 10 个级别嵌套字段的对象 - 您可以在此处运行它

deep-big:具有 1000 个级别嵌套字段的对象 - 您可以在此处运行它

测试中使用的对象显示在下面的代码片段中

let obj_ShallowSmall = { field0: false, field1: true, field2: 1, field3: 0, field4: null, field5: [], field6: {}, field7: "text7", field8: "text8", } let obj_DeepSmall = { level0: { level1: { level2: { level3: { level4: { level5: { level6: { level7: { level8: { level9: [[[[[[[[[['abc']]]]]]] ]]], }}}}}}}}}, };让 obj_ShallowBig = Array(1000).fill(0).reduce((a,c,i) => (a['field'+i]=getField(i),a) ,{});让 obj_DeepBig = genDeepObject(1000); // ------------------ // 显示对象 // ------------------ console.log('obj_ShallowSmall :',JSON.stringify(obj_ShallowSmall)); console.log('obj_DeepSmall:',JSON.stringify(obj_DeepSmall)); console.log('obj_ShallowBig:',JSON.stringify(obj_ShallowBig)); console.log('obj_DeepBig:',JSON.stringify(obj_DeepBig)); // ------------------ // 帮助 // ------------------ function getField(k) { let我=k%10;如果(i==0)返回假;如果(i==1)返回真; if(i==2) 返回 k;如果(i==3)返回 0; if(i==4) 返回空值; if(i==5) 返回 []; if(i==6) 返回 {};如果(i>=7)返回“文本”+k; } function genDeepObject(N) { // 生成:{level0:{level1:{...levelN: {end:[[[...N-times...['abc']...]]] } }}...}}} 让 obj={};让 o=obj;让 arr = [];让 a=arr; for(让 i=0; i

下面的代码片段展示了经过测试的解决方案并显示了它们之间的差异

函数 A(obj) { return {...obj} } 函数 B(obj) { return Object.assign({}, obj); } function C(obj) { return Object.keys(obj).reduce( (a,c) => (a[c]=obj[c], a), {}) } function D(obj) { let copyOfObject = {}; Object.defineProperties(copyOfObject, Object.getOwnPropertyDescriptors(obj));返回副本对象; } function E(obj) { return jQuery.extend({}, obj) // 浅层 } function F(obj) { return _.clone(obj); } 函数 G(obj) { return _.clone(obj,true); } 函数 H(obj) { return _.extend({},obj); } 函数 I(obj) { if (null == obj || "object" != typeof obj) 返回 obj; var copy = obj.constructor(); for (var attr in obj) { if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr]; } 返回副本; } 函数 J(obj) { return _.cloneDeep(obj,true); } 函数 K(obj) { 返回 JSON.parse(JSON.stringify(obj)); } function L(obj) { return jQuery.extend(true, {}, obj) // deep } function M(obj) { if(obj == null || typeof(obj) != 'object') return obj; var temp = new obj.constructor(); for(obj 中的变量键) temp[key] = M(obj[key]);返回温度; } function N(obj) { let EClone = function(obj) { var newObj = (obj instanceof Array) ? []:{}; for (var i in obj) { if (i == 'EClone') 继续; if (obj[i] && typeof obj[i] == "object") { newObj[i] = EClone(obj[i]); } 否则 newObj[i] = obj[i] } return newObj; };返回 EClone(obj); };函数 O(obj) { if (obj == null || typeof obj != "object") return obj; if (obj.constructor != Object && obj.constructor != Array) 返回 obj; if (obj.constructor == Date || obj.constructor == RegExp || obj.constructor == Function || obj.constructor == String || obj.constructor == Number || obj.constructor == Boolean) 返回新的 obj.constructor(obj);让 to = new obj.constructor(); for (var name in obj) { to[name] = typeof to[name] == "undefined" ? O(obj[name], null) : to[name]; } 还给; } function P(obj) { function clone(target, source){ for(let key in source){ // 使用 getOwnPropertyDescriptor 代替 source[key] 来防止触发 setter/getter。让描述符 = Object.getOwnPropertyDescriptor(source, key); if(descriptor.value instanceof String){ target[key] = new String(descriptor.value); } else if(descriptor.value instanceof Array){ target[key] = clone([], descriptor.value); } else if(descriptor.value instanceof Object){ 让原型 = Reflect.getPrototypeOf(descriptor.value);让 cloneObject = clone({}, descriptor.value); Reflect.setPrototypeOf(cloneObject, 原型);目标[键] = 克隆对象; } else { Object.defineProperty(目标,键,描述符); } } 让原型 = Reflect.getPrototypeOf(source); Reflect.setPrototypeOf(target, 原型);返回目标; } 返回克隆({},obj); } 函数 Q(obj) { var 复制; // 处理 3 种简单类型,null 或 undefined if (null == obj || "object" != typeof obj) return obj; // 处理日期 if (obj instanceof Date) { copy = new Date();复制.setTime(obj.getTime());返回副本; } // 处理数组 if (obj instanceof Array) { copy = []; for (var i = 0, len = obj.length; i < len; i++) { copy[i] = Q(obj[i]); } 返回副本; } // 处理对象 if (obj instanceof Object) { copy = {}; for (var attr in obj) { if (obj.hasOwnProperty(attr)) copy[attr] = Q(obj[attr]); } 返回副本; } throw new Error("无法复制 obj!不支持它的类型。"); } 函数 R(obj) { const gdcc = "__getDeepCircularCopy__"; if (obj !== Object(obj)) { 返回 obj; // 原始值 } var set = gdcc in obj, cache = obj[gdcc], result; if (set && typeof cache == "function") { return cache(); } // else obj[gdcc] = function() { 返回结果; }; // 覆盖 if (obj instanceof Array) { result = []; for (var i=0; i descriptor[attr] === undefined && (描述符[attr] = basicDesc[attr]) ) const { 获取、设置、值、可写、可枚举、可配置} = 描述符返回 Object.defineProperty(object, key, { 可枚举, 可配置, ...get || set ? { get, set } // 访问器描述符 : { value, writable } // 数据描述符 }) } function clone(object) { if (object !== Object(object)) return object /* —— 检查对象是否属于原始数据类型 */ if (object instanceof Node) return object.cloneNode(true) /* —— 克隆 DOM 树 */ let _object // 对象的克隆 switch (object.constructor) { case Array: case Object: _object = cloneObject(object) 破案日期:_object = new Date(+object) 破案函数:const fnStr = String(object) _object = new Function("return " + (/^(?!function |[^{]+ ?=>)[^(]+?\(/.test(fnStr) ? "function " : "" ) + fnStr )() copyPropDescs(_ob ject, object) break case RegExp: _object = new RegExp(object) break default: switch (Object.prototype.toString.call(object.constructor)) { // // Stem from: case "[object Function]": / / `class` case "[object Undefined]": // `Object.create(null)` _object = cloneObject(object) break default: // `Proxy` _object = object } } return _object } function cloneObject(object) { if (seen.has(object)) return seen.get(object) /* —— 处理递归引用(循环结构) */ const _object = Array.isArray(object) ? [] : Object.create(Object.getPrototypeOf(object)) /* —— 为继承赋值 [[Prototype]] */ seen.set(object, _object) /* —— 使`_object`成为`object的关联镜像` */ Reflect.ownKeys(object).forEach(key => defineProp(_object, key, { value: clone(object[key]) }, object) ) return _object } function copyPropDescs(target, source) { Object.defineProperties (target, Object.getOwnPropertyDescriptors(source) ) } } // ------------------------ // 测试属性 // ------ ------------------ console.log(` shallow deep func circ undefined date RegExp bigInt`) log(A);日志(B);日志(C);日志(D);日志(E);日志(F);日志(G);日志(H);日志(一);日志(J);日志(K);日志(L);日志(M);日志(N);标识);日志(P);日志(Q);日志(R);日志(S);日志(T);日志(U); console.log(` shallow deep func circ undefined date RegExp bigInt ---- LEGEND: shallow - 解决方案创建浅拷贝 deep - 解决方案创建深拷贝 func - 解决方案复制函数 circ - 解决方案可以复制具有循环引用的对象 undefined - 解决方案复制字段具有未定义的值 date - 解决方案可以复制日期 RegExp - 解决方案可以使用正则表达式复制字段 bigInt - 解决方案可以复制 BigInt `) // ---------- -- // 辅助函数 // ------------------------ function deepCompare(obj1,obj2) { return JSON.stringify(obj1)=== JSON.stringify(obj2); } function getCase() { // 纯数据 case return { undef: undefined, bool: true, num: 1, str: "txt1", e1: null, e2: [], e3: {}, e4: 0, e5 : false, arr: [ false, 2, "txt3", null, [], {}, [ true,4,"txt5",null, [], {}, [true,6,"txt7",null, [],{} ], {bool: true,num: 8, str: "txt9", e1:null, e2:[] ,e3:{} ,e4: 0, e5: false} ], {bool: true ,num: 10, str: "txt11", e1:null, e2:[] ,e3:{} ,e4: 0, e5: false}], obj: { bool: true, num: 12, str: "txt13 ", e1: null, e2: [], e3: {}, e4: 0, e5: false, arr: [true,14,"txt15",null,[],{} ], obj: { bool: true , num: 16, str: "txt17", e1: null, e2: [], e3: {}, e4: 0, e5: false, arr: [true,18,"txt19",null,[],{ } ], obj: {bool: true,num: 20, str: "txt21", e1:null, e2:[] ,e3:{} ,e4: 0, e5: false} } } }; } 功能检查(组织,复制,字段,新值){ 复制[字段] = newValue;返回 deepCompare(org,copy); } function testFunc(f) { let o = { a:1, fun: (i,j)=> i+j };让 c = f(o);让 val = false try{ val = c.fun(3,4)==7; } catch(e) { } 返回值; } 函数 testCirc(f) { 函数 Circ() { this.me = this; } var o = { x: 'a', circ: new Circ(), obj_circ: null, }; o.obj_circ = o;让 val = 假;尝试{让 c = f(o); val = (o.obj_circ == o) && (o.circ == o.circ.me); } catch(e) { } 返回值; } function testRegExp(f) { let o = { re: /a[0-9]+/, };让 val = 假;尝试{让 c = f(o); val = (String(c.re) == String(/a[0-9]+/)); } catch(e) { } 返回值; } function testDate(f) { let o = { date: new Date(), };让 val = 假;尝试{让 c = f(o); val = (+new Date(c.date) == +new Date(o.date)); } catch(e) { } 返回值; } 函数 testBigInt(f) { 让 val = false;尝试{让 o = { 大:123n,};让 c = f(o); val = o.big == c.big; } catch(e) { } 返回值; } 函数日志(f){ 让 o = getCase(); // 原始对象 let oB = getCase(); // “备份”用于浅有效测试 let c1 = f(o); // 复制 1 以供参考 let c2 = f(o); // 复制 2 用于测试浅层值 let c3 = f(o); // 复制 3 用于测试深度值 let is_proper_copy = deepCompare(c1,o); // 应该是真的 // 浅变化 let testShallow = [ ['bool',false],['num',666],['str','xyz'],['arr',[]],[' obj',{}] ] .reduce((acc,curr)=> acc && check(c1,c2,curr[0], curr[1]), true ); // 应该为真(原始对象不应该改变浅层) let is_valid = deepCompare(o,oB); // 深度测试(引入一些变化) if (c3.arr[6]) c3.arr[6][7].num = 777;让 diff_shallow = !testShallow; // 应该为真(浅字段被复制) let diff_deep = !deepCompare(c1,c3); // 应该为真(复制了深层字段) let can_copy_functions = testFunc(f);让 can_copy_circular = testCirc(f);让 can_copy_regexp = testRegExp(f);让 can_copy_date = testDate(f);让 can_copy_bigInt = testBigInt(f);让 has_undefined = 'undef' 在 c1 中; // 未定义值的字段被复制?let is_ok = is_valid && is_proper_copy;让 b=(bool) => (bool+'').padEnd(5,' '); // 将布尔值转换为格式化字符串 testFunc(f); if(is_ok) { console.log(`${f.name} ${b(diff_shallow)} ${b(diff_deep)} ${b(can_copy_functions)} ${b(can_copy_circular)} ${b(has_undefined) } ${b(can_copy_date)} ${b(can_copy_regexp)} ${b(can_copy_bigInt)}`) } else { console.log(`${f.name}: INVALID ${is_valid} ${is_proper_copy}`, {c1}) } } 此片段仅提供经过测试的解决方案并显示它们之间的差异(但它没有进行性能测试)

下面是 Chrome 用于浅大对象的示例结果

https://i.stack.imgur.com/zluC1.png


V
VaZaA

使用 Lodash:

var y = _.clone(x, true);

天哪,重新发明克隆是疯狂的。这是唯一理智的答案。
我更喜欢 _.cloneDeep(x),因为它本质上与上面相同,但读起来更好。
J
João Oliveira

在 ES-6 中,您可以简单地使用 Object.assign(...)。前任:

let obj = {person: 'Thor Odinson'};
let clone = Object.assign({}, obj);

这里有一个很好的参考:https://googlechrome.github.io/samples/object-assign-es6/


它不会深度克隆对象。
那是一个任务,而不是一个副本。 clone.Title = "just a clone" 表示 obj.Title = "just a clone"。
@HoldOffHunger 你错了。在浏览器的 JS 控制台 (let obj = {person: 'Thor Odinson'}; let clone = Object.assign({}, obj); clone.title = "Whazzup";) 中检查它
@collapsar:这正是我检查的内容,然后 console.log(person) 将是“Whazzup”,而不是“Thor Odinson”。见八月的评论。
@HoldOffHunger 在 Chrome 60.0.3112.113 和 Edge 14.14393 中都不会发生; August 的评论不适用,因为 obj 的属性的原始类型的值确实被克隆了。对象本身的属性值不会被克隆。
b
bukart

对克隆简单对象感兴趣:

JSON.parse(JSON.stringify(json_original));

来源:How to copy JavaScript object to new variable NOT by reference?


非常好 - 简单。
@MattH:这个答案已经给出 in 2012。你看见了吗?穆罕默德,您在复制其中一个之前检查过现有答案吗?
嗯,这是一种方式。你从来没想过
C
Community

您可以使用一行代码克隆一个对象并从前一个对象中删除任何引用。只需这样做:

var obj1 = { text: 'moo1' };
var obj2 = Object.create(obj1); // Creates a new clone without references

obj2.text = 'moo2'; // Only updates obj2's text property

console.log(obj1, obj2); // Outputs: obj1: {text:'moo1'}, obj2: {text:'moo2'}

对于当前不支持 Object.create 的浏览器/引擎,您可以使用这个 polyfill:

// Polyfill Object.create if it does not exist
if (!Object.create) {
    Object.create = function (o) {
        var F = function () {};
        F.prototype = o;
        return new F();
    };
}

+1 Object.create(...) 似乎绝对是要走的路。
完美的答案。也许您可以为 Object.hasOwnProperty 添加解释?这样人们就知道如何防止搜索原型链接。
效果很好,但是 polyfill 在哪些浏览器中工作?
这是使用 obj1 作为原型创建 obj2。它之所以有效,是因为您正在遮蔽 obj2 中的 text 成员。您不是在制作副本,只是在 obj2 上找不到成员时推迟到原型链。
这不会“没有引用”创建它,它只是将引用移动到原型。它仍然是一个参考。如果原始属性发生变化,“克隆”中的原型属性也会发生变化。它根本不是克隆。
f
flori
let clone = Object.assign( Object.create( Object.getPrototypeOf(obj)), obj)

ES6 解决方案,如果你想(浅)克隆一个类实例而不仅仅是一个属性对象。


这与 let cloned = Object.assign({}, obj) 有何不同?
@ceztko 当 obj 是类实例时, Object.assign() 不会克隆例如类方法(因为它们不可枚举)。
C
Charles Merriam

老问题的新答案!如果您有幸将 ECMAScript 2016 (ES6) 与 Spread Syntax 一起使用,这很容易。

keepMeTheSame = {first: "Me!", second: "You!"};
cloned = {...keepMeTheSame}

这为对象的浅拷贝提供了一种干净的方法。制作深层副本,即为每个递归嵌套对象中的每个值创建一个新副本,需要上述较重的解决方案。

JavaScript 不断发展。


当您在对象上定义了函数时,它不起作用
据我所知,传播运算符仅适用于迭代 - developer.mozilla.org 说:var obj = {'key1': 'value1'}; var array = [...obj]; // TypeError: obj is not iterable
@Oleh 所以使用 ` {... obj} 而不是 [...obj];`
@manikantgautam 我之前使用过 Object.assign(),但现在最新的 Chrome、Firefox 确实支持对象传播语法(Edge 和 Safari 中仍然不支持)。它的 ECMAScript 提案......但据我所知,Babel 确实支持它,所以它可能可以安全使用。
C
ConductedClever

我认为有一个简单而有效的答案。在深度复制中,有两个问题:

保持属性相互独立。并使方法在克隆对象上保持活动状态。

所以我认为一个简单的解决方案是首先序列化和反序列化,然后对其进行分配以复制函数。

let deepCloned = JSON.parse(JSON.stringify(source));
let merged = Object.assign({}, source);
Object.assign(merged, deepCloned);

虽然这个问题有很多答案,但我希望这个也有帮助。


虽然如果允许我导入 lodash,我更喜欢使用 lodash cloneDeep
我正在使用 JSON.parse(JSON.stringify(source))。一直在工作。
@Misha,这样你会错过这些功能。 “作品”一词有很多含义。
请记住,按照我提到的方式,只会复制第一层的功能。因此,如果我们彼此内部有一些对象,那么唯一的方法就是逐个字段递归地复制。
J
Jesse Reza Khorasanee

对于深拷贝和克隆,JSON.stringify 然后 JSON.parse 对象:

obj = { a: 0 , b: { c: 0}};
let deepClone = JSON.parse(JSON.stringify(obj));
obj.a = 5;
obj.b.c = 5;
console.log(JSON.stringify(deepClone)); // { a: 0, b: { c: 0}}

非常聪明......这种方法有什么缺点吗?
C
Christopher Peisert

结构化克隆

2022 年更新structuredClone() 全局函数已在 Node 17、Deno 1.14 和大多数主要浏览器中可用(请参阅 Can I Use)。

您可以使用 HTML 标准包含的相同结构化克隆机制在领域之间发送数据。

const clone = structuredClone(original);

有关详细信息,请参阅 the other answer


+1 用于给出最终可能以何种形式内置的想法 - 即使现在无法使用。
o
ooo

(以下主要是整合了@Maciej Bukowski、@A. Levy、@Jan Turoň、@Redu的回答,以及@LeviRoberts、@RobG的评论,多谢他们! !!)

深拷贝? - 是的! (大部分);
浅拷贝? - 不! (Proxy 除外)。

我真诚地欢迎大家测试 clone()
此外,defineProp() 旨在轻松快速地(重新)定义或复制任何类型的描述符。

功能

function clone(object) {
  /*
    Deep copy objects by value rather than by reference,
    exception: `Proxy`
  */

  const seen = new WeakMap()

  return clone(object)


  function clone(object) {
    if (object !== Object(object)) return object /*
    —— Check if the object belongs to a primitive data type */

    if (object instanceof Node) return object.cloneNode(true) /*
    —— Clone DOM trees */

    let _object // The clone of object

    switch (object.constructor) {
      case Array:
      case Object:
        _object = cloneObject(object)
        break

      case Date:
        _object = new Date(+object)
        break

      case Function:
        _object = copyFn(object)
        break

      case RegExp:
        _object = new RegExp(object)
        break

      default:
        switch (Object.prototype.toString.call(object.constructor)) {
          //                                  // Stem from:
          case "[object Function]":
            switch (object[Symbol.toStringTag]) {
              case undefined:
                _object = cloneObject(object) // `class`
                break

              case "AsyncFunction":
              case "GeneratorFunction":
              case "AsyncGeneratorFunction":
                _object = copyFn(object)
                break

              default:
                _object = object
            }
            break

          case "[object Undefined]":          // `Object.create(null)`
            _object = cloneObject(object)
            break

          default:
            _object = object                  // `Proxy`
        }
    }

    return _object
  }


  function cloneObject(object) {
    if (seen.has(object)) return seen.get(object) /*
    —— Handle recursive references (circular structures) */

    const _object = Array.isArray(object)
      ? []
      : Object.create(Object.getPrototypeOf(object)) /*
        —— Assign [[Prototype]] for inheritance */

    seen.set(object, _object) /*
    —— Make `_object` the associative mirror of `object` */

    Reflect.ownKeys(object).forEach(key =>
      defineProp(_object, key, { value: clone(object[key]) }, object)
    )

    return _object
  }
}


function copyPropDescs(target, source) {
  Object.defineProperties(target,
    Object.getOwnPropertyDescriptors(source)
  )
}


function convertFnToStr(fn) {
  let fnStr = String(fn)
  if (fn.name.startsWith("[")) // isSymbolKey
    fnStr = fnStr.replace(/\[Symbol\..+?\]/, '')
  fnStr = /^(?!(async )?(function\b|[^{]+?=>))[^(]+?\(/.test(fnStr)
    ? fnStr.replace(/^(async )?(\*)?/, "$1function$2 ") : fnStr
  return fnStr
}

function copyFn(fn) {
  const newFn = new Function(`return ${convertFnToStr(fn)}`)()
  copyPropDescs(newFn, fn)
  return newFn
}



function defineProp(object, key, descriptor = {}, copyFrom = {}) {
  const { configurable: _configurable, writable: _writable }
    = Object.getOwnPropertyDescriptor(object, key)
    || { configurable: true, writable: true }

  const test = _configurable // Can redefine property
    && (_writable === undefined || _writable) // Can assign to property

  if (!test || arguments.length <= 2) return test

  const basisDesc = Object.getOwnPropertyDescriptor(copyFrom, key)
    || { configurable: true, writable: true } // Custom…
    || {}; // …or left to native default settings

  ["get", "set", "value", "writable", "enumerable", "configurable"]
    .forEach(attr =>
      descriptor[attr] === undefined &&
      (descriptor[attr] = basisDesc[attr])
    )

  const { get, set, value, writable, enumerable, configurable }
    = descriptor

  return Object.defineProperty(object, key, {
    enumerable, configurable, ...get || set
      ? { get, set } // Accessor descriptor
      : { value, writable } // Data descriptor
  })
}

// 测试

const obj0 = {
  u: undefined,
  nul: null,
  t: true,
  num: 9,
  str: "",
  sym: Symbol("symbol"),
  [Symbol("e")]: Math.E,
  arr: [[0], [1, 2]],
  d: new Date(),
  re: /f/g,
  get g() { return 0 },
  o: {
    n: 0,
    o: { f: function (...args) { } }
  },
  f: {
    getAccessorStr(object) {
      return []
        .concat(...
          Object.values(Object.getOwnPropertyDescriptors(object))
            .filter(desc => desc.writable === undefined)
            .map(desc => Object.values(desc))
        )
        .filter(prop => typeof prop === "function")
        .map(String)
    },
    f0: function f0() { },
    f1: function () { },
    f2: a => a / (a + 1),
    f3: () => 0,
    f4(params) { return param => param + params },
    f5: (a, b) => ({ c = 0 } = {}) => a + b + c
  }
}

defineProp(obj0, "s", { set(v) { this._s = v } })
defineProp(obj0.arr, "tint", { value: { is: "non-enumerable" } })
obj0.arr[0].name = "nested array"


let obj1 = clone(obj0)
obj1.o.n = 1
obj1.o.o.g = function g(a = 0, b = 0) { return a + b }
obj1.arr[1][1] = 3
obj1.d.setTime(+obj0.d + 60 * 1000)
obj1.arr.tint.is = "enumerable? no"
obj1.arr[0].name = "a nested arr"
defineProp(obj1, "s", { set(v) { this._s = v + 1 } })
defineProp(obj1.re, "multiline", { value: true })

console.log("\n\n" + "-".repeat(2 ** 6))




console.log(">:>: Test - Routinely")

console.log("obj0:\n ", JSON.stringify(obj0))
console.log("obj1:\n ", JSON.stringify(obj1))
console.log()

console.log("obj0:\n ", obj0)
console.log("obj1:\n ", obj1)
console.log()

console.log("obj0\n ",
  ".arr.tint:", obj0.arr.tint, "\n ",
  ".arr[0].name:", obj0.arr[0].name
)
console.log("obj1\n ",
  ".arr.tint:", obj1.arr.tint, "\n ",
  ".arr[0].name:", obj1.arr[0].name
)
console.log()

console.log("Accessor-type descriptor\n ",
  "of obj0:", obj0.f.getAccessorStr(obj0), "\n ",
  "of obj1:", obj1.f.getAccessorStr(obj1), "\n ",
  "set (obj0 & obj1) .s :", obj0.s = obj1.s = 0, "\n ",
  "  → (obj0 , obj1) ._s:", obj0._s, ",", obj1._s
)

console.log("—— obj0 has not been interfered.")

console.log("\n\n" + "-".repeat(2 ** 6))




console.log(">:>: Test - More kinds of functions")

const fnsForTest = {
  f(_) { return _ },
  func: _ => _,
  aFunc: async _ => _,
  async function() { },
  async asyncFunc() { },
  aFn: async function () { },
  *gen() { },
  async *asyncGen() { },
  aG1: async function* () { },
  aG2: async function* gen() { },
  *[Symbol.iterator]() { yield* Object.keys(this) }
}

console.log(Reflect.ownKeys(fnsForTest).map(k =>
  `${String(k)}:
  ${fnsForTest[k].name}-->
    ${String(fnsForTest[k])}`
).join("\n"))

const normedFnsStr = `{
  f: function f(_) { return _ },
  func: _ => _,
  aFunc: async _ => _,
  function: async function() { },
  asyncFunc: async function asyncFunc() { },
  aFn: async function () { },
  gen: function* gen() { },
  asyncGen: async function* asyncGen() { },
  aG1: async function* () { },
  aG2: async function* gen() { },
  [Symbol.iterator]: function* () { yield* Object.keys(this) }
}`

const copiedFnsForTest = clone(fnsForTest)
console.log("fnsForTest:", fnsForTest)
console.log("fnsForTest (copied):", copiedFnsForTest)
console.log("fnsForTest (normed str):", eval(`(${normedFnsStr})`))
console.log("Comparison of fnsForTest and its clone:",
  Reflect.ownKeys(fnsForTest).map(k =>
    [k, fnsForTest[k] === copiedFnsForTest[k]]
  )
)

console.log("\n\n" + "-".repeat(2 ** 6))




console.log(">:>: Test - Circular structures")

obj0.o.r = {}
obj0.o.r.recursion = obj0.o
obj0.arr[1] = obj0.arr

obj1 = clone(obj0)
console.log("obj0:\n ", obj0)
console.log("obj1:\n ", obj1)

console.log("Clear obj0's recursion:",
  obj0.o.r.recursion = null, obj0.arr[1] = 1
)
console.log(
  "obj0\n ",
  ".o.r:", obj0.o.r, "\n ",
  ".arr:", obj0.arr
)
console.log(
  "obj1\n ",
  ".o.r:", obj1.o.r, "\n ",
  ".arr:", obj1.arr
)
console.log("—— obj1 has not been interfered.")


console.log("\n\n" + "-".repeat(2 ** 6))




console.log(">:>: Test - Classes")

class Person {
  constructor(name) {
    this.name = name
  }
}

class Boy extends Person { }
Boy.prototype.sex = "M"

const boy0 = new Boy
boy0.hobby = { sport: "spaceflight" }

const boy1 = clone(boy0)
boy1.hobby.sport = "superluminal flight"

boy0.name = "one"
boy1.name = "neo"

console.log("boy0:\n ", boy0)
console.log("boy1:\n ", boy1)
console.log("boy1's prototype === boy0's:",
  Object.getPrototypeOf(boy1) === Object.getPrototypeOf(boy0)
)

参考

对象.create() | MDN Object.defineProperties() | MDN 可枚举性和属性所有权 | MDN TypeError: 循环对象值 | MDN

使用的语言技巧

有条件地向对象添加道具


既然 Symbol("a") === Symbol("a")false,那么 clone(Symbol("a")) 不应该使用 Symbol(object.description) 创建一个新符号吗?或者这会对众所周知的符号产生太奇怪的影响?
@SebastianSimon 👍 你的考虑很全面!你的最后一句话more正确,例如(new Map)[Symbol.iterator] vs (new Map)[Symbol(Symbol.iterator.description)]
A
Ashok R

使用 lodash _.cloneDeep()。

浅拷贝:lodash _.clone()

可以通过简单地复制引用来进行浅拷贝。

let obj1 = {
    a: 0,
    b: {
        c: 0,
        e: {
            f: 0
        }
    }
};
let obj3 = _.clone(obj1);
obj1.a = 4;
obj1.b.c = 4;
obj1.b.e.f = 100;

console.log(JSON.stringify(obj1));
//{"a":4,"b":{"c":4,"e":{"f":100}}}

console.log(JSON.stringify(obj3));
//{"a":0,"b":{"c":4,"e":{"f":100}}}

https://i.stack.imgur.com/vmysI.png

深拷贝:lodash _.cloneDeep()

字段被取消引用:而不是对被复制对象的引用

let obj1 = {
    a: 0,
    b: {
        c: 0,
        e: {
            f: 0
        }
    }
};
let obj3 = _.cloneDeep(obj1);
obj1.a = 100;
obj1.b.c = 100;
obj1.b.e.f = 100;

console.log(JSON.stringify(obj1));
{"a":100,"b":{"c":100,"e":{"f":100}}}

console.log(JSON.stringify(obj3));
{"a":0,"b":{"c":0,"e":{"f":0}}}

https://i.stack.imgur.com/z0M6n.png