Jak scalić głęboko zamiast scalenia płytkiego?


339

Zarówno Object.assign, jak i Object spread powodują tylko płytkie scalanie.

Przykład problemu:

// No object nesting
const x = { a: 1 }
const y = { b: 1 }
const z = { ...x, ...y } // { a: 1, b: 1 }

Dane wyjściowe są zgodne z oczekiwaniami. Jeśli jednak spróbuję:

// Object nesting
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = { ...x, ...y } // { a: { b: 1 } }

Zamiast

{ a: { a: 1, b: 1 } }

dostajesz

{ a: { b: 1 } }

x jest całkowicie nadpisane, ponieważ składnia spreadu przechodzi tylko o jeden poziom w głąb. Tak samo jest z Object.assign().

Czy jest na to sposób?


czy głębokie scalanie jest takie samo jak kopiowanie właściwości z jednego obiektu do drugiego?

2
Nie, ponieważ właściwości obiektu nie powinny być nadpisywane, raczej każdy obiekt potomny powinien zostać scalony z tym samym dzieckiem w celu, jeśli już istnieje.
Mike

ES6 jest sfinalizowany i nowe funkcje nie są już dodawane, AFAIK.
kangax


1
@Oriol wymaga jQuery ...
m0meni

Odpowiedzi:


331

Czy ktoś wie, czy istnieje głębokie scalanie w specyfikacji ES6 / ES7?

Nie.


21
Przejrzyj historię edycji. W chwili, gdy odpowiedziałem na to pytanie, pytanie brzmiało: Czy ktoś wie, czy głębokie scalanie istnieje w specyfikacji ES6 / ES7? .

37
Ta odpowiedź nie ma już zastosowania do tego pytania - należy je zaktualizować lub usunąć
DonVaughn

13
Pytanie nie powinno być edytowane do tego stopnia. Zmiany mają na celu wyjaśnienie. Nowe pytanie powinno zostać opublikowane.
CJ Thompson,

171

Wiem, że to trochę stary problem, ale najłatwiejsze rozwiązanie w ES2015 / ES6, jakie mogłem wymyślić, było w rzeczywistości dość proste, używając Object.assign (),

Mam nadzieję, że to pomaga:

/**
 * Simple object check.
 * @param item
 * @returns {boolean}
 */
export function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item));
}

/**
 * Deep merge two objects.
 * @param target
 * @param ...sources
 */
export function mergeDeep(target, ...sources) {
  if (!sources.length) return target;
  const source = sources.shift();

  if (isObject(target) && isObject(source)) {
    for (const key in source) {
      if (isObject(source[key])) {
        if (!target[key]) Object.assign(target, { [key]: {} });
        mergeDeep(target[key], source[key]);
      } else {
        Object.assign(target, { [key]: source[key] });
      }
    }
  }

  return mergeDeep(target, ...sources);
}

Przykładowe użycie:

mergeDeep(this, { a: { b: { c: 123 } } });
// or
const merged = mergeDeep({a: 1}, { b : { c: { d: { e: 12345}}}});  
console.dir(merged); // { a: 1, b: { c: { d: [Object] } } }

Niezmienna wersja tego znajduje się w odpowiedzi poniżej.

Zauważ, że doprowadzi to do nieskończonej rekurencji w odniesieniu do okólników. Istnieje kilka świetnych odpowiedzi na temat wykrywania okrągłych odniesień, jeśli uważasz, że napotkasz ten problem.


1
jeśli wykres obiektu zawiera cykle, które doprowadzą do nieskończonej rekurencji
the8472 20.01.2016

item !== nullnie powinno być potrzebne w środku isObject, ponieważ itemjest już sprawdzane pod kątem prawdziwości na początku warunku
mcont

2
Po co pisać: Object.assign(target, { [key]: {} })jeśli mogłoby tak być target[key] = {}?
Jürg Lehni

1
... a target[key] = source[key]zamiastObject.assign(target, { [key]: source[key] });
Jürg Lehni

3
To nie obsługuje żadnych nie zwykłych obiektów w target. Na przykład mergeDeep({a: 3}, {a: {b: 4}})spowoduje powiększenie Numberobiektu, co oczywiście nie jest pożądane. Ponadto isObjectnie akceptuje tablic, ale akceptuje każdy inny rodzimy typ obiektu, taki jak np. Date, Który nie powinien być głęboko kopiowany.
riv

122

Możesz użyć scalania Lodash :

var object = {
  'a': [{ 'b': 2 }, { 'd': 4 }]
};

var other = {
  'a': [{ 'c': 3 }, { 'e': 5 }]
};

_.merge(object, other);
// => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }

6
Cześć ludzie, to jest najprostsze i najpiękniejsze rozwiązanie. Lodash jest niesamowity, powinien zawierać go jako główny obiekt js
Nurbol Alpysbayev

11
Czy nie powinien to być wynik { 'a': [{ 'b': 2 }, { 'c': 3 }, { 'd': 4 }, { 'e': 5 }] }?
J. Hesters

Dobre pytanie. To może być osobne pytanie lub jedno dla opiekunów Lodash.
AndrewHenderson

7
Wynik { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }jest prawidłowy, ponieważ łączymy elementy tablicy. Element 0of object.aIs {b: 2}element 0o other.ato {c: 3}. Gdy te dwa są scalone, ponieważ mają ten sam indeks tablicy, wynikiem jest { 'b': 2, 'c': 3 }, który jest elementem 0w nowym obiekcie.
Alexandru Furculita,

Wolę ten , jest 6 razy mniejszy gzip.
Solo

101

