วิธีการโคลนลึกในจาวาสคริปต์


107

คุณโคลนวัตถุ Javascript ได้อย่างไร?

ฉันรู้ว่ามีฟังก์ชั่นต่าง ๆ ตามเฟรมเวิร์กเช่นJSON.parse(JSON.stringify(o))และ$.extend(true, {}, o)แต่ฉันไม่ต้องการใช้เฟรมเวิร์กแบบนั้น

วิธีใดเป็นวิธีที่สง่างามหรือมีประสิทธิภาพที่สุดในการสร้างโคลนลึก

เราดูแลเกี่ยวกับ edge case เช่นการโคลนอาร์เรย์ ไม่ทำลายโซ่ต้นแบบจัดการกับการอ้างอิงตัวเอง

เราไม่สนใจเกี่ยวกับการสนับสนุนการคัดลอกวัตถุ DOM และชอบเพราะ.cloneNodeมีอยู่ด้วยเหตุผลนั้น

เนื่องจากฉันต้องการใช้โคลนลึกเป็นหลักในการnode.jsใช้คุณสมบัติ ES5 ของเครื่องยนต์ V8 เป็นที่ยอมรับ

[แก้ไข]

ก่อนที่ทุกคนแสดงให้เห็นให้ฉันพูดถึงมีความแตกต่างที่ชัดเจนระหว่างการสร้างสำเนาโดย prototypically สืบทอดจากวัตถุและโคลนมัน อดีตทำให้โซ่ต้นแบบยุ่งเหยิง

[แก้ไขเพิ่มเติม]

หลังจากอ่านคำตอบของคุณฉันก็ได้พบกับการค้นพบที่น่ารำคาญว่าการโคลนวัตถุทั้งหมดเป็นเกมที่อันตรายและยากมาก ยกตัวอย่างเช่นวัตถุตามการปิดต่อไปนี้

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); // how to implement clone to support closures

มีวิธีใดบ้างในการเขียนฟังก์ชันโคลนที่โคลนอ็อบเจ็กต์มีสถานะเดียวกันในขณะทำการโคลนนิ่ง แต่ไม่สามารถเปลี่ยนสถานะได้oโดยไม่ต้องเขียนตัวแยกวิเคราะห์ JS ใน JS

ไม่ควรมีความจำเป็นในโลกแห่งความเป็นจริงสำหรับฟังก์ชันดังกล่าวอีกต่อไป นี่เป็นเพียงความสนใจทางวิชาการ


2
ก่อนที่จะถูกทำเครื่องหมายว่าซ้ำกันฉันดูที่stackoverflow.com/questions/122102/…และไม่พบคำตอบใด ๆ ที่เกี่ยวข้องกับกรณีขอบทั้งหมด
Raynos

ข้อกำหนดในส่วน "แก้ไขเพิ่มเติม" เป็นไปไม่ได้ที่จะบรรลุโดยไม่มี "ความช่วยเหลือ" จากออบเจ็กต์เองเนื่องจากตัวแปรส่วนตัวดังกล่าวเป็นส่วนตัวอย่างแท้จริงและด้วยเหตุcloneนี้ฟังก์ชันทั่วไปจึงไม่สามารถเข้าถึงได้ ออบเจ็กต์ที่เป็นปัญหาควรแสดงcloneวิธีการที่ปรับแต่งขึ้นเอง
trincot

คืนนี้ฉันได้อ่านเกี่ยวกับเรื่องนี้เป็นจำนวนมากและในบรรดาแหล่งข้อมูลที่ฉันพบคือบล็อกโพสต์ที่ดูน่าเกลียดซึ่งมีแฮ็กสองสามตัวสำหรับการเข้าถึงอัลกอริธึมโคลนที่มีโครงสร้างในเบราว์เซอร์: dassur.ma/things/deep-copy
Cat


เมื่อคุณพูดว่า " ฉันรู้ว่ามีฟังก์ชั่นต่าง ๆ ที่อยู่บนพื้นฐานของกรอบเหมือนJSON.parse(JSON.stringify(o))และ$.extend(true, {}, o)แต่ฉันไม่ต้องการที่จะใช้กรอบเช่นนั้น " ฉันสับสน $แน่นอนห้องสมุดภายนอก (jQuery) แต่JSONเป็นส่วนหนึ่งของวานิลลา JavaScript อะไรคือความเกลียดชัง (ถ้าคุณจำได้) ที่จะใช้JSON? ในปี 2010 อาจขาดการรองรับ IE 6 & 7 ?
ruffin

คำตอบ:


68

