Franz: Javascript, Array, Objekte kopieren

Hallo,

Ich habe einen Teil eines assoziativen Arrays an das selbige angehängt (kopiert) und wollte diverse Werte dann ändern. Die gewünschten Werte wurden auch geändert. Allerdings die der Quellelemente auch.
Im alten Wiki stand der Hinweis, das eine Art Zeiger auf die Objekte gespeichert wird. Einen solchen Hinweis vermisse ich im aktuellen Wiki.
Mit der Suchfunktion fand ich diverse Erklärungen, das Texte tatsächlich kopiert Objekte hingegen nur verlinkt werden. Ein Lösungsweg zu einer echten Kopie fand ich jedoch nirgendwo.
Selbst wenn ich die fraglichen Objekte mittels Schleife durchlaufe und dann per push-Methode anhänge (auch über ein eigens dafür erstelltes Array) bleibt es doch nur ein Verweis.

gekürztes Beispiel-Array:

var camListe = [
 {
  "LfdBearbId": 1,
  "BearbeitungTab": "TabHBohrO",
  "Bezeichnung": "",
  "FraesProg": "H_BLD_Pneu",
  "XPos": "21",
  "BemassungsArt": "1"
 },
 {
  "LfdBearbId": 2,
  "BearbeitungTab": "TabHBohrO",
  "Bezeichnung": "",
  "FraesProg": "H_BLD_Pneu",
  "XPos": "32",
  "BemassungsArt": "2"
 },
 {
  "LfdBearbId": 3,
  "BearbeitungTab": "TabHBohrO",
  "Bezeichnung": "",
  "FraesProg": "H_BLD_Pneu",
  "XPos": "64",
  "BemassungsArt": "2"
 }
]

Wenn jetzt Objekt 1 und 2 als Kopie ans Ende angehängt werden soll, ginge das mit

for (var a=0;a<2;a++) { camListe.push(camListe[a]) }

auch mit der copyWithin()-Methode ist es mir gelungen was aber für meinen Geschmack doch recht kompliziert ist.
Werte können dann mit

camListe[3]['XPos'] = 'Neuer Wert'

geändert werden (inklusive dem Quellobjekt 😟).
Alternativ kann man auch mit

camListe.splice(camListe.length,0,camListe.slice(0,2))
//oder
camListe.push(camListe.slice(0,2))

die Werte duplizieren was dann allerdings als zusätzliches Array eingebunden wird und ich dann auch nicht aufgelöst bekomme.

