Hoe kloon ik een JavaScript-object correct?

Ik heb een object x. Ik wil het graag kopiëren als object y, zodat wijzigingen in yxniet wijzigen. Ik realiseerde me dat het kopiëren van objecten die zijn afgeleid van ingebouwde JavaScript-objecten, zal resulteren in extra, ongewenste eigenschappen. Dit is geen probleem, aangezien ik een van mijn eigen letterlijk geconstrueerde objecten kopieer.

Hoe kloon ik een JavaScript-object correct?


Antwoord 1, autoriteit 100%

Dit doen voor elk object in JavaScript is niet eenvoudig of duidelijk. U zult het probleem tegenkomen dat u ten onrechte attributen van het prototype van het object oppikt die in het prototype moeten worden achtergelaten en niet naar de nieuwe instantie moeten worden gekopieerd. Als u bijvoorbeeld een clone-methode toevoegt aan Object.prototype, zoals sommige antwoorden laten zien, moet u dat kenmerk expliciet overslaan. Maar wat als er andere aanvullende methoden zijn toegevoegd aan Object.prototype, of andere tussenprototypen waarvan u niets weet? In dat geval kopieert u kenmerken die u niet zou moeten kopiëren, dus u moet onvoorziene, niet-lokale kenmerken detecteren met de hasOwnPropertymethode.

Naast niet-opsombare attributen, zul je een moeilijker probleem tegenkomen wanneer je objecten probeert te kopiëren die verborgen eigenschappen hebben. prototypeis bijvoorbeeld een verborgen eigenschap van een functie. Ook wordt naar het prototype van een object verwezen met het attribuut __proto__, dat ook verborgen is, en niet gekopieerd zal worden door een for/in-lus die de attributen van het bronobject herhaalt. Ik denk dat __proto__specifiek is voor Firefox’ JavaScript-interpreter en dat het in andere browsers iets anders kan zijn, maar je snapt het wel. Niet alles is op te sommen. Je kunt een verborgen kenmerk kopiëren als je de naam weet, maar ik weet geen manier om het automatisch te ontdekken.

Nog een ander probleem in de zoektocht naar een elegante oplossing is het probleem van het correct instellen van de prototype-overerving. Als het prototype van uw bronobject Objectis, werkt het eenvoudig om een nieuw algemeen object te maken met {}, maar als het prototype van de bron een afstammeling is van Object, dan mis je de extra leden van dat prototype die je hebt overgeslagen met het hasOwnProperty-filter, of die in het prototype zaten, maar die in de eerste plaats niet op te noemen waren. Een oplossing zou kunnen zijn om de eigenschap constructorvan het bronobject aan te roepen om het oorspronkelijke kopieerobject te krijgen en dan over de attributen te kopiëren, maar dan krijg je nog steeds geen niet-opsombare attributen. Bijvoorbeeld een Dateobject slaat zijn gegevens op als een verborgen lid:

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);

De datumreeks voor d1ligt 5 seconden achter die van d2. Een manier om de ene Datehetzelfde te maken als de andere, is door de methode setTimeaan te roepen, maar dat is specifiek voor de klasse Date. Ik denk niet dat er een kogelvrije algemene oplossing voor dit probleem is, hoewel ik graag ongelijk zou hebben!

Toen ik algemene deep copying moest implementeren, kwam ik uiteindelijk tot compromissen door aan te nemen dat ik alleen een gewoon Object, Array, Date, String, Numberof Boolean. De laatste 3 typen zijn onveranderlijk, dus ik zou een oppervlakkige kopie kunnen maken zonder me zorgen te maken dat het verandert. Ik nam verder aan dat alle elementen in Objectof Arrayook een van de 6 eenvoudige typen in die lijst zouden zijn. Dit kan worden bereikt met de volgende code:

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.");
}

De bovenstaande functie werkt adequaat voor de 6 eenvoudige typen die ik noemde, zolang de gegevens in de objecten en arrays een boomstructuur vormen. Dat wil zeggen, er is niet meer dan één verwijzing naar dezelfde gegevens in het object. Bijvoorbeeld:

// 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;

Het kan geen JavaScript-object aan, maar het kan voor veel doeleinden voldoende zijn, zolang je er niet vanuit gaat dat het gewoon werkt voor alles wat je erop gooit.


Antwoord 2, autoriteit 64%

Als u geen Dates, functies, undefined, regExp of Infinity gebruikt in uw object, is een zeer eenvoudige one-liner JSON.parse(JSON.stringify(object)):

const a = {
  string: 'string',
  number: 123,
  bool: false,
  nul: null,
  date: new Date(),  // stringified
  undef: undefined,  // lost
  inf: Infinity,  // forced to 'null'
}
console.log(a);
console.log(typeof a.date);  // Date object
const clone = JSON.parse(JSON.stringify(a));
console.log(clone);
console.log(typeof clone.date);  // result of .toISOString()

3, Autoriteit 48%

Met jQuery, kun je ondiepe kopie met verlengen :

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

Latere wijzigingen in de copiedObjectheeft geen invloed op de originalObject, en vice versa.

of om een ​​diepe kopie te maken:

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

4, Autoriteit 46%

In Ecmascript 6 is er object.Assign methode, die waarden van alle traaglijke eigen eigenschappen van het ene object naar de andere kopieën kopieert. Bijvoorbeeld:

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

Maar wees op de hoogte Dit is een ondiepe kopie – geneste objecten worden nog steeds gekopieerd als referentie.


5, Autoriteit 17%

per MDN :

  • Als u ondiepe kopie wilt, gebruikt u Object.assign({}, a)
  • Voor “Diep” Kopiëren, gebruik JSON.parse(JSON.stringify(a))

Er zijn geen externe bibliotheken nodig, maar u moet browsercompatibiliteit eerst.


Antwoord 6, autoriteit 8%

Er zijn veel antwoorden, maar geen enkele noemt Object.createvan ECMAScript 5, dat weliswaar geen exacte kopie geeft, maar de bron instelt als het prototype van het nieuwe object.

Dit is dus geen exact antwoord op de vraag, maar het is een éénregelige oplossing en dus elegant. En het werkt het beste voor 2 gevallen:

  1. Waar zo’n erfenis nuttig is (duh!)
  2. Waar het bronobject niet wordt gewijzigd, waardoor de relatie tussen de 2 objecten geen probleem is.

Voorbeeld:

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

Waarom beschouw ik deze oplossing als superieur? Het is native, dus geen looping, geen recursie. Oudere browsers hebben echter een polyfill nodig.


Antwoord 7, autoriteit 8%

Een elegante manier om een Javascript-object in één regel code te klonen

Een Object.assign-methode maakt deel uit van de ECMAScript 2015 (ES6)-standaard en doet precies wat je nodig hebt.

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

De methode Object.assign() wordt gebruikt om de waarden van alle opsombare eigen eigenschappen van een of meer bronobjecten naar een doelobject te kopiëren.

Meer lezen…

De polyfillom oudere browsers te ondersteunen:

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;
    }
  });
}

Antwoord 8, autoriteit 5%

Als je geen probleem hebt met een oppervlakkige kopie, heeft de underscore.js-bibliotheek een kloon-methode.

y = _.clone(x);

of je kunt het verlengen zoals

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

Antwoord 9, autoriteit 4%

OK,stel je voor dat je dit object hieronder hebt en je wilt het klonen:

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

of

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

het antwoord hangt voornamelijk af van welk ECMAscriptu gebruikt, in ES6+kunt u eenvoudig Object.assigngebruiken om de kloon te doen:

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

of gebruik de spread-operator als volgt:

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

Maar als je ES5gebruikt, kun je een paar methoden gebruiken, maar de JSON.stringify, zorg er gewoon voor dat je niet een groot stuk gegevens gebruikt om te kopiëren, maar het kan in veel gevallen een handige manier zijn, zoiets als dit:

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