Problem nie jest trywialny, jeśli chodzi o obiekty hosta lub dowolny obiekt bardziej złożony niż worek wartości

  • czy wywołujesz moduł pobierający, aby uzyskać wartość, czy kopiujesz deskryptor właściwości?
  • co jeśli cel scalania ma obiekt ustawiający (własność własną lub w łańcuchu prototypów)? Czy uważasz, że wartość jest już obecna, czy też wywołujesz settera, aby zaktualizować bieżącą wartość?
  • wywołujesz funkcje własności lub kopiujesz je? Co jeśli są funkcjami powiązanymi lub funkcjami strzałek w zależności od czegoś w swoim łańcuchu zasięgu w momencie ich definiowania?
  • co jeśli jest to coś w rodzaju węzła DOM? Na pewno nie chcesz traktować go jako prostego obiektu i po prostu głęboko scal wszystkie swoje właściwości
  • jak radzić sobie z „prostymi” strukturami, takimi jak tablice, mapy lub zestawy? Uważasz, że już są obecne, czy też je łączysz?
  • jak radzić sobie z niepoliczalnymi własnymi nieruchomościami?
  • co z nowymi poddrzewami? Po prostu przypisać przez odniesienie lub głęboki klon?
  • jak radzić sobie z przedmiotami zamrożonymi / zapieczętowanymi / nierozciągliwymi?

Kolejna rzecz, o której należy pamiętać: wykresy obiektów zawierające cykle. Zwykle nie jest trudno sobie z tym poradzić - po prostu przechowywać Setjuż odwiedzone obiekty źródłowe - ale często zapominane.

Prawdopodobnie powinieneś napisać funkcję głębokiego scalania, która oczekuje tylko prymitywnych wartości i prostych obiektów - co najwyżej tych typów, które może obsłużyć algorytm ustrukturyzowanego klonowania - jako źródeł scalania. Rzuć, jeśli napotka coś, czego nie może obsłużyć lub po prostu przypisać przez odniesienie zamiast głębokiego scalania.

Innymi słowy, nie ma jednego uniwersalnego algorytmu, musisz albo rzucić własny, albo poszukać metody bibliotecznej, która pokrywa twoje przypadki użycia.


2
wymówki dla deweloperów V8, aby nie wdrożyć bezpiecznego transferu „stanu dokumentu”
neaumusic

Podnosisz wiele dobrych problemów i chciałbym zobaczyć wdrożenie twojego zalecenia. Więc próbowałem zrobić jeden poniżej. Czy możesz spojrzeć i skomentować? stackoverflow.com/a/48579540/8122487
RaphaMex

66

Oto niezmienna (nie modyfikuje danych wejściowych) wersja odpowiedzi @ Salakar. Przydatne, jeśli wykonujesz czynności typu programowania funkcjonalnego.

export function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item));
}

export default function mergeDeep(target, source) {
  let output = Object.assign({}, target);
  if (isObject(target) && isObject(source)) {
    Object.keys(source).forEach(key => {
      if (isObject(source[key])) {
        if (!(key in target))
          Object.assign(output, { [key]: source[key] });
        else
          output[key] = mergeDeep(target[key], source[key]);
      } else {
        Object.assign(output, { [key]: source[key] });
      }
    });
  }
  return output;
}

1
@torazaburo zobacz poprzedni post przeze mnie dla funkcji
isObject

zaktualizowałem to. po kilku testach znalazłem błąd w głęboko zagnieżdżonych obiektach
CpILL

3
Jest to obliczona nazwa właściwości, pierwsza użyje wartości keyjako nazwa właściwości, później zmieni „klucz” na nazwę właściwości. Patrz: es6-features.org/#ComputedPropertyNames
CpILL

2
w isObjectnie trzeba by sprawdzić && item !== nullna końcu, bo zaczyna linii z item &&, nie?
efemeryczny

2
Jeśli źródło zagnieżdżało obiekty potomne głębiej niż cel, obiekty te nadal będą odwoływać się do tych samych wartości w mergedDeepdanych wyjściowych (myślę). Np. const target = { a: 1 }; const source = { b: { c: 2 } }; const merged = mergeDeep(target, source); merged.b.c; // 2 source.b.c = 3; merged.b.c; // 3 Czy to problem? Nie mutuje danych wejściowych, ale wszelkie przyszłe mutacje na wejściach mogą mutować dane wyjściowe i odwrotnie w / mutacje na wyjściowe dane mutacji. Jednak za to, co jest warte, ramda zachowuje się R.merge()tak samo.
James Conkling,

40

Ponieważ ten problem jest nadal aktywny, oto inne podejście:

  • ES6 / 2015
  • Niezmienny (nie modyfikuje oryginalnych obiektów)
  • Obsługuje tablice (łączy je)

/**
* Performs a deep merge of objects and returns new object. Does not modify
* objects (immutable) and merges arrays via concatenation.
*
* @param {...object} objects - Objects to merge
* @returns {object} New object with merged key/values
*/
function mergeDeep(...objects) {
  const isObject = obj => obj && typeof obj === 'object';
  
  return objects.reduce((prev, obj) => {
    Object.keys(obj).forEach(key => {
      const pVal = prev[key];
      const oVal = obj[key];
      
      if (Array.isArray(pVal) && Array.isArray(oVal)) {
        prev[key] = pVal.concat(...oVal);
      }
      else if (isObject(pVal) && isObject(oVal)) {
        prev[key] = mergeDeep(pVal, oVal);
      }
      else {
        prev[key] = oVal;
      }
    });
    
    return prev;
  }, {});
}

// Test objects
const obj1 = {
  a: 1,
  b: 1, 
  c: { x: 1, y: 1 },
  d: [ 1, 1 ]
}
const obj2 = {
  b: 2, 
  c: { y: 2, z: 2 },
  d: [ 2, 2 ],
  e: 2
}
const obj3 = mergeDeep(obj1, obj2);

// Out
console.log(obj3);


To jest miłe. Jednak gdy mamy tablicę z powtarzanymi elementami, są one łączone (są powtarzane elementy). Dostosowałem to do parametru (tablice unikalne: prawda / fałsz).
Astronauta

1
Aby tablice były unikalne, możesz zmienić prev[key] = pVal.concat(...oVal);naprev[key] = [...pVal, ...oVal].filter((element, index, array) => array.indexOf(element) === index);
Richard Herries,

1
Tak miło i czysto !! Zdecydowanie najlepsza odpowiedź tutaj!
538ROMEO,

Wspaniały. Ten pokazuje również, że tablice się łączą, i tego właśnie szukałem.
Tschallacka

Tak, uważa się, że rozwiązanie @CplLL jest niezmienne, ale używa faktycznej zmienności obiektu wewnątrz funkcji, podczas gdy reduce nie używa .
Augustin Riedinger