มันขึ้นอยู่กับสิ่งที่คุณต้องการจะโคลน นี่เป็นออบเจ็กต์ JSON อย่างแท้จริงหรือเป็นเพียงวัตถุใน JavaScript? หากคุณต้องการทำการโคลนใด ๆ มันอาจทำให้คุณมีปัญหา ปัญหาไหน? ฉันจะอธิบายไว้ด้านล่าง แต่ก่อนอื่นคือตัวอย่างโค้ดที่โคลนวัตถุตัวอักษรดั้งเดิมอาร์เรย์และโหนด DOM

function clone(item) {
    if (!item) { return item; } // null, undefined values check

    var types = [ Number, String, Boolean ], 
        result;

    // normalizing primitives if someone did new String('aaa'), or 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") {
            // testing that this is DOM
            if (item.nodeType && typeof item.cloneNode == "function") {
                result = item.cloneNode( true );    
            } else if (!item.prototype) { // check that this is a literal
                if (item instanceof Date) {
                    result = new Date(item);
                } else {
                    // it is an object literal
                    result = {};
                    for (var i in item) {
                        result[i] = clone( item[i] );
                    }
                }
            } else {
                // depending what you would like here,
                // just keep the reference, or create new object
                if (false && item.constructor) {
                    // would not advice to do that, reason? Read below
                    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 )คำสั่ง

เหตุใดการโคลนนิ่งจึงเป็นความเจ็บปวด? ก่อนอื่นทุกออบเจ็กต์ / อินสแตนซ์อาจมีสถานะบางอย่าง คุณไม่มีทางแน่ใจได้เลยว่าอ็อบเจกต์ของคุณไม่มีตัวอย่างเช่นตัวแปรส่วนตัวและหากเป็นกรณีนี้การโคลนนิ่งอ็อบเจกต์คุณจะทำลายสถานะ

ลองนึกภาพว่าไม่มีรัฐก็ไม่เป็นไร แล้วเรายังมีปัญหาอีก การโคลนด้วยวิธี "ตัวสร้าง" จะทำให้เรามีอุปสรรคอีกอย่าง เป็นการพึ่งพาอาร์กิวเมนต์ คุณไม่มีทางแน่ใจได้เลยว่ามีคนสร้างวัตถุนี้ไม่ได้ทำบางอย่าง

new User({
   bike : someBikeInstance
});

หากเป็นกรณีนี้แสดงว่าคุณโชคไม่ดี someBikeInstance อาจถูกสร้างขึ้นในบางบริบทและบริบทนั้นไม่รู้จักสำหรับวิธีการโคลน

แล้วจะทำอย่างไร? คุณยังคงสามารถfor inแก้ปัญหาและปฏิบัติต่อวัตถุดังกล่าวได้เช่นตามตัวอักษรของวัตถุทั่วไป แต่อาจเป็นความคิดที่จะไม่โคลนวัตถุดังกล่าวเลยและเพียงแค่ส่งต่อการอ้างอิงของวัตถุนี้?

อีกวิธีหนึ่งคือ - คุณสามารถกำหนดแบบแผนว่าอ็อบเจ็กต์ทั้งหมดที่ต้องโคลนควรใช้ส่วนนี้ด้วยตัวเองและจัดเตรียมเมธอด API ที่เหมาะสม (เช่น cloneObject) สิ่งที่cloneNodeกำลังทำเพื่อ DOM

คุณตัดสินใจ.


ฉันเจออุปสรรคในการจัดการกับวัตถุที่ใช้การปิดเพื่อซ่อนสถานะตัวเอง เราสามารถโคลนวัตถุและสถานะทั้งหมดได้อย่างไร แต่ยังคงมั่นใจว่าโคลนไม่สามารถเปลี่ยนแปลงสถานะต้นฉบับได้ด้วยตัวมันเอง จุด couinter ที่result = new item.constructor();ไม่ดีคือการกำหนดฟังก์ชันคอนสตรัคเตอร์และอ็อบเจ็กต์ไอเท็มคุณควรจะสามารถ RE พารามิเตอร์ใด ๆ ที่ส่งผ่านเข้าไปในคอนสตรัคเตอร์
Raynos

8
@Raynos: หากวัตถุใช้การปิดเพื่อซ่อนสถานะคุณจะไม่สามารถโคลนได้ ดังนั้นคำว่า 'ปิด' ดังที่ nemisj กล่าวในตอนท้ายวิธีที่ดีที่สุดคือการใช้วิธี API สำหรับการโคลน (หรือ serialization / deserialization) หากเป็นตัวเลือก
Michiel Kalkman

@MichielKalkman ฉันมีความรู้สึกที่เป็นเช่นนั้น แม้ว่าใครบางคนอาจมีวิธีแก้ปัญหาที่ชาญฉลาดจริงๆ
Raynos

ผลของfalse && item.constructorอะไร? ไม่ifไร้ประโยชน์เหรอ?
Gabriel Petrovay

2
@GabrielPetrovay นั่นifคือ 'ไร้ประโยชน์' จากมุมมองการใช้งานเพราะมันจะไม่เคยทำงาน แต่มันมีจุดประสงค์ทางวิชาการในการแสดงการใช้งานสมมุติที่อาจพยายามใช้ซึ่งผู้เขียนไม่ได้ให้คำแนะนำเนื่องจากเหตุผลที่อธิบายในภายหลัง ใช่มันจะทริกเกอร์elseประโยคทุกครั้งที่มีการประเมินเงื่อนไข แต่ก็มีเหตุผลที่รหัสจะอยู่ที่นั่น
Gui Imamura

158

วิธีที่ง่ายมากอาจจะง่ายเกินไป:

var cloned = JSON.parse(JSON.stringify(objectToClone));

12
ดีมากเว้นแต่ค่าวัตถุจะเป็นฟังก์ชัน ณ จุดนี้คุณจะต้องใช้คำตอบที่ยอมรับ หรือใช้ฟังก์ชันตัวช่วยเช่นcloneDeepใน Lodash
matthoiland

31
ถ้าค่าวัตถุเป็นฟังก์ชันวัตถุจะไม่มี JSON
Jos de Jong

6
กรณีการใช้งานใดที่สามารถใช้เพื่อโคลนฟังก์ชันแทนที่จะใช้เพียงอย่างเดียว
G.Ghez

3
ถ้าฉันจำไม่ผิดสิ่งนี้จะแปลงวันที่เป็นสตริงด้วย
ปีเตอร์

3
@ G.Ghez ถ้าคุณโคลนวัตถุที่มีฟังก์ชันฟังก์ชันนั้นจะหายไป ..
ปีเตอร์

38

การJSON.parse(JSON.stringify())รวมกันเพื่อคัดลอกออบเจ็กต์ Javascript แบบเจาะลึกเป็นการแฮ็กที่ไม่มีประสิทธิภาพเนื่องจากมีไว้สำหรับข้อมูล JSON ไม่สนับสนุนค่าของundefinedหรือfunction () {}และจะเพิกเฉยต่อค่าเหล่านี้ (หรือค่าnullนั้น) เมื่อ "stringifying" (การจัดเรียง) วัตถุ Javascript ลงใน JSON

ทางออกที่ดีกว่าคือการใช้ฟังก์ชันสำเนาลึก ฟังก์ชันด้านล่างคัดลอกวัตถุในรายละเอียดและไม่จำเป็นต้องใช้ไลบรารีของบุคคลที่สาม (jQuery, LoDash ฯลฯ )

function copy(aObject) {
  if (!aObject) {
    return aObject;
  }

  let v;
  let bObject = Array.isArray(aObject) ? [] : {};
  for (const k in aObject) {
    v = aObject[k];
    bObject[k] = (typeof v === "object") ? copy(v) : v;
  }

  return bObject;
}

7
ยกเว้นเมื่อ aObject (หรือวัตถุอื่นมี) มีการอ้างอิงตัวเองถึงตัวมันเอง ... stackoverflow ™!
e2-e4

@ ringø - คุณสามารถระบุกรณีทดสอบ "อ้างอิงตนเอง" ได้หรือไม่
tfmontague

4
var o = { a:1, b:2 } ; o["oo"] = { c:3, m:o };
e2-e4

4
ฉันชอบวิธีนี้ เฉพาะการแก้ไขสำหรับฉันคือการจัดการกับค่าว่าง: bObject[k] = (v === null) ? null : (typeof v === "object") ? copy(v) : v;
David Kirkland

2
ฟังก์ชั่นนี้เรียบง่ายเข้าใจง่ายและจะจับได้เกือบทุกกรณี ในโลกของ JavaScript ที่สมบูรณ์แบบที่สุดเท่าที่คุณจะทำได้
icc97

23

นี่คือฟังก์ชัน ES6 ที่จะทำงานกับวัตถุที่มีการอ้างอิงแบบวนซ้ำ:

function deepClone(obj, hash = new WeakMap()) {
    if (Object(obj) !== obj) return obj; // primitives
    if (hash.has(obj)) return hash.get(obj); // cyclic reference
    const result = obj instanceof Set ? new Set(obj) // See note about this!
                 : obj instanceof Map ? new Map(Array.from(obj, ([key, val]) => 
                                        [key, deepClone(val, hash)])) 
                 : obj instanceof Date ? new Date(obj)
                 : obj instanceof RegExp ? new RegExp(obj.source, obj.flags)
                 // ... add here any specific treatment for other classes ...
                 // and finally a catch-all:
                 : obj.constructor ? new obj.constructor() 
                 : Object.create(null);
    hash.set(obj, result);
    return Object.assign(result, ...Object.keys(obj).map(
        key => ({ [key]: deepClone(obj[key], hash) }) ));
}

// Sample data
var p = {
  data: 1,
  children: [{
    data: 2,
    parent: null
  }]
};
p.children[0].parent = p;

var q = deepClone(p);

console.log(q.children[0].parent.data); // 1

หมายเหตุเกี่ยวกับชุดและแผนที่

วิธีจัดการกับคีย์ของชุดและแผนที่เป็นที่ถกเถียงกันอยู่: คีย์เหล่านั้นมักเป็นแบบดั้งเดิม (ซึ่งในกรณีนี้ไม่มีการถกเถียงกัน) แต่ก็สามารถเป็นวัตถุได้เช่นกัน ในกรณีนี้คำถามจะกลายเป็น: คีย์เหล่านั้นควรถูกโคลนหรือไม่?

อาจมีคนโต้แย้งว่าควรทำสิ่งนี้เพื่อที่ว่าหากวัตถุเหล่านั้นกลายพันธุ์ในสำเนาวัตถุในต้นฉบับจะไม่ได้รับผลกระทบและในทางกลับกัน

ในทางกลับกันคนหนึ่งต้องการว่าหากตั้งค่า / แมปhasคีย์ควรเป็นจริงทั้งในต้นฉบับและสำเนา - อย่างน้อยก่อนที่จะทำการเปลี่ยนแปลงใด ๆ กับคีย์ใดคีย์หนึ่ง มันจะแปลกถ้าสำเนาจะเป็น Set / Map ที่มีคีย์ที่ไม่เคยเกิดขึ้นมาก่อน (เหมือนถูกสร้างขึ้นในระหว่างขั้นตอนการโคลน) แน่นอนว่ามันไม่มีประโยชน์มากสำหรับโค้ดใด ๆ ที่จำเป็นต้องรู้ว่าอ็อบเจกต์นั้นเป็น คีย์ใน Set / Map นั้นหรือไม่

ดังที่คุณสังเกตเห็นฉันมีความคิดเห็นที่สองมากกว่า: กุญแจของชุดและแผนที่คือค่า (อาจอ้างอิง ) ที่ควรจะยังคงเหมือนเดิม

ตัวเลือกดังกล่าวมักจะปรากฏร่วมกับวัตถุอื่น ๆ (อาจกำหนดเอง) ไม่มีวิธีแก้ปัญหาทั่วไปมากนักทั้งนี้ขึ้นอยู่กับว่าวัตถุที่ถูกโคลนคาดว่าจะทำงานอย่างไรในกรณีเฉพาะของคุณ


ไม่จัดการวันที่และ regexp
mkeremguc

1
@mkeremguc ขอบคุณสำหรับความคิดเห็นของคุณ ฉันอัปเดตรหัสเพื่อรองรับวันที่และ regexp
trincot

1
ถ้าฉันจำไม่ผิดคุณสามารถเพิ่มการสนับสนุนสำหรับชุดได้ด้วย:if (object instanceof Set) Array.from(object, val => result.add(deepClone(val, hash)));
Robert Biggs

1
@RobertBiggs นั่นเป็นความเป็นไปได้อย่างหนึ่ง แต่ในความคิดของฉันถ้า Set มีคีย์ที่แน่นอนก็ควรจะเป็นจริงในเวอร์ชันโคลนของ Set นั้น ด้วยรหัสที่คุณแนะนำสิ่งนี้จะไม่ถือเป็นจริงหากคีย์เป็นวัตถุ ดังนั้นฉันไม่แนะนำให้โคลนกุญแจ - ฉันคิดว่ามันจะทำงานได้มากกว่าที่คาดไว้ ดูการอัปเดตคำตอบของฉันในแง่นั้น
trincot

1
จริง แต่ถ้าคุณกำลังทำงานกับโค้ดรีแอคทีฟที่คาดว่าข้อมูลที่ไม่เปลี่ยนรูปคุณก็ต้องการเซ็ตที่เป็นโคลนนิ่งแบบลึกเช่นกัน ฉันต้องยอมรับว่าบางครั้งฉันต้องการวัตถุที่มีพอยน์เตอร์ไปยังวัตถุระยะไกลเพื่อที่ฉันจะได้กลายพันธุ์จากที่นั่นแทนที่จะต้องยื่นมือไปหาต้นฉบับ ที่กล่าวมาดูเหมือนว่าฉันจะทำแบบนั้นน้อยลงเรื่อย ๆ ตามกาลเวลา การมีข้อมูลที่ไม่เปลี่ยนรูปจะมีความหมายมากกว่าในสถานการณ์อื่น ๆ
Robert Biggs

10

Underscore.js ห้องสมุด contribห้องสมุดมีฟังก์ชั่นที่เรียกว่าภาพรวมว่าโคลนลึกวัตถุ

ตัวอย่างจากแหล่งที่มา:

snapshot: function(obj) {
  if(obj == null || typeof(obj) != 'object') {
    return obj;
  }

  var temp = new obj.constructor();

  for(var key in obj) {
    if (obj.hasOwnProperty(key)) {
      temp[key] = _.snapshot(obj[key]);
    }
  }

  return temp;
}

เมื่อห้องสมุดเชื่อมโยงกับโครงการของคุณแล้วให้เรียกใช้ฟังก์ชันโดยใช้

_.snapshot(object);

4
ทางออกที่ดีเพียงแค่จุดที่ต้องจำ: โคลนและต้นฉบับใช้ต้นแบบเดียวกัน หากเป็นปัญหาก็สามารถเพิ่ม "temp .__ proto__ = .snapshot (obj .__ proto_ );" เหนือ "return temp" และเพื่อรองรับคลาส build-in ที่มีคุณสมบัติที่ระบุว่า 'no enumerate' คุณสามารถทำซ้ำบน getOwnPropertyNames () แทน "for (var key in obj)"
Ronen Ness

3

นี่เป็นวิธีการโคลนนิ่งแบบลึกที่ฉันใช้ฉันคิดว่าดีมากหวังว่าคุณจะให้คำแนะนำ

function deepClone (obj) {
    var _out = new obj.constructor;

    var getType = function (n) {
        return Object.prototype.toString.call(n).slice(8, -1);
    }

    for (var _key in obj) {
        if (obj.hasOwnProperty(_key)) {
            _out[_key] = getType(obj[_key]) === 'Object' || getType(obj[_key]) === 'Array' ? deepClone(obj[_key]) : obj[_key];
        }
    }
    return _out;
}

3

ดังที่คนอื่น ๆ ได้ตั้งข้อสังเกตเกี่ยวกับคำถามนี้และคำถามที่คล้ายกันการโคลน "วัตถุ" ในความหมายทั่วไปเป็นเรื่องที่น่าสงสัยใน JavaScript

อย่างไรก็ตามมีคลาสของอ็อบเจ็กต์ซึ่งฉันเรียกว่าอ็อบเจ็กต์ "data" นั่นคือสิ่งที่สร้างขึ้นจาก{ ... }ตัวอักษรและ / หรือการกำหนดคุณสมบัติอย่างง่ายหรือ deserialized จาก JSON ซึ่งมีเหตุผลที่จะต้องการโคลน วันนี้ฉันต้องการขยายข้อมูลที่ได้รับจากเซิร์ฟเวอร์ขึ้น 5 เท่าเทียมเพื่อทดสอบว่าเกิดอะไรขึ้นกับชุดข้อมูลขนาดใหญ่ แต่ออบเจ็กต์ (อาร์เรย์) และลูกของมันต้องเป็นวัตถุที่แตกต่างกันเพื่อให้สิ่งต่างๆทำงานได้อย่างถูกต้อง การโคลนทำให้ฉันทำสิ่งนี้เพื่อคูณชุดข้อมูลของฉัน:

return dta.concat(clone(dta),clone(dta),clone(dta),clone(dta));

อีกที่หนึ่งที่ฉันมักจะลงเอยด้วยการโคลนออบเจ็กต์ข้อมูลคือการส่งข้อมูลกลับไปยังโฮสต์ที่ฉันต้องการดึงฟิลด์สถานะออกจากอ็อบเจ็กต์ในโมเดลข้อมูลก่อนส่ง ตัวอย่างเช่นฉันอาจต้องการดึงช่องทั้งหมดที่ขึ้นต้นด้วย "_" ออกจากวัตถุเมื่อมีการโคลน

นี่คือรหัสที่ฉันเขียนขึ้นเพื่อทำสิ่งนี้โดยทั่วไปรวมถึงการสนับสนุนอาร์เรย์และตัวเลือกเพื่อเลือกสมาชิกที่จะโคลน (ซึ่งใช้สตริง "เส้นทาง" เพื่อกำหนดบริบท):

function clone(obj,sel) {
    return (obj ? _clone("",obj,sel) : obj);
    }

function _clone(pth,src,sel) {
    var ret=(src instanceof Array ? [] : {});

    for(var key in src) {
        if(!src.hasOwnProperty(key)) { continue; }

        var val=src[key], sub;

        if(sel) {
            sub+=pth+"/"+key;
            if(!sel(sub,key,val)) { continue; }
            }

        if(val && typeof(val)=='object') {
            if     (val instanceof Boolean) { val=Boolean(val);        }
            else if(val instanceof Number ) { val=Number (val);        }
            else if(val instanceof String ) { val=String (val);        }
            else                            { val=_clone(sub,val,sel); }
            }
        ret[key]=val;
        }
    return ret;
    }

วิธีแก้ปัญหาการโคลนในระดับลึกที่ง่ายที่สุดโดยถือว่าออบเจ็กต์รูทที่ไม่เป็นค่าว่างและไม่มีการเลือกสมาชิกคือ:

function clone(src) {
    var ret=(src instanceof Array ? [] : {});
    for(var key in src) {
        if(!src.hasOwnProperty(key)) { continue; }
        var val=src[key];
        if(val && typeof(val)=='object') { val=clone(val);  }
        ret[key]=val;
        }
    return ret;
    }

2

Lo-Dashตอนนี้เป็นส่วนเหนือของUnderscore.jsมีฟังก์ชันโคลนลึกสองสามฟังก์ชัน:

จากคำตอบของผู้เขียนเอง:

lodash underscore สร้างขึ้นเพื่อให้แน่ใจว่าเข้ากันได้กับ Underscore เวอร์ชันเสถียรล่าสุด


คำถามบอกว่า "ฉันไม่ต้องการใช้ห้องสมุด"
Femi Oni

@ FemiOni คำถามไม่มีอะไรเกี่ยวกับไลบรารี (แม้จะเป็นการแก้ไขแบบเก่าก็ตาม) ... คำตอบอื่น ๆ ในที่นี้ก็ใช้ไลบรารีหนึ่งหรืออีกอันเช่นกัน
CPHPython

@FemiOni ตอบโหวตเมื่อวานนี้ ฉันสงสัยว่า ... อย่างไรก็ตามนี่เป็นสถานที่เรียนรู้และในกรณีที่มีคนกำลังจะใช้การโคลนลึกจริง ๆซอร์สโค้ด lodashbaseCloneอาจให้แนวคิดบางอย่าง
CPHP

@FemiOni อ็อบเจกต์JSONไม่ใช่ทั้งไลบรารีหรือเฟรมเวิร์ก ... หากคุณจะใช้ฟังก์ชันนี้ฉันขอแนะนำให้ดูไลบรารีโอเพนซอร์สและใช้ส่วนที่คุณต้องการ (หลาย ๆ ชิ้นได้รับการทดสอบมาหลายปีแล้ว) มันจะหลีกเลี่ยงข้อบกพร่องและพลาดการพิจารณาในระยะยาว
CPHPython

2

ฟังก์ชันด้านล่างนี้เป็นวิธีที่มีประสิทธิภาพที่สุดในการโคลนวัตถุจาวาสคริปต์แบบลึก

function deepCopy(obj){
    if (!obj || typeof obj !== "object") return obj;

    var retObj = {};

    for (var attr in obj){
        var type = obj[attr];

        switch(true){
            case (type instanceof Date):
                var _d = new Date();
                _d.setDate(type.getDate())
                retObj[attr]= _d;
                break;

            case (type instanceof Function):
                retObj[attr]= obj[attr];
                break;

            case (type instanceof Array):
                var _a =[];
                for (var e of type){
                    //_a.push(e);
                    _a.push(deepCopy(e));
                }
                retObj[attr]= _a;
                break;

            case (type instanceof Object):
                var _o ={};
                for (var e in type){
                    //_o[e] = type[e];
                    _o[e] = deepCopy(type[e]);
                }
                retObj[attr]= _o;
                break;

            default:
                retObj[attr]= obj[attr];
        }
    }
    return retObj;
}

var obj = {
    string: 'test',
    array: ['1'],
    date: new Date(),
    object:{c: 2, d:{e: 3}},
    function: function(){
        return this.date;
    }
};

var copyObj = deepCopy(obj);

console.log('object comparison', copyObj === obj); //false
console.log('string check', copyObj.string === obj.string); //true
console.log('array check', copyObj.array === obj.array); //false
console.log('date check', copyObj2.date === obj.date); //false
console.log('object check', copyObj.object === obj.object); //false
console.log('function check', copyObj.function() === obj.function()); //true

2
คุณมีข้อโต้แย้งที่จะสนับสนุนการอ้างสิทธิ์ของคุณหรือไม่?
Fabian von Ellerts

ฉันได้ยกตัวอย่างด้านล่างของฟังก์ชัน คุณมีข้อสงสัยหรือไม่?
Kooldandy

ตัวอย่างเหล่านี้แสดงให้เห็นว่าฟังก์ชันนี้ใช้งานได้ดี แต่เหตุใดจึงเป็น "วิธีที่มีประสิทธิภาพสูงสุด"
Fabian von Ellerts

เนื่องจากเป็นการคัดลอกแอตทริบิวต์ของออบเจ็กต์แบบวนซ้ำแบบวนซ้ำ วันที่, ฟังก์ชัน, วัตถุ, อาร์เรย์, หมายเลข, สตริงทั้งหมดได้รับการจัดการอย่างเหมาะสม คุณมีวิธีอื่นหรือไม่?
Kooldandy

2

ไม่ควรมีความจำเป็นในโลกแห่งความเป็นจริงสำหรับฟังก์ชันดังกล่าวอีกต่อไป นี่เป็นเพียงความสนใจทางวิชาการ

เป็นการออกกำลังกายอย่างแท้จริงวิธีนี้เป็นวิธีที่ได้ผลมากกว่า เป็นส่วนขยายของคำตอบของ@ tfmontagueเนื่องจากฉันแนะนำให้เพิ่มบล็อกป้องกันที่นั่น แต่เมื่อฉันรู้สึกว่าถูกบังคับให้ใช้ ES6 และใช้งานได้ทุกอย่างนี่คือเวอร์ชันที่ผิดพลาดของฉัน มันทำให้ตรรกะซับซ้อนเนื่องจากคุณต้องแมปบนอาร์เรย์และลดลงบนวัตถุ แต่หลีกเลี่ยงการกลายพันธุ์ใด ๆ

const cloner = (x) => {
    const recurseObj = x => (typeof x === 'object') ? cloner(x) : x
    const cloneObj = (y, k) => {
        y[k] = recurseObj(x[k])
        return y
    }
    // Guard blocks
    // Add extra for Date / RegExp if you want
    if (!x) {
        return x
    }
    if (Array.isArray(x)) {
        return x.map(recurseObj)
    }
    return Object.keys(x).reduce(cloneObj, {})
}
const tests = [
    null,
    [],
    {},
    [1,2,3],
    [1,2,3, null],
    [1,2,3, null, {}],
    [new Date('2001-01-01')], // FAIL doesn't work with Date
    {x:'', y: {yx: 'zz', yy: null}, z: [1,2,3,null]},
    {
        obj : new function() {
            this.name = "Object test";
        }
    } // FAIL doesn't handle functions
]
tests.map((x,i) => console.log(i, cloner(x)))


1

ฉันสังเกตเห็นว่าแผนที่ควรต้องได้รับการดูแลเป็นพิเศษดังนั้นด้วยคำแนะนำทั้งหมดในเธรดนี้รหัสจะเป็น:

function deepClone( obj ) {
    if( !obj || true == obj ) //this also handles boolean as true and false
        return obj;
    var objType = typeof( obj );
    if( "number" == objType || "string" == objType ) // add your immutables here
        return obj;
    var result = Array.isArray( obj ) ? [] : !obj.constructor ? {} : new obj.constructor();
    if( obj instanceof Map )
        for( var key of obj.keys() )
            result.set( key, deepClone( obj.get( key ) ) );
    for( var key in obj )
        if( obj.hasOwnProperty( key ) )
            result[key] = deepClone( obj[ key ] );
    return result;
}

1

นอกจากคำตอบทั้งหมดของฉันแล้ว

deepCopy = arr => {
  if (typeof arr !== 'object') return arr
  if(arr.pop) return [...arr].map(deepCopy)
  const copy = {}
  for (let prop in arr)
    copy[prop] = deepCopy(arr[prop])
  return copy
}

0

สิ่งนี้ใช้ได้กับอาร์เรย์วัตถุและวัตถุดั้งเดิม อัลกอริธึมแบบวนซ้ำทวีคูณที่สลับไปมาระหว่างสองวิธีการข้ามผ่าน:

const deepClone = (objOrArray) => {

  const copyArray = (arr) => {
    let arrayResult = [];
    arr.forEach(el => {
        arrayResult.push(cloneObjOrArray(el));
    });
    return arrayResult;
  }

  const copyObj = (obj) => {
    let objResult = {};
    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        objResult[key] = cloneObjOrArray(obj[key]);
      }
    }
    return objResult;
  }

  const cloneObjOrArray = (el) => {
    if (Array.isArray(el)) {
      return copyArray(el);
    } else if (typeof el === 'object') {
      return copyObj(el);
    } else {
      return el;
    }
  }

  return cloneObjOrArray(objOrArray);
}