Antwoord 10, autoriteit 3%

Een bijzonder onelegante oplossing is om JSON-codering te gebruiken om diepe kopieën te maken van objecten die geen lidmethoden hebben. De methode is om uw doelobject met JSON te coderen en door het vervolgens te decoderen, krijgt u de kopie die u zoekt. Je kunt zo vaak decoderen als je wilt en zoveel kopieën maken als je nodig hebt.

Functies horen natuurlijk niet thuis in JSON, dus dit werkt alleen voor objecten zonder lidmethoden.

Deze methode was perfect voor mijn gebruik, aangezien ik JSON-blobs opsla in een sleutelwaarde-archief, en wanneer ze worden weergegeven als objecten in een JavaScript-API, bevat elk object in feite een kopie van de oorspronkelijke staat van de object zodat we de delta kunnen berekenen nadat de beller het blootgestelde object heeft gemuteerd.

var object1 = {key:"value"};
var object2 = object1;
object2 = JSON.stringify(object1);
object2 = JSON.parse(object2);
object2.key = "a change";
console.log(object1);// returns value

Antwoord 11, autoriteit 3%

Update 06 juli 2020

Er zijn drie (3) manieren om objecten in JavaScript te klonen. Aangezien objecten in JavaScript referentiewaarden zijn, kunt u niet zomaar kopiëren met de =.

De manieren zijn:

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' }

Ik hoop dat dit kan worden gebruikt als referentieoverzicht.


Antwoord 12, autoriteit 2%

Je kunt eenvoudig een spread-eigenschapgebruiken om kopieer een object zonder verwijzingen. Maar wees voorzichtig (zie opmerkingen), de ‘kopie’ bevindt zich net op het laagste object/array-niveau. Geneste eigenschappen zijn nog steeds referenties!


Volledige kloon:

let x = {a: 'value1'}
let x2 = {...x}
// => mutate without references:
x2.a = 'value2'
console.log(x.a)    // => 'value1'

Kloon met referenties op het tweede niveau:

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 biedt eigenlijk geen native ondersteuning voor diepe klonen. Gebruik een nutsfunctie. Bijvoorbeeld Ramda:

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


Antwoord 13, autoriteit 2%

Voor degenen die AngularJS gebruiken, is er ook een directe methode voor het klonen of uitbreiden van de objecten in deze bibliotheek.

var destination = angular.copy(source);

of

angular.copy(source, destination);

Meer in angular.copy documentatie


Antwoord 14, autoriteit 2%

Uit dit artikel: Hoe arrays en objecten in Javascript te kopiërendoor 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;
};

Antwoord 15, autoriteit 2%

A.Levy’s antwoord is bijna compleet, hier is mijn kleine bijdrage: er is een manier om recursieve verwijzingen te verwerken, zie deze regel

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

Als het object een XML DOM-element is, moeten we in plaats daarvan cloneNodegebruiken

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

Geïnspireerd door de uitgebreide studie van A.Levy en de prototypingbenadering van Calvin, bied ik deze oplossing:

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;
}

Zie ook de opmerking van Andy Burke in de antwoorden.


Antwoord 16

Hier is een functie die u kunt gebruiken.

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;
}

Antwoord 17

In ECMAScript 2018

let objClone = { ...obj };

Houd er rekening mee dat geneste objectennog steeds als referentie worden gekopieerd.


Antwoord 18

In ES-6 kunt u eenvoudig Object.assign(…) gebruiken.
Bijv.:

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

Een goede referentie is hier:
https://googlechrome.github.io/samples/object-assign-es6/


Antwoord 19

Prestaties

Vandaag 2020.04.30 voer ik tests uit van gekozen oplossingen op Chrome v81.0, Safari v13.1 en Firefox v75.0 op MacOs High Sierra v10.13.6.

Ik focus op snelheid van kopie-gegevens (object met eenvoudige typebelden, geen methoden enz.). De oplossingen A-i kan alleen ondiepe kopie maken, oplossingen J-U kunt diepe kopie maken.