30

Wiem, że jest już wiele odpowiedzi i jak wiele komentarzy twierdzi, że nie zadziałają. Jedyny konsensus jest taki, że jest tak skomplikowany, że nikt nie ustanowił dla niego standardu . Jednak większość zaakceptowanych odpowiedzi w SO ujawnia „proste sztuczki”, które są szeroko stosowane. Tak więc, dla wszystkich takich jak ja, którzy nie są ekspertami, ale chcą pisać bezpieczniejszy kod, chwytając nieco więcej o złożoności javascript, postaram się rzucić nieco światła.

Zanim ubrudzimy sobie ręce, wyjaśnię 2 punkty:

  • [OŚWIADCZENIE] Poniżej proponuję funkcję, która zajmuje się tym, jak głęboko zapętlamy się w obiektach javascript do kopiowania i ilustruje to, co na ogół jest zbyt krótko komentowane. Nie jest gotowy do produkcji. Dla jasności celowo odłożyłem na bok inne rozważania, takie jak okrągłe obiekty (śledzenie według zestawu lub nieprzekonująca właściwość symbolu) , kopiowanie wartości referencyjnej lub głębokiego klonowania , niezmienny obiekt docelowy (głęboki klon?), Studium przypadku każdy typ obiektów , pobierz / ustaw właściwości za pomocą akcesoriów ... Nie testowałem też wydajności - choć jest to ważne - ponieważ tutaj też nie o to chodzi.
  • Użyję kopiowania lub przypisywania warunków zamiast scalania . Ponieważ moim zdaniem połączenie jest konserwatywne i powinno zawieść w przypadku konfliktów. W tym przypadku chcemy, aby źródło zastąpiło miejsce docelowe. Tak jak Object.assignrobi.

Odpowiedzi z for..inlub Object.keyssą mylące

Wykonywanie głębokiej kopii wydaje się tak podstawową i powszechną praktyką, że spodziewamy się, że znajdziemy jedną linijkę lub przynajmniej szybką wygraną poprzez prostą rekurencję. Nie oczekujemy, że powinniśmy potrzebować biblioteki lub napisać niestandardową funkcję zawierającą 100 wierszy.

Kiedy po raz pierwszy przeczytałem odpowiedź Salakara , naprawdę pomyślałem, że mogę zrobić to lepiej i prościej (można to porównać z Object.assigndalej x={a:1}, y={a:{b:1}}). Potem przeczytałem odpowiedź the8472 i pomyślałem ... że nie da się tak łatwo uciec, poprawa już udzielonych odpowiedzi nie zaprowadzi nas daleko.

Odłóżmy na bok głębokie kopiowanie i rekurencję. Zastanów się, jak (błędnie) ludzie analizują właściwości, aby skopiować bardzo prosty obiekt.

const y = Object.create(
    { proto : 1 },
    { a: { enumerable: true, value: 1},
      [Symbol('b')] : { enumerable: true, value: 1} } )

Object.assign({},y)
> { 'a': 1, Symbol(b): 1 } // All (enumerable) properties are copied

((x,y) => Object.keys(y).reduce((acc,k) => Object.assign(acc, { [k]: y[k] }), x))({},y)
> { 'a': 1 } // Missing a property!

((x,y) => {for (let k in y) x[k]=y[k];return x})({},y)
> { 'a': 1, 'proto': 1 } // Missing a property! Prototype's property is copied too!

Object.keyspominie własne, niepoliczalne właściwości, własne właściwości z kluczem symbolicznym i wszystkie właściwości prototypu. Może być dobrze, jeśli twoje obiekty nie mają żadnego z nich. Ale pamiętaj, że Object.assignobsługuje własne wyliczalne właściwości z kluczami symbolicznymi. Twoja niestandardowa kopia straciła swój rozkwit.

for..indostarczy właściwości źródła, jego prototypu i pełnego łańcucha prototypów bez Twojej woli (lub wiedzy). Twój cel może mieć zbyt wiele właściwości, mieszając właściwości prototypu i właściwości własne.

Jeśli piszesz funkcję ogólnego przeznaczenia i nie używasz Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames, Object.getOwnPropertySymbolslub Object.getPrototypeOf, jesteś najprawdopodobniej robi to źle.

Rzeczy do rozważenia przed napisaniem funkcji

Najpierw upewnij się, że rozumiesz, czym jest obiekt JavaScript. W Javascript obiekt składa się z własnych właściwości i (macierzystego) obiektu prototypowego. Z kolei obiekt prototypowy składa się z własnych właściwości i obiektu prototypowego. I tak dalej, definiując łańcuch prototypów.

Właściwość jest parą klucza ( stringlub symbol) i deskryptora ( valuelub get/ setakcesorium i atrybutów podobnychenumerable ).

Wreszcie istnieje wiele rodzajów obiektów . Możesz chcieć inaczej obsługiwać obiekt Obiekt od obiektu Data lub obiekt Funkcja.

Pisząc swoją głęboką kopię, powinieneś przynajmniej odpowiedzieć na następujące pytania:

  1. Co uważam za głębokie (odpowiednie do rekurencyjnego wyszukiwania w górę) lub płaskie?
  2. Jakie właściwości chcę skopiować? (policzalne / niepoliczalne, kluczowane ciągiem / kluczem symboli, własne właściwości / własne właściwości prototypu, wartości / deskryptory ...)

W moim przykładzie uważam, że tylko object Objects są głębokie , ponieważ inne obiekty utworzone przez innych konstruktorów mogą nie być odpowiednie do dogłębnego spojrzenia. Dostosowane z tego SO .

function toType(a) {
    // Get fine type (object, array, function, null, error, date ...)
    return ({}).toString.call(a).match(/([a-z]+)(:?\])/i)[1];
}

function isDeepObject(obj) {
    return "Object" === toType(obj);
}

I zrobiłem optionsobiekt, aby wybrać, co skopiować (do celów demonstracyjnych).

const options = {nonEnum:true, symbols:true, descriptors: true, proto:true};

Proponowana funkcja

Możesz to przetestować w tym narzędziu .