0

เราสามารถใช้การเรียกซ้ำสำหรับการทำ deepCopy สามารถสร้างสำเนาของอาร์เรย์วัตถุอาร์เรย์ของวัตถุวัตถุที่มีฟังก์ชัน หากคุณต้องการคุณสามารถเพิ่มฟังก์ชันสำหรับโครงสร้างข้อมูลประเภทอื่น ๆ เช่นแผนที่เป็นต้น

function deepClone(obj) {
         var retObj;
        _assignProps = function(obj, keyIndex, retObj) {
               var subType = Object.prototype.toString.call(obj[keyIndex]);
               if(subType === "[object Object]" || subType === "[object Array]") {
                    retObj[keyIndex] = deepClone(obj[keyIndex]);
               }
               else {
                     retObj[keyIndex] = obj[keyIndex];
               }
        };

        if(Object.prototype.toString.call(obj) === "[object Object]") {
           retObj = {};
           for(key in obj) {
               this._assignProps(obj, key, retObj);
           }
        }
        else if(Object.prototype.toString.call(obj) == "[object Array]") {
           retObj = [];
           for(var i = 0; i< obj.length; i++) {
              this._assignProps(obj, i, retObj);
            }
        };

        return retObj;
    };

0

ใช้ไม่เปลี่ยนรูป JS