Resultaten voor ondiepe kopie

  • Oplossing {...obj}(a) is snelst op chroom en firefox en medium snel op safari
  • Oplossing op basis van Object.assign(B) is snel op alle browsers
  • JQuery (E) en Lodash (F, G, H) Oplossingen zijn gemiddeld / vrij snel
  • Oplossing JSON.parse/stringify(k) is vrij traag
  • Solutions D en u zijn traag in alle browsers

Resultaten voor diepe kopie

  • Oplossing Q is snelst op alle browsers
  • JQuery (L) en Lodash (J) zijn medium snel
  • Oplossing JSON.parse/stringify(k) is vrij traag
  • Oplossing U is langzaamst op alle browsers
  • Lodash (J) en oplossing U crasht op chroom voor 1000 niveau diep object

Details

Voor het kiezen van oplossingen:
a
b
C (mijn)
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u ,
Ik voer 4 tests uit

  • Ondiepe-Small: Object met 10 niet-geneste velden – U kunt het uitvoeren hier
  • Ondiepe-Big: Object met 1000 niet-geneste velden – U kunt het uitvoeren hier
  • Diep-klein: object met 10 niveaus-geneste velden – U kunt het uitvoeren hier
  • Diep-Big: Object met 1000 niveaus-geneste velden – U kunt het uitvoeren hier

Objecten die in tests worden gebruikt, worden weergegeven in onderstaande fragment


20

Gebruik Lodash:

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

21

Geïnteresseerd in het klonen van eenvoudige objecten:

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

Bron: Hoe JavaScript-object naar nieuwe variabele niet door verwijzing kopiëren?


22

Je kunt een object klonen en elke verwijzing van het vorige verwijderen met een enkele regel code. Gewoon doen:

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'}

Voor browsers/engines die Object.create momenteel niet ondersteunen, kunt u deze polyfill gebruiken:

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

Antwoord 23

Nieuw antwoord op een oude vraag! Als u ECMAScript 2016 (ES6) gebruikt met Verspreid syntaxis, het is gemakkelijk.

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

Dit biedt een schone methode voor een ondiepe kopie van een object. Het maken van een diepe kopie, dat wil zeggen een nieuwe kopie maken van elke waarde in elk recursief genest object, vereist een van de zwaardere oplossingen hierboven.

JavaScript blijft evolueren.


Antwoord 24

let clone = Object.assign( Object.create( Object.getPrototypeOf(obj)), obj)

ES6-oplossing als u (ondiep) een klasse-instantiewilt klonen en niet alleen een eigenschapsobject.


Antwoord 25

Voor een diepe kopie en kloon, JSON.stringify en vervolgens JSON.parse het object:

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}}

Antwoord 26

Ik denk dat er een eenvoudig en werkend antwoord is. Bij diep kopiëren zijn er twee problemen:

  1. Houd eigenschappen onafhankelijk van elkaar.
  2. En houd de methoden levend op het gekloonde object.

Dus ik denk dat een eenvoudige oplossing zal zijn om eerst te serialiseren en deserialiseren en er vervolgens een toewijzing aan te doen om ook functies te kopiëren.

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

Hoewel deze vraag veel antwoorden heeft, hoop ik dat deze ook helpt.


Antwoord 27

Gebruik lodash _.cloneDeep().

Ondiepe kopie: lodash _.clone()

Een oppervlakkige kopie kan worden gemaakt door simpelweg de referentie te kopiëren.

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}}}

Deep Copy: lodash _.cloneDeep()

velden worden gederefereerd: in plaats van verwijzingen naar objecten die worden gekopieerd

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}}}


Antwoord 28

(Het volgende was voornamelijk een integratie van @Maciej Bukowski, @A. Levy, @Jan Turoň, @Redu‘s antwoorden en @LeviRoberts, @RobG‘s opmerkingen, veel dank aan hen!! !)