function deepAssign(options) {
    return function deepAssignWithOptions (target, ...sources) {
        sources.forEach( (source) => {

            if (!isDeepObject(source) || !isDeepObject(target))
                return;

            // Copy source's own properties into target's own properties
            function copyProperty(property) {
                const descriptor = Object.getOwnPropertyDescriptor(source, property);
                //default: omit non-enumerable properties
                if (descriptor.enumerable || options.nonEnum) {
                    // Copy in-depth first
                    if (isDeepObject(source[property]) && isDeepObject(target[property]))
                        descriptor.value = deepAssign(options)(target[property], source[property]);
                    //default: omit descriptors
                    if (options.descriptors)
                        Object.defineProperty(target, property, descriptor); // shallow copy descriptor
                    else
                        target[property] = descriptor.value; // shallow copy value only
                }
            }

            // Copy string-keyed properties
            Object.getOwnPropertyNames(source).forEach(copyProperty);

            //default: omit symbol-keyed properties
            if (options.symbols)
                Object.getOwnPropertySymbols(source).forEach(copyProperty);

            //default: omit prototype's own properties
            if (options.proto)
                // Copy souce prototype's own properties into target prototype's own properties
                deepAssign(Object.assign({},options,{proto:false})) (// Prevent deeper copy of the prototype chain
                    Object.getPrototypeOf(target),
                    Object.getPrototypeOf(source)
                );

        });
        return target;
    }
}

Można tego użyć w następujący sposób:

const x = { a: { a: 1 } },
      y = { a: { b: 1 } };
deepAssign(options)(x,y); // { a: { a: 1, b: 1 } }

13

Używam lodash:

import _ = require('lodash');
value = _.merge(value1, value2);

2
Zauważ, że scalanie zmieni obiekt, jeśli chcesz czegoś, co nie mutuje obiektu, to _cloneDeep(value1).merge(value2)
gekony

3
@geckos Możesz zrobić _.merge ({}, wartość1, wartość2)
Spenhouet

10

Oto implementacja TypeScript:

export const mergeObjects = <T extends object = object>(target: T, ...sources: T[]): T  => {
  if (!sources.length) {
    return target;
  }
  const source = sources.shift();
  if (source === undefined) {
    return target;
  }

  if (isMergebleObject(target) && isMergebleObject(source)) {
    Object.keys(source).forEach(function(key: string) {
      if (isMergebleObject(source[key])) {
        if (!target[key]) {
          target[key] = {};
        }
        mergeObjects(target[key], source[key]);
      } else {
        target[key] = source[key];
      }
    });
  }

  return mergeObjects(target, ...sources);
};

const isObject = (item: any): boolean => {
  return item !== null && typeof item === 'object';
};

const isMergebleObject = (item): boolean => {
  return isObject(item) && !Array.isArray(item);
};

I testy jednostkowe:

describe('merge', () => {
  it('should merge Objects and all nested Ones', () => {
    const obj1 = { a: { a1: 'A1'}, c: 'C', d: {} };
    const obj2 = { a: { a2: 'A2'}, b: { b1: 'B1'}, d: null };
    const obj3 = { a: { a1: 'A1', a2: 'A2'}, b: { b1: 'B1'}, c: 'C', d: null};
    expect(mergeObjects({}, obj1, obj2)).toEqual(obj3);
  });
  it('should behave like Object.assign on the top level', () => {
    const obj1 = { a: { a1: 'A1'}, c: 'C'};
    const obj2 = { a: undefined, b: { b1: 'B1'}};
    expect(mergeObjects({}, obj1, obj2)).toEqual(Object.assign({}, obj1, obj2));
  });
  it('should not merge array values, just override', () => {
    const obj1 = {a: ['A', 'B']};
    const obj2 = {a: ['C'], b: ['D']};
    expect(mergeObjects({}, obj1, obj2)).toEqual({a: ['C'], b: ['D']});
  });
  it('typed merge', () => {
    expect(mergeObjects<TestPosition>(new TestPosition(0, 0), new TestPosition(1, 1)))
      .toEqual(new TestPosition(1, 1));
  });
});

class TestPosition {
  constructor(public x: number = 0, public y: number = 0) {/*empty*/}
}

9

Oto kolejne rozwiązanie ES6, współpracujące z obiektami i tablicami.

function deepMerge(...sources) {
  let acc = {}
  for (const source of sources) {
    if (source instanceof Array) {
      if (!(acc instanceof Array)) {
        acc = []
      }
      acc = [...acc, ...source]
    } else if (source instanceof Object) {
      for (let [key, value] of Object.entries(source)) {
        if (value instanceof Object && key in acc) {
          value = deepMerge(acc[key], value)
        }
        acc = { ...acc, [key]: value }
      }
    }
  }
  return acc
}

3
jest to przetestowane i / lub część biblioteki, wygląda ładnie, ale chciałbym się upewnić, że jest to nieco udowodnione.


8

Chciałbym przedstawić całkiem prostą alternatywę dla ES5. Funkcja otrzymuje 2 parametry - targeti sourcemuszą być typu „obiekt”. Targetbędzie wynikowym obiektem. Targetzachowuje wszystkie swoje oryginalne właściwości, ale ich wartości mogą być modyfikowane.

function deepMerge(target, source) {
if(typeof target !== 'object' || typeof source !== 'object') return false; // target or source or both ain't objects, merging doesn't make sense
for(var prop in source) {
  if(!source.hasOwnProperty(prop)) continue; // take into consideration only object's own properties.
  if(prop in target) { // handling merging of two properties with equal names
    if(typeof target[prop] !== 'object') {
      target[prop] = source[prop];
    } else {
      if(typeof source[prop] !== 'object') {
        target[prop] = source[prop];
      } else {
        if(target[prop].concat && source[prop].concat) { // two arrays get concatenated
          target[prop] = target[prop].concat(source[prop]);
        } else { // two objects get merged recursively
          target[prop] = deepMerge(target[prop], source[prop]); 
        } 
      }  
    }
  } else { // new properties get added to target
    target[prop] = source[prop]; 
  }
}
return target;
}