import { fromJS } from 'immutable';

// An object we want to clone
let objA = { 
   a: { deep: 'value1', moreDeep: {key: 'value2'} } 
};

let immB = fromJS(objA); // Create immutable Map
let objB = immB.toJS(); // Convert to plain JS object

console.log(objA); // Object { a: { deep: 'value1', moreDeep: {key: 'value2'} } }
console.log(objB); // Object { a: { deep: 'value1', moreDeep: {key: 'value2'} } }

// objA and objB are equalent, but now they and their inner objects are undependent
console.log(objA === objB); // false
console.log(objA.a === objB.a); // false
console.log(objA.moreDeep === objB.moreDeep); // false

หรือlodash / merge

import merge from 'lodash/merge'

var objA = {
    a: [{ 'b': 2 }, { 'd': 4 }]
};
// New deeply cloned object:
merge({}, objA ); 

// We can also create new object from several objects by deep merge:
var objB = {
    a: [{ 'c': 3 }, { 'e': 5 }]
};
merge({}, objA , objB ); // Object { a: [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }

0

อันนี้ใช้การอ้างอิงแบบวงกลมได้ผลสำหรับฉัน

 //a test-object with circular reference :
 var n1 = {   id:0,   text:"aaaaa",   parent:undefined} 
 var n2 = {  id:1,   text:"zzzzz",   parent:undefined } 
 var o = { arr:[n1,n2],   parent:undefined } 
 n1.parent = n2.parent = o;
 var obj = {   a:1,   b:2,   o:o }
 o.parent = obj;

 function deepClone(o,output){ 

     if(!output) output = {};  
     if(o.______clone) return o.______clone;
     o.______clone = output.______clone = output;

   for(var z in o){

     var obj = o[z];
     if(typeof(obj) == "object") output[z] = deepClone(obj)
     else output[z] = obj; 
    }

   return output;
}