Diepe kopie? – JA! (meestal);
Ondiepe kopie? – NEE! (behalve Proxy).

Ik heet iedereen van harte welkom om clone()te testen.
Bovendien is defineProp()ontworpen om gemakkelijk en snel elk type descriptor te (her)definiëren of kopiëren.

Functie

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
  })
}

// Testen

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)
)

Referenties

  1. Object.create()| MDN
  2. Object.defineProperties()| MDN
  3. Opsomming en eigendom van eigenschappen | MDN
  4. TypeError: cyclische objectwaarde | MDN

Taaltrucs gebruikt

  1. Voorwaardelijk prop toevoegen aan object

Antwoord 29

Dit is een aanpassing van de code van A. Levy om ook het klonen van functies en meervoudige/cyclische verwijzingen af te handelen – wat dit betekent is dat als twee eigenschappen in de boom die wordt gekloond verwijzingen zijn van hetzelfde object, de gekloonde objectboom zullen deze eigenschappen verwijzen naar één en dezelfde kloon van het object waarnaar wordt verwezen. Dit lost ook het geval op van cyclische afhankelijkheden die, als ze niet worden behandeld, tot een oneindige lus leiden. De complexiteit van het algoritme is O(n)

function clone(obj){
    var clonedObjectsArray = [];
    var originalObjectsArray = []; //used to remove the unique ids when finished
    var next_objid = 0;
    function objectId(obj) {
        if (obj == null) return null;
        if (obj.__obj_id == undefined){
            obj.__obj_id = next_objid++;
            originalObjectsArray[obj.__obj_id] = obj;
        }
        return obj.__obj_id;
    }
    function cloneRecursive(obj) {
        if (null == obj || typeof obj == "string" || typeof obj == "number" || typeof obj == "boolean") 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; i < obj.length; ++i) {
                copy[i] = cloneRecursive(obj[i]);
            }
            return copy;
        }
        // Handle Object
        if (obj instanceof Object) {
            if (clonedObjectsArray[objectId(obj)] != undefined)
                return clonedObjectsArray[objectId(obj)];
            var copy;
            if (obj instanceof Function)//Handle Function
                copy = function(){return obj.apply(this, arguments);};
            else
                copy = {};
            clonedObjectsArray[objectId(obj)] = copy;
            for (var attr in obj)
                if (attr != "__obj_id" && obj.hasOwnProperty(attr))
                    copy[attr] = cloneRecursive(obj[attr]);                 
            return copy;
        }       
        throw new Error("Unable to copy obj! Its type isn't supported.");
    }
    var cloneObj = cloneRecursive(obj);
    //remove the unique ids
    for (var i = 0; i < originalObjectsArray.length; i++)
    {
        delete originalObjectsArray[i].__obj_id;
    };
    return cloneObj;
}

Enkele snelle tests

var auxobj = {
    prop1 : "prop1 aux val", 
    prop2 : ["prop2 item1", "prop2 item2"]
    };
var obj = new Object();
obj.prop1 = "prop1_value";
obj.prop2 = [auxobj, auxobj, "some extra val", undefined];
obj.nr = 3465;
obj.bool = true;
obj.f1 = function (){
    this.prop1 = "prop1 val changed by f1";
};
objclone = clone(obj);
//some tests i've made
console.log("test number, boolean and string cloning: " + (objclone.prop1 == obj.prop1 && objclone.nr == obj.nr && objclone.bool == obj.bool));
objclone.f1();
console.log("test function cloning 1: " + (objclone.prop1 == 'prop1 val changed by f1'));
objclone.f1.prop = 'some prop';
console.log("test function cloning 2: " + (obj.f1.prop == undefined));
objclone.prop2[0].prop1 = "prop1 aux val NEW";
console.log("test multiple references cloning 1: " + (objclone.prop2[1].prop1 == objclone.prop2[0].prop1));
console.log("test multiple references cloning 2: " + (objclone.prop2[1].prop1 != obj.prop2[0].prop1));

Other episodes