przypadki:

  • jeśli targetnie ma sourcewłaściwości,target otrzymuje ją;
  • jeśli targetma sourcewłaściwość i target& sourcenie są oba obiekty (3 przypadkach na 4),target „s nieruchomość zostanie nadpisane;
  • jeśli targetma sourcewłaściwość i oba są obiektami / tablicami (1 pozostały przypadek), wówczas rekurencja polega na scaleniu dwóch obiektów (lub konkatenacji dwóch tablic);

rozważ także następujące kwestie :

  1. tablica + obj = tablica
  2. obj + tablica = obj
  3. obj + obj = obj (rekurencyjnie scalone)
  4. tablica + tablica = tablica (konkat)

Jest przewidywalny, obsługuje typy pierwotne, a także tablice i obiekty. Ponieważ możemy scalić 2 obiekty, myślę, że możemy scalić więcej niż 2 za pomocą funkcji zmniejszania .

spójrz na przykład (i pobaw się nim, jeśli chcesz) :

var a = {
   "a_prop": 1,
   "arr_prop": [4, 5, 6],
   "obj": {
     "a_prop": {
       "t_prop": 'test'
     },
     "b_prop": 2
   }
};

var b = {
   "a_prop": 5,
   "arr_prop": [7, 8, 9],
   "b_prop": 15,
   "obj": {
     "a_prop": {
       "u_prop": false
     },
     "b_prop": {
        "s_prop": null
     }
   }
};

function deepMerge(target, source) {
    if(typeof target !== 'object' || typeof source !== 'object') return false;
    for(var prop in source) {
    if(!source.hasOwnProperty(prop)) continue;
      if(prop in target) {
        if(typeof target[prop] !== 'object') {
          target[prop] = source[prop];
        } else {
          if(typeof source[prop] !== 'object') {
            target[prop] = source[prop];
          } else {
            if(target[prop].concat && source[prop].concat) {
              target[prop] = target[prop].concat(source[prop]);
            } else {
              target[prop] = deepMerge(target[prop], source[prop]); 
            } 
          }  
        }
      } else {
        target[prop] = source[prop]; 
      }
    }
  return target;
}

console.log(deepMerge(a, b));

Istnieje ograniczenie - długość stosu wywołań przeglądarki. Nowoczesne przeglądarki generują błąd na bardzo głębokim poziomie rekurencji (pomyśl o tysiącach zagnieżdżonych wywołań). Możesz również dowolnie traktować sytuacje takie jak tablica + obiekt itp., Dodając nowe warunki i sprawdzanie typu.



7

Czy jest na to sposób?

Jeśli jako rozwiązanie można zastosować biblioteki npm , zaawansowane scalanie obiektów pozwala naprawdę głęboko scalać obiekty i dostosowywać / zastępować każdą akcję scalania za pomocą znanej funkcji wywołania zwrotnego. Jego głównym założeniem jest coś więcej niż tylko głębokie scalanie - co dzieje się z wartością, gdy dwa klucze są takie same ? Ta biblioteka się tym zajmuje - kiedy dwa klucze się zderzają,object-merge-advanced waży typy, starając się zachować jak najwięcej danych po scaleniu:

klucz obiektowy łączący typy wartości ważenia, aby zachować jak najwięcej danych

Klucz pierwszego argumentu wejściowego jest oznaczony # 1, drugi argument - # 2. W zależności od każdego typu wybiera się jeden dla wartości klucza wyniku. Na schemacie „obiekt” oznacza zwykły obiekt (nie tablicę itp.).

Kiedy klucze się nie kolidują, wszystkie wprowadzają wynik.

Z fragmentu przykładowego, jeśli użyłeś object-merge-advanceddo scalenia fragmentu kodu:

const mergeObj = require("object-merge-advanced");
const x = { a: { a: 1 } };
const y = { a: { b: 1 } };
const res = console.log(mergeObj(x, y));
// => res = {
//      a: {
//        a: 1,
//        b: 1
//      }
//    }

Algorytm rekurencyjnie przegląda wszystkie klucze obiektów wejściowych, porównuje i buduje oraz zwraca nowy scalony wynik.


6

