Jak sklonować tablicę obiektów za pomocą podkreślenia?

82
#!/usr/bin/env node
var _ = require('underscore');
var a = [{f: 1}, {f:5}, {f:10}];
var b = _.clone(a);
b[1].f = 55;
console.log(JSON.stringify(a));

To skutkuje:

[{"f":1},{"f":55},{"f":10}]

Wygląda na to, że Clone nie działa! Więc RTFM i zobacz to:

http://underscorejs.org/#clone

Utwórz płytko skopiowany klon obiektu. Wszelkie zagnieżdżone obiekty lub tablice zostaną skopiowane przez odniesienie, a nie powielone.

Więc _.clonejest całkiem bezużyteczny. Czy istnieje sposób na skopiowanie tablicy obiektów?

Pęto
źródło
4
Odmówiono żądania ściągnięcia dla głębokiej kopii: github.com/jashkenas/underscore/pull/595 Lo-Dash has cloneDeep
epascarello
5
lol Właśnie zrozumiałem grę słów na podkreśleniu. Niska kreska.
Jess,
1
underscore vs. lodash: stackoverflow.com/questions/13789618/…
Jess

Odpowiedzi:

122

Cóż, jest podstęp! Jeśli clone nie „klonuje” zagnieżdżonych obiektów, możesz to wymusić, klonując jawnie każdy obiekt wewnątrz wywołania mapy! Lubię to:

#!/usr/bin/env node
var _ = require('underscore');
var a = [{f: 1}, {f:5}, {f:10}];
var b = _.map(a, _.clone);       // <----
b[1].f = 55;
console.log(JSON.stringify(a));

Wydruki:

[{"f":1},{"f":5},{"f":10}]

Yay! ajest niezmieniony! Teraz mogę edytować bwedług własnych upodobań!

Pęto
źródło
48
Uważaj jednak. To oczywiście działa tylko na dwa poziomy. nie dla tablic lub obiektów, które są zagnieżdżone nawet bardziej niż w tym przykładzie.
Simon Zyx,
1
Zgodnie z projektem podkreślenie również nie klonuje poprawnie wartości RegExp lub Date
Mark K Cowan
1
Każdy, kto tu zagląda, powinien zobaczyć moją odpowiedź poniżej.
gdibble
65

Wyodrębniono inne rozwiązanie z problemu na Github, które działa z dowolnym poziomem zagnieżdżonych danych i nie wymaga podkreślenia:

JSON.parse(JSON.stringify(obj))
Nacho Coloma
źródło
13
Działa to, chyba że obiekt ma cykl, w którym to przypadku JSON.stringifyzgłasza błąd. Co nie ma miejsca w oryginale, ale nadal jest interesującym stanem rzeczy. a = {simple: 'thing'}; a.cycle = a ; JSON.stringify(a).
mcdave
10
Warto również zauważyć, że to rozwiązanie działa tylko dla obiektów z prostymi typami. Na przykład, jeśli masz obiekt Datelub Regexwystąpienia, zostaną one serializowane do ciągów. To nie koniec świata, ale musisz sobie z tym poradzić, jeśli używasz tego i oczekujesz Dateinstancji.
cayleyh
1
A jeśli myślisz, że ktoś może spróbować to nakarmić undefined, będziesz chciał, w JSON.parse(JSON.stringify(obj) || null)przeciwnym razie spowoduje to błąd.
Ian Mackinnon
1
Wraz z tym, o czym wspomniał @cayleyh, functioncałkowicie spadnie .
Marko Grešak
13

FWIW, lodash posiada funkcję cloneDeep :

Ta metoda jest podobna do _.clone, z tą różnicą, że rekurencyjnie klonuje wartość.

Hertzel Guinness
źródło
9

Odniesienie do podkreślonego interfejsu API :

_.toArray(list)Tworzy prawdziwą tablicę z listy (wszystko, po czym można iterować). Przydatne do transmutowania obiektu arguments.

... lub w tym przypadku klonowanie tablicy. Spróbuj tego:

var _ = require('underscore');
var array1 =  [{a:{b:{c:1}}},{b:{c:{a:2}}},{c:{a:{b:3}}}];
var array2 = _.toArray(array1);
console.log(array1 === array2); --> false
console.log(array1[0] === array2[0]); --> true

Poniżej znajduje się dodatek, który utworzyłem po komentarzu Steve'a poniżej -thx

Vanilla JS (lub używając w _.clonerazie potrzeby) rekursywnego pomocnika do głębokiego klonowania :