Kennt jemand eine Möglichkeit eine "echte" Kopie von Objekten zu erstellen?

  1. Hallo Franz,

    du hast recht, in den Einstiegsartikeln zu Objekten findet man das nicht. Ich bin aber sicher, irgendwo mal geschrieben zu haben, dass Objekte und Arrays nur Verweise auf interne Strukturen sind und die Zuweisung eines Objektes von a nach b nur den Verweis kopiert. Ich find's nur grad nicht wieder. Wie soll es da ein Einsteiger finden...

    Welche hilfreichen Stellen hast Du gefunden? Und hättest Du einen Vorschlag, wo man das anfängergerecht bündeln kann?

    Um Objekte oder Arrays echt zu kopieren, muss man sich gut überlegen, was man haben will. Es gibt flache und tiefe Kopien.

    Bei einer flachen Kopie werden die Eigenschaften aus a nach b übertragen. Falls die Eigenschaftswerte ihrerseits Objekte oder Arrays sind, wird nur der Verweis übertragen.

    Bei einer tiefen Kopie wird beim Übertragen einer Eigenschaft, deren Wert ein Array oder Objekt ist, eine tiefe Kopie dieses Wertes erstellt. Das ist ein rekursiver Vorgang, der mit etwas Pech Zeit kostet und mit viel Pech das Script abschießt (wenn das zu klonende Objekt nämlich keine einfache Baumstruktur ist, sondern ein Graph mit Rückverweisen, dann gibt's eine Endlosrekursion bis der Aufrufstapel überläuft). Eine tiefe Kopie muss also gut überlegt werden, das macht man nur, wenn es wirklich nötig ist.

    Der zweite Aspekt ist, ob man nur die aufzählbaren Eigenschaften übertragen möchte oder auch die nicht aufzählbaren Eigenschaften. Aufzählbarkeit ist etwas, das man pro Objekteigenschaft festlegen kann. Normalerweise sind Eigenschaften aufzählbar, einige aber nicht. Zum Beispiel die length-Eigenschaft eines Arrays, die ist nicht aufzählbar.

    Und drittens fragt es sich, ob man nur Eigenschaften mit einem String als Schlüssel übertragen möchte, oder auch die, deren Schlüssel ein Symbol ist.

    Eine einfache Methode, um eine flache Kopie eines Objekts zu erhalten (also eines assoziativen Arrays), bei der alle aufzählbaren Eigenschaften übertragen werden (egal ob der Schlüssel ein String oder ein Symbol ist), ist der Spread-Operator …. Den gibt's in jedem aktuellen Brauser. Im IE nicht, aber der interessiert keinen mehr.

    const s = Symbol();
    const a = { foo: 7, bar: 99, [s]: "symbolisch" };
    const b = { ... a };
    
    console.log("b.foo: ", b.foo);
    console.log("b.bar: ", b.bar);
    console.log("b.[s]: ", b.[s]);
    

    Mit Arrays ging das schon, bevor es mit Objekten ging, da verwendest Du eckige Klammern beim Spread. Es gibt nur leichte Fehler bei schwach besetzten Arrays…

    const arr1 = [ 4, 7, 9 ];
    arr1[99] = 17;
    const arr2 = [ ...arr1 ];
    
    console.log(arr1.hasOwnProperty(5));   // false
    console.log(arr2.hasOwnProperty(5));   // true
    

    Im Ergebnis haben arr1 und arr2 beide die Länge 100, aber in arr1 sind die Indizes 3 bis 98 nicht existent, während sie in arr2 existieren und den Wert undefined haben. D.h. arr2 braucht mehr Speicher. Aber im Normalfall stört das keinen 😀

    Rolf

    --
    sumpsi - posui - obstruxi
  2. @@Franz

    Ich habe einen Teil eines assoziativen Arrays an das selbige angehängt (kopiert) und wollte diverse Werte dann ändern. Die gewünschten Werte wurden auch geändert. Allerdings die der Quellelemente auch.

    Yep.

    const a = [0, 0];
    const b = a;
    b[0] = 1;
    console.log(a); // [1, 0]
    console.log(b); // [1, 0]
    
    
    const A = {foo: 0, bar: 0};
    const B = A;
    B.foo = 1;
    console.log(A); // {foo: 1, bar: 0}
    console.log(B); // {foo: 1, bar: 0}
    

    Kennt jemand eine Möglichkeit eine "echte" Kopie von Objekten zu erstellen?

    Zwei:

    const a = [0, 0];
    const b = [...a];
    b[0] = 1;
    console.log(a); // [0, 0]
    console.log(b); // [1, 0]
    
    const A = {foo: 0, bar: 0};
    const B = {...A};
    B.foo = 1;
    console.log(A); // {foo: 0, bar: 0}
    console.log(B); // {foo: 1, bar: 0}
    
    const a = [0, 0];
    const b = Array.from(a);
    b[0] = 1;
    console.log(a); // [0, 0]
    console.log(b); // [1, 0]
    
    const A = {foo: 0, bar: 0};
    const B = Object.create(A);
    B.foo = 1;
    console.log(A); // {foo: 0, bar: 0}
    console.log(B); // {foo: 1, bar: 0}
    

    🖖 Живіть довго і процвітайте

    --
    „Ukončete, prosím, výstup a nástup, dveře se zavírají.“
    1. Hallo
      an Rolf:
      Mittels Google bin ich als erstes auf den alten Artikel zu contact() auf http://www-hera-b.desy.de/subgroup/computing/IT/www/selfhtml/javascript/objekte/array.htm#concat gestoßen. Unter "Beachten Sie:" steht dann der Hinweis wie ich mir den z.B. auch im heutigen Wiki gewünscht hätte.

      Mit unter zur Hilfenahme der hiesigen Suchfunktion bin ich dann auf den Eintrag
      "Enthält das Array ein Objekt, wird nur die Objektreferenz gedoppelt. D.h. die Kopie und das Original zeigen danach auf das gleiche Objekt. Wenn man das nicht will, braucht man einen deep clone Algorithmus, der auch die referenzierten Objekte doppelt - und zwar rekursiv, denn die können ja auch wieder auf Objekte zeigen. Das ist nicht trivial - solche Verweisketten können geschlossen sein und das würde eine Endlosrekursion hervorrufen."
      unter https://forum.selfhtml.org/self/2021/oct/26/javascript-reference-types-beispiel-und-frage/1793056#m1793056 gestoßen wo allerdings auf den erwähnten "deep clone Algorithmus" nicht weiter eingegangen wurde und das für mich er böhmische Dörfer sind.
      Dann gibt es noch diese Aussage:
      "Dazu musst du eine Kopie des Arrays erzeugen. Eine einfache Zuweisung gibt nur die Referenz weiter. Beide Eigenschaften zeigen damit auf dasselbe Array. Eine einfache Kopie kann mit .slice() (ohne Parameter) angelegt werden. Damit wird das Array kopiert. Da du Strings als Inhalte hast, werden diese kopiert. Bei Objekten würden nur die Referenzen kopiert werden."
      https://forum.selfhtml.org/self/2021/oct/26/javascript-reference-types-beispiel-und-frage/1793050#m1793050

      Deine Beispiele sind recht einfacher Natur. Ich müsste einen ganzen Satz Objekt kopieren, wobei ich nur die Schlüssel, Werte und die Länge des Hautarrays (camListe) benötige. Es gibt auch nur Text bzw. Zahlen und keine Symbole.

      Was genau ist mit dem "Spread-Operator" gemeint?

      Ich werde wohl mal die Version von Gunnar Bittersmann mit Array.from(a) und Object.create(A) testen.

      Wenn das nicht funktioniert kann ich vielleicht noch analog per Doppelschleife die einzelnen Einträge abfragen und neu hinzufügen was meinen Code wohl ziemlich aufblähen wird.

      vielen Dank für die Vorschläge

      1. Hallo Franz,

        der Spread-Operator ist vergleichweise neu in JavaScript (nur 6 Jahre oder so) und besteht aus drei Punkten: ...

        Spread im Wiki. Das ist ein typischer Orlok-Artikel, der extrem ausführlich ist und viele Beispiele enthält. Er ist aber im Wesentlichen auch von 2016, wo der Spread-Operator gerade neu war und auch noch nicht richtig für Objekte spezifiziert. Aber mit denen geht's auch.

        Ich habe ein Objekt a = { foo: 2, bar: 3, baz: 6 } und wenn ich das mit dem Spread-Operator in ein Objektliteral hineinverteile, werden die vorhandenen Eigenschaften "ausgebreitet": b = { ...a }. Was auch eine Kopie ist.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Ja, dein Code auf einer Testseite programmiert funktioniert soweit. Das Array arr1 nach meinem Beispiel umstrukturiert funktioniert auch.

          Mein aktuelles Problem:
          ich extrahiere einen Teil aus arr1 was zu einem neuen Array (brr1 genannt) führt um darin befindliche Werte mittels Schleife in allen Objekten zu ändern:

          for (var a=0;a<brr1.length;a++) { brr1[a]['XPos'] = 'Neuer Wert' }
          

          Dein arr1[99] ist ja nur ein weiteres Objekt was an Position 99 hinzugefügt werden soll.
          Ich habe jetzt aber ein weiteres Array was mit ... nicht mehr hinzugefügt werden kann.

          arr1 = [...brr1]
          

          Wobei das nach einem Beispiel auf deiner verlinkten Seite gehen müsste.

          Aber ich glaube das ist was für Morgen.
          Schöne Grüße

    2. Hallo Gunnar Bittersmann,

      const B = Object.create(A);

      Du irrst. Das erzeugt ein neues Objekt in B mit A als Prototyp und keine unabhängige Kopie. Wenn, dann mit Object.assign:

      const A = { foo: 2, bar: 3 };
      const B = Object.create(A);
      const C = Object.assign({}, A);
      
      console.log(B.foo, C.foo);    // 2  2
      A.foo = 22;
      console.log(B.foo, C.foo);    // 22 2
      

      Object.assign(x,y) nimmt die Eigenschaften aus y und kopiert sie nach x. x wird dann auch zurückgegeben. Deswegen das leere Objekt als erstes Argument.

      Der Spread-Operator ist einfacher und gut unterstützt, darum habe ich Object.assign nicht erwähnt.

      Rolf

      --
      sumpsi - posui - obstruxi
  3. Lieber Franz,

    Kennt jemand eine Möglichkeit eine "echte" Kopie von Objekten zu erstellen?

    Man kann das Objekt als JSON-String serialisieren und dann wieder parsen (einlesen):

    const a = { a:0, b:1, c:2 };
    const b = JSON.parse(JSON.stringify(a));
    

    Liebe Grüße

    Felix Riesterer

    1. Lieber Felix Riesterer
      vielen vielen Dank an dich. Mit deinem Ansatz ist es mir nach vielen Fehlversuchen endlich gelungen.

      Mein Vorgehen:
      Zunächst mach ich mir die Tatsache zu Nutze, das die zu kopierenden Objekt vorher gerade erst frisch ans Array angefügt wurden. Das heißt ich kenne die Anzahl und ich weiß das diese sich am Array-Ende befinden.

      Mein Hauptarray:

      var arr1 = [
       {
        "LfdBearbId": 1,
        "BearbeitungTab": "TabHBohrO",
        "Bezeichnung": "",
        "FraesProg": "H_BLD_Pneu",
        "XPos": "21",
        "BemassungsArt": "1"
       },
       {
        "LfdBearbId": 2,
        "BearbeitungTab": "TabHBohrO",
        "Bezeichnung": "",
        "FraesProg": "H_BLD_Pneu",
        "XPos": "32",
        "BemassungsArt": "2"
       },
       {
        "LfdBearbId": 3,
        "BearbeitungTab": "TabHBohrO",
        "Bezeichnung": "",
        "FraesProg": "H_BLD_Pneu",
        "XPos": "64",
        "BemassungsArt": "2"
       }
      ];
      

      Meine Verarbeitung:

      const n = 2  // Anzahl der zu kopierenden Objekte
      const arr2 = JSON.parse(JSON.stringify(arr1.slice(-n)));
      /*
         arr1.slice(-n) extrahieren der Objekte vom Arrayende
         JSON.stringify(...) umwandeln der Objekte (die Verweise) in eine Zeichenkette mit Verlust aller Beziehungen
         const arr2 = JSON.parse(...) wieder umwandeln der Zeichenkette zu Objekten und speichern in einem neuen Array
      */
      arr1 = arr1.concat(arr2) // erweiteren des alten Arrays mit den Objekten aus dem neuen Array
      const max = arr1.length // neue Arraylänge ermitteln und in Variable speichern
      for (var a=arr1.length-n;a<max;a++) {
         arr1[a]['XPos'] = 'Neuer Wert' // Schleife durchläuft die letzten Einträge und ändert Werte
      }
      console.log(arr1)
      

      Endlich!
      Es ist schon interessant:
      Ein Browser kann heutzutage von Hause aus Bilder anzeigen, Dokumente öffnen, Audio- und Videos abspielen und programmierte Animation anzeigen.
      Und beim kopieren von ein paar Zeilen Text bricht er sich die Ohren.😉

      Danke an alle die sich meiner angenommen haben.

      Mit freundlichen Grüßen
      Franz

      1. @@Franz

        Lieber Felix Riesterer
        vielen vielen Dank an dich. Mit deinem Ansatz ist es mir nach vielen Fehlversuchen endlich gelungen.

        Ich hätte Felix’ Posting gleich den Minuspunkt geben sollen, damit du das gar nicht erst machst. Das ist IMHO kein guter Ansatz, sondern ein übler Hack.

        Was spricht denn gegen den von Rolf und mir gezeigten Spread-Operator?

        const n = 2  // Anzahl der zu kopierenden Objekte
        const arr2 = [...arr1.slice(-n)];
        

        🖖 Живіть довго і процвітайте

        --
        „Ukončete, prosím, výstup a nástup, dveře se zavírají.“
        1. Lieber Gunnar,

          Das ist IMHO kein guter Ansatz, sondern ein übler Hack.

          magst Du mir das näher erklären? Dass es hacky ist, kann man direkt sehen. Aber warum ist es sogar nicht empfehlenswert?

          Liebe Grüße

          Felix Riesterer

          1. @@Felix Riesterer

            magst Du mir das näher erklären? Dass es hacky ist, kann man direkt sehen. Aber warum ist es sogar nicht empfehlenswert?

            Weil die JavaScript-Engine mühsam aus einem Array bzw. Objekt einen String machen muss, nur um diesen gleich darauf wieder mühsam auseinanderzupflücken und in ein Array bzw. Objekt verwandeln muss.

            Mühsam heißt: Der Client – d.h. alle Clients, auf denen die Seite angezeigt wird – müssen sinnlos arbeiten. Ressourcenverschwendung.

            Warum dann nicht gleich so? 🤪

            const b = JSON.parse(JSON.stringify(a).split('').join(''));
            

            Wenn man ein Array bzw. Objekt kopieren will, sollte man genau das tun – ohne Umweg über einen String.

            🖖 Живіть довго і процвітайте

            --
            „Ukončete, prosím, výstup a nástup, dveře se zavírají.“
        2. Hallo Gunnar,

          ich hab auch einen gegeben.

          Pro Stringify+Parse:

          • Führt einen Deep Clone durch
          • Erkennt eine zirkuläre Struktur (a.b.c.d == a) und bricht dann ab

          Contra

          • Ineffizienzwunder
          • Stringify serialisiert nicht immer den richtigen Typ mit, und beim Parse bleibt dann der String stehen. Dazu muss man an parse() noch einen reviver-Callback als 2. Parameter mitgeben. Siehe Ineffizienzwunder…

          Zum Thema const arr2 = [...arr1.slice(-n)];

          Wozu der Spread? Slice erzeugt eh schon eine flache Kopie.

          Rolf

          --
          sumpsi - posui - obstruxi
          1. @@Rolf B

            Zum Thema const arr2 = [...arr1.slice(-n)];

            Wozu der Spread? Slice erzeugt eh schon eine flache Kopie.

            Ich hatte nur das JSON-Hin-und-Her umgeschrieben; in das Innere hatte ich gar nicht reingekuckt.

            🖖 Живіть довго і процвітайте

            --
            „Ukončete, prosím, výstup a nástup, dveře se zavírají.“
        3. Hallo Gunnar

          Was spricht denn gegen den von Rolf und mir gezeigten Spread-Operator?

          const n = 2  // Anzahl der zu kopierenden Objekte
          const arr2 = [...arr1.slice(-n)];
          

          Er funktioniert nicht!
          Mit "const arr2 = [...arr1.slice(-n)];" erzeugst du ein neues Array. Soweit so gut. Und wie bindest du dieses Array jetzt ins Orginal am Ende ein?
          Mit

          arr1 = arr1.concat(arr2)
          

          Geht auch!
          Dann wollen wir noch ein paar Werte verändern:

          max = arr1.length
          for (var a=arr1.length-n;a<max;a++) {
             arr1[a]['XPos'] = 'Neuer Wert'
          }
          

          Und zumindest bei mir bin ich dann wieder am Anfang, denn es werden nicht nur die letzten neuen Werte aus arr2 verändert, sondern auch die ursprünglichen aus arr1. Das heißt es ist auch wieder nur ein Verweis und keine Kopie!

          1. Hallo Franz,

            ja, es ging ja auch nicht darum, eine Kopie des slice-Ergebnisses zu erzeugen. Slice liefert eh schon eine Kopie.

            Du musst die einzelnen Objekte in der slice kopieren. Das geht aber nur einzeln. Siehe meine andere Antwort.

            Oder – äh – mit Hilfe von array.map() so:

            arr1 = arr1.concat(arr1.slice(-2).map(e=>(e={...e},e.XPos='Neuer Wert',e)));
            

            🤮

            Das .map Gedöns ist die minifizierte und uglyfizierte Variante hiervon:

            .map(function(element) {
               let copy = { ...element };
               copy.XPos = 'NeuerWert';
               return copy;
            })
            

            😉 Rolf

            --
            sumpsi - posui - obstruxi
      2. Hallo Franz,

        JSON.parse(JSON.stringify(arr1.slice(-n)));

        also, bevor Du in diesem Kaninchenloch auf ein schwarzes Kaninchen oder gar die Herzkönigin triffst…

        Folge lieber dem hier: 🕳️🐇 🏃💨

        Du kannst in einem Rutsch kopieren und anpassen. Vorausgesetzt, dass Du den neuen Wert im forEach-Callback gut bestimmen kannst.

        arr1.slice(-2).forEach(function(element) {
           const copy = {...element};  // Kopie des Elements
           copy.XPos = 'Neuer Wert';   // Fix
           arr1.push(copy);            // Ab ins Array
        });
        

        Es geht auch mit einer for...of Schleife (das ist das prozedurale Gegenstück zur forEach Funktion):

        for (let element of arr1.slice(-2)) {
           const copy = {...element};
           copy.XPos = 'Neuer Wert';
           arr1.push(copy);
        }
        

        copy.XPos? Ja, das ist ein normaler String als Key, da brauchst Du keine ["XPos"] Schreibweise. Hab ich bislang im Thread nicht drauf geachtet.

        Da deine Array-Einträge reine Key-Value Paare sind, ist die Frage nach flacher oder tiefer Kopie eh irrelevant.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Hallo Rolf,

          Du kannst in einem Rutsch kopieren und anpassen. Vorausgesetzt, dass Du den neuen Wert im forEach-Callback gut bestimmen kannst.

          arr1.slice(-2).forEach(function(element) {
             const copy = {...element};  // Kopie des Elements
             copy.XPos = 'Neuer Wert';   // Fix
             arr1.push(copy);            // Ab ins Array
          });
          

          Stimmt, damit geht es.
          Das ist eine überlegenswerte Alternative!