Poniższa funkcja tworzy głęboką kopię obiektów, obejmuje kopiowanie prymitywów, tablic oraz obiektów

 function mergeDeep (target, source)  {
    if (typeof target == "object" && typeof source == "object") {
        for (const key in source) {
            if (source[key] === null && (target[key] === undefined || target[key] === null)) {
                target[key] = null;
            } else if (source[key] instanceof Array) {
                if (!target[key]) target[key] = [];
                //concatenate arrays
                target[key] = target[key].concat(source[key]);
            } else if (typeof source[key] == "object") {
                if (!target[key]) target[key] = {};
                this.mergeDeep(target[key], source[key]);
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

6

Proste rozwiązanie z ES5 (zastąp istniejącą wartość):

function merge(current, update) {
  Object.keys(update).forEach(function(key) {
    // if update[key] exist, and it's not a string or array,
    // we go in one level deeper
    if (current.hasOwnProperty(key) 
        && typeof current[key] === 'object'
        && !(current[key] instanceof Array)) {
      merge(current[key], update[key]);

    // if update[key] doesn't exist in current, or it's a string
    // or array, then assign/overwrite current[key] to update[key]
    } else {
      current[key] = update[key];
    }
  });
  return current;
}

var x = { a: { a: 1 } }
var y = { a: { b: 1 } }

console.log(merge(x, y));


właśnie tego potrzebowałem - es6 powodował problemy w kompilacji - ta alternatywa es5 to bomba
danday74,

5

Większość przykładów tutaj wydaje się zbyt skomplikowana, używam jednego z utworzonych przeze mnie TypeScript, myślę, że powinien on obejmować większość przypadków (obsługuję tablice jako zwykłe dane, po prostu je zastępuję).

const isObject = (item: any) => typeof item === 'object' && !Array.isArray(item);

export const merge = <A = Object, B = Object>(target: A, source: B): A & B => {
  const isDeep = (prop: string) =>
    isObject(source[prop]) && target.hasOwnProperty(prop) && isObject(target[prop]);
  const replaced = Object.getOwnPropertyNames(source)
    .map(prop => ({ [prop]: isDeep(prop) ? merge(target[prop], source[prop]) : source[prop] }))
    .reduce((a, b) => ({ ...a, ...b }), {});

  return {
    ...(target as Object),
    ...(replaced as Object)
  } as A & B;
};

To samo w zwykłym JS, na wszelki wypadek:

const isObject = item => typeof item === 'object' && !Array.isArray(item);

const merge = (target, source) => {
  const isDeep = prop => 
    isObject(source[prop]) && target.hasOwnProperty(prop) && isObject(target[prop]);
  const replaced = Object.getOwnPropertyNames(source)
    .map(prop => ({ [prop]: isDeep(prop) ? merge(target[prop], source[prop]) : source[prop] }))
    .reduce((a, b) => ({ ...a, ...b }), {});

  return {
    ...target,
    ...replaced
  };
};

Oto moje przypadki testowe, które pokazują, jak możesz z nich skorzystać

describe('merge', () => {
  context('shallow merges', () => {
    it('merges objects', () => {
      const a = { a: 'discard' };
      const b = { a: 'test' };
      expect(merge(a, b)).to.deep.equal({ a: 'test' });
    });
    it('extends objects', () => {
      const a = { a: 'test' };
      const b = { b: 'test' };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: 'test' });
    });
    it('extends a property with an object', () => {
      const a = { a: 'test' };
      const b = { b: { c: 'test' } };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: { c: 'test' } });
    });
    it('replaces a property with an object', () => {
      const a = { b: 'whatever', a: 'test' };
      const b = { b: { c: 'test' } };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: { c: 'test' } });
    });
  });

  context('deep merges', () => {
    it('merges objects', () => {
      const a = { test: { a: 'discard', b: 'test' }  };
      const b = { test: { a: 'test' } } ;
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: 'test' } });
    });
    it('extends objects', () => {
      const a = { test: { a: 'test' } };
      const b = { test: { b: 'test' } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: 'test' } });
    });
    it('extends a property with an object', () => {
      const a = { test: { a: 'test' } };
      const b = { test: { b: { c: 'test' } } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: { c: 'test' } } });
    });
    it('replaces a property with an object', () => {
      const a = { test: { b: 'whatever', a: 'test' } };
      const b = { test: { b: { c: 'test' } } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: { c: 'test' } } });
    });
  });
});

Daj mi znać, jeśli uważasz, że brakuje mi niektórych funkcji.


5

Jeśli chcesz mieć jedną linijkę bez konieczności posiadania ogromnej biblioteki, takiej jak lodash, sugeruję skorzystanie z deepmerge . ( npm install deepmerge)

Następnie możesz to zrobić

deepmerge({ a: 1, b: 2, c: 3 }, { a: 2, d: 3 });

dostać

{ a: 2, b: 2, c: 3, d: 3 }

Zaletą jest to, że od razu zawiera napisy do TypeScript. Pozwala także łączyć tablice . To jest naprawdę wszechstronne rozwiązanie.


4

Możemy użyć $ .extend (true, object1, object2) do głębokiego scalenia. Wartość true oznacza rekurencyjne scalenie dwóch obiektów, modyfikując pierwszy.

$ extension (true, target, object)


9
Pytający nigdy nie wskazał, że używa jquery i wydaje się, że prosi o natywne rozwiązanie javascript.
Te JoE

Jest to bardzo prosty sposób na zrobienie tego i działa. Realne rozwiązanie, które rozważyłbym, gdybym zadawał to pytanie. :)
kashiraja

To bardzo dobra odpowiedź, ale brakuje linku do kodu źródłowego do jQuery. jQuery ma wiele osób pracujących nad projektem i spędzili trochę czasu na poprawnym kopiowaniu działającym poprawnie. Ponadto kod źródłowy jest dość „prosty”: github.com/jquery/jquery/blob/master/src/core.js#L125 „Prosty” jest w cudzysłowie, ponieważ zaczyna się komplikować, gdy się w niego zagłębiszjQuery.isPlainObject() . Ujawnia to złożoność określania, czy coś jest prostym przedmiotem, którego większość odpowiedzi tutaj omija z daleka. Zgadnij, w jakim języku jest napisane jQuery?
CubicleSoft

4

Oto proste, proste rozwiązanie, które działa jak Object.assignpo prostu deeep i działa na tablicę, bez żadnych modyfikacji

function deepAssign(target, ...sources) {
    for( source of sources){
        for(let k in source){
            let vs = source[k], vt = target[k];
            if(Object(vs)== vs && Object(vt)===vt ){
                target[k] = deepAssign(vt, vs)
                continue;
            }
            target[k] = source[k];
        }    
    }
    return target;
}

Przykład

x = { a: { a: 1 }, b:[1,2] };
y = { a: { b: 1 }, b:[3] };
z = {c:3,b:[,,,4]}
x = deepAssign(x,y,z)
// x will be
x ==  {
  "a": {
    "a": 1,
    "b": 1
  },
  "b": [    1,    2,    null,    4  ],
  "c": 3
}


3

Miałem ten problem podczas ładowania buforowanego stanu redux. Gdybym tylko załadował stan buforowany, napotkałbym błędy dla nowej wersji aplikacji ze zaktualizowaną strukturą stanu.

Wspomniano już, że lodash oferuje mergefunkcję, z której korzystałem:

const currentInitialState = configureState().getState();
const mergedState = _.merge({}, currentInitialState, cachedState);
const store = configureState(mergedState);

3

Wiele odpowiedzi wykorzystuje dziesiątki linii kodu lub wymaga dodania nowej biblioteki do projektu, ale jeśli użyjesz rekurencji, to tylko 4 linie kodu.

function merge(current, updates) {
  for (key of Object.keys(updates)) {
    if (!current.hasOwnProperty(key) || typeof updates[key] !== 'object') current[key] = updates[key];
    else merge(current[key], updates[key]);
  }
  return current;
}
console.log(merge({ a: { a: 1 } }, { a: { b: 1 } }));

Obsługa tablic: powyższa wersja zastępuje stare wartości tablic nowymi. Jeśli chcesz zachować stare wartości tablic i dodać nowe, po prostu dodaj else if (current[key] instanceof Array && updates[key] instanceof Array) current[key] = current[key].concat(updates[key])blok nad elsestatamentem i wszystko gotowe.