function clone(thing, opts) {
    var newObject = {};
    if (thing instanceof Array) {
        return thing.map(function (i) { return clone(i, opts); });
    } else if (thing instanceof Date) {
        return new Date(thing);
    } else if (thing instanceof RegExp) {
        return new RegExp(thing);
    } else if (thing instanceof Function) {
        return opts && opts.newFns ?
                   new Function('return ' + thing.toString())() :
                   thing;
    } else if (thing instanceof Object) {
        Object.keys(thing).forEach(function (key) {
            newObject[key] = clone(thing[key], opts);
        });
        return newObject;
    } else if ([ undefined, null ].indexOf(thing) > -1) {
        return thing;
    } else {
        if (thing.constructor.name === 'Symbol') {
            return Symbol(thing.toString()
                       .replace(/^Symbol\(/, '')
                       .slice(0, -1));
        }
        // return _.clone(thing);  // If you must use _ ;)
        return thing.__proto__.constructor(thing);
    }
}

var a = {
    a: undefined,
    b: null,
    c: 'a',
    d: 0,
    e: Symbol('a'),
    f: {},
    g: { a:1 },
    h: [],
    i: [ { a:2 }, { a:3 } ],
    j: [ 1, 2 ],
    k: function (a) { return a; },
    l: /[a-z]/g,
    z: [ {
        a: undefined,
        b: null,
        c: 'b',
        d: 1,
        e: Symbol(1),
        f: {},
        g: { b:2 },
        h: { c:{ c:3 } },
        i: { a:Symbol('b') },
        j: { a:undefined, b:null },
        k: [],
        l: [ 1, [ 1, 2 ], [ [ 1, 2, 3 ] ] ],
        m: function (a) { return !a; },
        n: { a:function (a) { return !!a; } },
        o: /(a|b)/i
       } ]
};
var b = clone(a);
var c = clone(a, { newFns:true });


/* Results - value beneath each for reference:

a.a === b.a --> true
undefined

a.b === b.b --> true
null

a.c === b.c --> true
'a'

a.d === b.d --> true
0

a.e === b.e --> false
Symbol(a)

a.f === b.f --> false
{}

a.g === b.g --> false
{ a:1 }

a.h === b.h --> false
[]

a.i === b.i --> false
[ { a:2 }, { a:3 } ]

a.i[0] === b.i[0] --> false
{ a:2 }

a.i[0].a === b.i[0].a --> true
2

a.j === b.j --> false
[ 1, 2 ]

a.k === b.k --> true
a.k === c.k --> false
function (a) { return a; }

a.l === b.l --> false
/[a-z]/g

a.z === b.z --> false
[Object]

a.z[0].a === b.z[0].a --> true
undefined

a.z[0].b === b.z[0].b --> true
null

a.z[0].c === b.z[0].c --> true
'b'

a.z[0].d === b.z[0].d --> true
1

a.z[0].e === b.z[0].e --> 
false
Symbol(1)

a.z[0].f === b.z[0].f --> false
{}

a.z[0].g === b.z[0].g -- > false
{ b:2 }

a.z[0].g.b === b.z[0].g.b --> true
2

a.z[0].h === b.z[0].h --> false
{ c:{ c:3 } }

a.z[0].h.c === b.z[0].h.c --> false
{ c:3 }

a.z[0].h.c.c === b.z[0].h.c.c --> true
3

a.z[0].i === b.z[0].i --> false
{ a:Symbol(b) }

a.z[0].i.a === b.z[0].i.a --> false
Symbol(b)

a.z[0].j === b.z[0].j --> false
{ a:undefined, b:null }

a.z[0].j.a === b.z[0].j.a --> true
undefined

a.z[0].k === b.z[0].k --> false
[]

a.z[0].l === b.z[0].l --> false
[ 1, [ 1, 2 ], [ [ 1, 2, 3 ] ] ]

a.z[0].l[1] === b.z[0].l[1] --> false
[ 1, 2 ]

a.z[0].l[1][1] === b.z[0].l[1][1] --> true
2

a.z[0].m === b.z[0].m --> true
a.z[0].m === c.z[0].m --> false
function (a) { return !a; }

a.z[0].n === b.z[0].n --> false
{ a:function (a) { return !!a; } }

a.z[0].n.a === b.z[0].n.a --> true
a.z[0].n.a === c.z[0].n.a --> false
function (a) { return !!a; }

a.z[0].o === b.z[0].o --> false
/(a|b)/i

*/
gdibble
źródło
To najlepsza odpowiedź.
Pierre
_.toArray(list)nie klonuje obiektów w tablicy. var array1 = [{a: 1}, {a: 2}, {a: 3}]; var array2 = _.toArray(array1); array2[0].a = 999; console.log(array1[0]); --> {a: 999}
Steve Lang
@SteveLang dzięki za wskazanie tego. Ups. Dlatego poświęciłem trochę czasu na zrobienie waniliowego JS fn powyżej, którego jeśli użytkownik naprawdę musi użyć _.clonew elsewarunku;)
gdibble