console.log(deepClone(obj));

0

var newDate = วันที่ใหม่ (this.oldDate); ฉันกำลังส่ง oldDate ไปยังฟังก์ชันและสร้าง newDate จาก this.oldDate แต่มันกำลังเปลี่ยน this.oldDate ด้วยดังนั้นฉันจึงใช้วิธีแก้ปัญหานั้นและมันก็ใช้ได้


0

วิธีนี้จะหลีกเลี่ยงปัญหาการเรียกซ้ำเมื่อใช้ [... target] หรือ {... target}

function shallowClone(target) {
  if (typeof a == 'array') return [...target]
  if (typeof a == 'object') return {...target}
  return target
}

/* set skipRecursion to avoid throwing an exception on recursive references */
/* no need to specify refs, or path -- they are used interally */
function deepClone(target, skipRecursion, refs, path) {
  if (!refs) refs = []
  if (!path) path = ''
  if (refs.indexOf(target) > -1) {
    if (skipRecursion) return null
    throw('Recursive reference at ' + path)
  }
  refs.push(target)
  let clone = shallowCopy(target)
  for (i in target) target[i] = deepClone(target, refs, path + '.' + i)
  return clone
}

0

หลีกเลี่ยงการใช้วิธีนี้

let cloned = JSON.parse(JSON.stringify(objectToClone));

ทำไม? วิธีนี้จะแปลง 'function, undefined' เป็นnull

const myObj = [undefined, null, function () {}, {}, '', true, false, 0, Symbol];

const IsDeepClone = JSON.parse(JSON.stringify(myObj));

console.log(IsDeepClone); //[null, null, null, {…}, "", true, false, 0, null]

พยายามใช้ฟังก์ชัน deepClone มีหลายประการข้างต้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.