1
Podoba mi się, ale wymaga prostego niezdefiniowanego sprawdzenia „bieżącej”, w przeciwnym razie {foo: undefined} się nie połączy. Po prostu dodaj if (bieżący) przed pętlą for.
Andreas Pardeike

Dzięki za sugestię
Vincent

2

Oto kolejny, który właśnie napisałem, który obsługuje tablice. To ich konkretyzuje.

function isObject(obj) {
    return obj !== null && typeof obj === 'object';
}


function isPlainObject(obj) {
    return isObject(obj) && (
        obj.constructor === Object  // obj = {}
        || obj.constructor === undefined // obj = Object.create(null)
    );
}

function mergeDeep(target, ...sources) {
    if (!sources.length) return target;
    const source = sources.shift();

    if(Array.isArray(target)) {
        if(Array.isArray(source)) {
            target.push(...source);
        } else {
            target.push(source);
        }
    } else if(isPlainObject(target)) {
        if(isPlainObject(source)) {
            for(let key of Object.keys(source)) {
                if(!target[key]) {
                    target[key] = source[key];
                } else {
                    mergeDeep(target[key], source[key]);
                }
            }
        } else {
            throw new Error(`Cannot merge object with non-object`);
        }
    } else {
        target = source;
    }

    return mergeDeep(target, ...sources);
};

2

Użyj tej funkcji:

merge(target, source, mutable = false) {
        const newObj = typeof target == 'object' ? (mutable ? target : Object.assign({}, target)) : {};
        for (const prop in source) {
            if (target[prop] == null || typeof target[prop] === 'undefined') {
                newObj[prop] = source[prop];
            } else if (Array.isArray(target[prop])) {
                newObj[prop] = source[prop] || target[prop];
            } else if (target[prop] instanceof RegExp) {
                newObj[prop] = source[prop] || target[prop];
            } else {
                newObj[prop] = typeof source[prop] === 'object' ? this.merge(target[prop], source[prop]) : source[prop];
            }
        }
        return newObj;
    }

2

Ramda, która jest ładną biblioteką funkcji javascript, ma mergeDeepLeft i mergeDeepRight. Każda z nich działa całkiem nieźle na ten problem. Zajrzyj do dokumentacji tutaj: https://ramdajs.com/docs/#mergeDeepLeft

Do konkretnego przykładu, o którym mowa, możemy użyć:

import { mergeDeepLeft } from 'ramda'
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = mergeDeepLeft(x, y)) // {"a":{"a":1,"b":1}}

2
// copies all properties from source object to dest object recursively
export function recursivelyMoveProperties(source, dest) {
  for (const prop in source) {
    if (!source.hasOwnProperty(prop)) {
      continue;
    }

    if (source[prop] === null) {
      // property is null
      dest[prop] = source[prop];
      continue;
    }

    if (typeof source[prop] === 'object') {
      // if property is object let's dive into in
      if (Array.isArray(source[prop])) {
        dest[prop] = [];
      } else {
        if (!dest.hasOwnProperty(prop)
        || typeof dest[prop] !== 'object'
        || dest[prop] === null || Array.isArray(dest[prop])
        || !Object.keys(dest[prop]).length) {
          dest[prop] = {};
        }
      }
      recursivelyMoveProperties(source[prop], dest[prop]);
      continue;
    }

    // property is simple type: string, number, e.t.c
    dest[prop] = source[prop];
  }
  return dest;
}

Test jednostkowy:

describe('recursivelyMoveProperties', () => {
    it('should copy properties correctly', () => {
      const source: any = {
        propS1: 'str1',
        propS2: 'str2',
        propN1: 1,
        propN2: 2,
        propA1: [1, 2, 3],
        propA2: [],
        propB1: true,
        propB2: false,
        propU1: null,
        propU2: null,
        propD1: undefined,
        propD2: undefined,
        propO1: {
          subS1: 'sub11',
          subS2: 'sub12',
          subN1: 11,
          subN2: 12,
          subA1: [11, 12, 13],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
        propO2: {
          subS1: 'sub21',
          subS2: 'sub22',
          subN1: 21,
          subN2: 22,
          subA1: [21, 22, 23],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      };
      let dest: any = {
        propS2: 'str2',
        propS3: 'str3',
        propN2: -2,
        propN3: 3,
        propA2: [2, 2],
        propA3: [3, 2, 1],
        propB2: true,
        propB3: false,
        propU2: 'not null',
        propU3: null,
        propD2: 'defined',
        propD3: undefined,
        propO2: {
          subS2: 'inv22',
          subS3: 'sub23',
          subN2: -22,
          subN3: 23,
          subA2: [5, 5, 5],
          subA3: [31, 32, 33],
          subB2: false,
          subB3: true,
          subU2: 'not null --- ',
          subU3: null,
          subD2: ' not undefined ----',
          subD3: undefined,
        },
        propO3: {
          subS1: 'sub31',
          subS2: 'sub32',
          subN1: 31,
          subN2: 32,
          subA1: [31, 32, 33],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      };
      dest = recursivelyMoveProperties(source, dest);

      expect(dest).toEqual({
        propS1: 'str1',
        propS2: 'str2',
        propS3: 'str3',
        propN1: 1,
        propN2: 2,
        propN3: 3,
        propA1: [1, 2, 3],
        propA2: [],
        propA3: [3, 2, 1],
        propB1: true,
        propB2: false,
        propB3: false,
        propU1: null,
        propU2: null,
        propU3: null,
        propD1: undefined,
        propD2: undefined,
        propD3: undefined,
        propO1: {
          subS1: 'sub11',
          subS2: 'sub12',
          subN1: 11,
          subN2: 12,
          subA1: [11, 12, 13],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
        propO2: {
          subS1: 'sub21',
          subS2: 'sub22',
          subS3: 'sub23',
          subN1: 21,
          subN2: 22,
          subN3: 23,
          subA1: [21, 22, 23],
          subA2: [],
          subA3: [31, 32, 33],
          subB1: false,
          subB2: true,
          subB3: true,
          subU1: null,
          subU2: null,
          subU3: null,
          subD1: undefined,
          subD2: undefined,
          subD3: undefined,
        },
        propO3: {
          subS1: 'sub31',
          subS2: 'sub32',
          subN1: 31,
          subN2: 32,
          subA1: [31, 32, 33],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      });
    });
  });

2

Znalazłem tylko 2-liniowe rozwiązanie, aby uzyskać głębokie scalenie w javascript. Daj mi znać, jak Ci się to uda.

const obj1 = { a: { b: "c", x: "y" } }
const obj2 = { a: { b: "d", e: "f" } }
temp = Object.assign({}, obj1, obj2)
Object.keys(temp).forEach(key => {
    temp[key] = (typeof temp[key] === 'object') ? Object.assign(temp[key], obj1[key], obj2[key]) : temp[key])
}
console.log(temp)

Obiekt Temp wydrukuje {a: {b: 'd', e: 'f', x: 'y'}}


1
To nie powoduje rzeczywistego głębokiego scalenia. To się nie powiedzie merge({x:{y:{z:1}}}, {x:{y:{w:2}}}). Il również nie zaktualizuje istniejących wartości w obj1, jeśli obj2 je ma, na przykład za pomocą merge({x:{y:1}}, {x:{y:2}}).
Oreilles

1

Czasami nie potrzebujesz głębokiego łączenia, nawet jeśli tak uważasz. Na przykład, jeśli masz domyślną konfigurację z zagnieżdżonymi obiektami i chcesz ją głęboko rozszerzyć o własną konfigurację, możesz dla tego stworzyć klasę. Koncepcja jest bardzo prosta:

function AjaxConfig(config) {

  // Default values + config

  Object.assign(this, {
    method: 'POST',
    contentType: 'text/plain'
  }, config);

  // Default values in nested objects

  this.headers = Object.assign({}, this.headers, { 
    'X-Requested-With': 'custom'
  });
}

// Define your config

var config = {
  url: 'https://google.com',
  headers: {
    'x-client-data': 'CI22yQEI'
  }
};

// Extend the default values with your own
var fullMergedConfig = new AjaxConfig(config);

// View in DevTools
console.log(fullMergedConfig);

Możesz przekonwertować go na funkcję (nie konstruktor).


1

Jest to tanie głębokie scalanie, które wykorzystuje tak mało kodu, jak mogłem sobie wyobrazić. Każde źródło zastępuje poprzednią właściwość, jeśli istnieje.

const { keys } = Object;

const isObject = a => typeof a === "object" && !Array.isArray(a);
const merge = (a, b) =>
  isObject(a) && isObject(b)
    ? deepMerge(a, b)
    : isObject(a) && !isObject(b)
    ? a
    : b;

const coalesceByKey = source => (acc, key) =>
  (acc[key] && source[key]
    ? (acc[key] = merge(acc[key], source[key]))
    : (acc[key] = source[key])) && acc;

/**
 * Merge all sources into the target
 * overwriting primitive values in the the accumulated target as we go (if they already exist)
 * @param {*} target
 * @param  {...any} sources
 */
const deepMerge = (target, ...sources) =>
  sources.reduce(
    (acc, source) => keys(source).reduce(coalesceByKey(source), acc),
    target
  );

console.log(deepMerge({ a: 1 }, { a: 2 }));
console.log(deepMerge({ a: 1 }, { a: { b: 2 } }));
console.log(deepMerge({ a: { b: 2 } }, { a: 1 }));

1

Używam następującej krótkiej funkcji do głębokiego scalania obiektów.
Działa świetnie dla mnie.
Autor całkowicie wyjaśnia, jak tu działa.

/*!
 * Merge two or more objects together.
 * (c) 2017 Chris Ferdinandi, MIT License, https://gomakethings.com
 * @param   {Boolean}  deep     If true, do a deep (or recursive) merge [optional]
 * @param   {Object}   objects  The objects to merge together
 * @returns {Object}            Merged values of defaults and options
 * 
 * Use the function as follows:
 * let shallowMerge = extend(obj1, obj2);
 * let deepMerge = extend(true, obj1, obj2)
 */

var extend = function () {

    // Variables
    var extended = {};
    var deep = false;
    var i = 0;

    // Check if a deep merge
    if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) {
        deep = arguments[0];
        i++;
    }

    // Merge the object into the extended object
    var merge = function (obj) {
        for (var prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                // If property is an object, merge properties
                if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]') {
                    extended[prop] = extend(extended[prop], obj[prop]);
                } else {
                    extended[prop] = obj[prop];
                }
            }
        }
    };

    // Loop through each object and conduct a merge
    for (; i < arguments.length; i++) {
        merge(arguments[i]);
    }

    return extended;

};

Chociaż ten link może odpowiedzieć na pytanie, lepiej jest dołączyć tutaj istotne części odpowiedzi i podać link w celach informacyjnych. Odpowiedzi zawierające tylko łącze mogą stać się nieprawidłowe, jeśli połączona strona ulegnie zmianie. - Z recenzji
Chris Camaratta

Cześć @ChrisCamaratta. Nie tylko tutaj jest istotna część, to wszystko tutaj - funkcja i sposób jej użycia. Więc to zdecydowanie nie jest tylko odpowiedź na link. Jest to funkcja, której używałem do głębokiego scalania obiektów. Link jest tylko wtedy, gdy chcesz wyjaśnić autorom, jak to działa. Wydaje mi się, że próba wyjaśnienia działania lepiej niż autorowi, który uczy JavaScript, byłaby niekorzystna dla społeczności. Dziękuję za komentarz.
John Shearing,

Huh Albo go przegapiłem, albo kod nie pojawił się w interfejsie recenzenta, kiedy go sprawdziłem. Zgadzam się, że to wysokiej jakości odpowiedź. Wygląda na to, że inni recenzenci przesłonili moją wstępną ocenę, więc myślę, że wszystko jest w porządku. Przepraszamy za flagę inspiracji.
Chris Camaratta,

Świetny! @ChrisCamaratta, Dziękujemy za pomoc w zrozumieniu, co się stało.
John Shearing,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.