Bagaimana cara menggabungkan dalam dan bukannya menggabungkan?


340

Keduanya Object.assign dan Object spread hanya melakukan penggabungan yang dangkal.

Contoh masalah:

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

Output adalah apa yang Anda harapkan. Namun jika saya coba ini:

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

Dari pada

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

Anda mendapatkan

{ a: { b: 1 } }

x sepenuhnya ditimpa karena sintaks penyebaran hanya berjalan satu tingkat dalam. Ini sama denganObject.assign() .

Apakah ada cara untuk melakukan ini?


apakah penggabungan yang dalam sama dengan menyalin properti dari satu objek ke objek lainnya?

2
Tidak, karena properti objek tidak boleh ditimpa, melainkan setiap objek anak harus digabungkan menjadi anak yang sama pada target jika sudah ada.
Mike

ES6 selesai dan fitur baru tidak lagi ditambahkan, AFAIK.
kangax


1
@Oriol membutuhkan jQuery ...
m0meni

Jawaban:


331

Apakah ada yang tahu jika penggabungan dalam ada dalam spesifikasi ES6 / ES7?

Tidak.


21
Harap tinjau riwayat edit. Pada saat saya menjawab ini, pertanyaannya adalah Apakah ada yang tahu jika penggabungan dalam ada dalam spesifikasi ES6 / ES7? .

37
Jawaban ini tidak lagi berlaku untuk pertanyaan ini - harus diperbarui atau dihapus
DonVaughn

13
Pertanyaannya seharusnya tidak diedit ke tingkat ini. Suntingan untuk klarifikasi. Pertanyaan baru seharusnya sudah diposting.
CJ Thompson

171

Saya tahu ini sedikit masalah lama tapi solusi termudah di ES2015 / ES6 yang bisa saya buat sebenarnya cukup sederhana, menggunakan Object.assign (),

Semoga ini membantu:

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

Contoh penggunaan:

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

Anda akan menemukan versi yang tidak dapat diubah dari jawaban ini di bawah.

Perhatikan bahwa ini akan menyebabkan rekursi tak terbatas pada referensi melingkar. Ada beberapa jawaban bagus di sini tentang cara mendeteksi referensi melingkar jika Anda pikir Anda akan menghadapi masalah ini.


1
jika grafik objek Anda berisi siklus yang akan menyebabkan rekursi tak terbatas
the8472

item !== nullseharusnya tidak diperlukan di dalam isObject, karena itemsudah diperiksa kebenarannya di awal kondisi
mcont

2
Mengapa menulis ini: Object.assign(target, { [key]: {} })kalau bisa begitu saja target[key] = {}?
Jürg Lehni

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

3
Ini tidak mendukung objek yang tidak biasa di target. Misalnya, mergeDeep({a: 3}, {a: {b: 4}})akan menghasilkan Numberobjek augmented , yang jelas tidak diinginkan. Juga, isObjecttidak menerima array, tetapi menerima jenis objek asli lainnya, seperti Date, yang tidak boleh disalin dalam.
riv

122

Anda dapat menggunakan gabungan 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
Hai teman-teman, ini adalah solusi paling sederhana dan paling indah. Lodash luar biasa, mereka harus memasukkannya sebagai objek inti js
Nurbol Alpysbayev

11
Bukankah seharusnya hasilnya { 'a': [{ 'b': 2 }, { 'c': 3 }, { 'd': 4 }, { 'e': 5 }] }?
J. Hesters

Pertanyaan bagus. Itu mungkin pertanyaan terpisah atau satu untuk pengelola Lodash.
AndrewHenderson

7
Hasilnya { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }benar, karena kami menggabungkan elemen array. Unsur 0dari object.ayaitu {b: 2}, unsur 0dari other.aadalah {c: 3}. Ketika keduanya digabungkan karena mereka memiliki indeks array yang sama, hasilnya adalah { 'b': 2, 'c': 3 }, yang merupakan elemen 0dalam objek baru.
Alexandru Furculita

Saya lebih suka yang ini , ukurannya 6x lebih kecil.
Solo

101

Masalahnya adalah non-sepele ketika datang ke objek host atau segala jenis objek yang lebih kompleks daripada sekumpulan nilai

  • Apakah Anda meminta seorang pengambil untuk mendapatkan nilai atau Anda menyalin atas deskriptor properti?
  • bagaimana jika target gabungan memiliki setter (baik properti sendiri atau dalam rantai prototipe)? Apakah Anda menganggap nilai sudah ada atau memanggil setter untuk memperbarui nilai saat ini?
  • apakah Anda menjalankan fungsi properti sendiri atau menyalinnya? Bagaimana jika mereka terikat fungsi atau fungsi panah tergantung pada sesuatu dalam rantai lingkupnya pada saat mereka didefinisikan?
  • bagaimana jika itu seperti simpul DOM? Anda tentu tidak ingin memperlakukannya sebagai objek sederhana dan menggabungkan semua propertinya secara mendalam
  • bagaimana menangani struktur "sederhana" seperti array atau peta atau set? Anggap mereka sudah ada atau gabungkan juga?
  • bagaimana cara menangani properti sendiri yang tidak dapat dihitung?
  • bagaimana dengan subtrees baru? Cukup dengan referensi atau klon mendalam?
  • bagaimana menangani benda yang beku / tersegel / tidak dapat diperluas?

Hal lain yang perlu diingat: Objek grafik yang berisi siklus. Biasanya tidak sulit untuk ditangani - cukup simpan aSet objek sumber yang sudah dikunjungi - tetapi sering dilupakan.

Anda mungkin harus menulis fungsi penggabungan-dalam yang hanya mengharapkan nilai-nilai primitif dan objek-objek sederhana - paling banyak tipe yang dapat ditangani oleh algoritma klon terstruktur - sebagai sumber penggabungan. Lempar jika menemukan sesuatu yang tidak dapat ditangani atau hanya ditugaskan dengan referensi alih-alih penggabungan yang mendalam.

Dengan kata lain, tidak ada algoritma satu ukuran untuk semua, Anda harus memutar sendiri atau mencari metode pustaka yang terjadi untuk menutupi kasus penggunaan Anda.


2
alasan untuk dev V8 untuk tidak menerapkan transfer "status dokumen" yang aman
neaumusic

Anda mengangkat banyak masalah bagus dan saya ingin melihat implementasi dari rekomendasi Anda. Jadi saya mencoba membuatnya di bawah. Bisakah Anda melihat dan berkomentar? stackoverflow.com/a/48579540/8122487
RaphaMex

66

Berikut adalah versi jawaban @ Salakar yang tidak berubah (tidak mengubah input). Berguna jika Anda melakukan hal-hal jenis pemrograman fungsional.

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 lihat posting sebelumnya oleh saya untuk fungsi
isObject

memperbaruinya. setelah beberapa pengujian saya menemukan bug dengan objek yang sangat bersarang
CpILL

3
Dengan nama properti yang dihitung, yang pertama akan menggunakan nilai keysebagai nama properti, yang kemudian akan membuat "kunci" nama properti. Lihat: es6-features.org/#ComputedPropertyNames
CpILL

2
di isObjectAnda tidak perlu memeriksa && item !== nulldi akhir, karena garis dimulai dengan item &&, bukan?
ephemer

2
Jika sumber memiliki objek anak bersarang lebih dalam dari target, objek tersebut masih akan merujuk nilai yang sama dalam mergedDeepoutput (saya pikir). Misalnya, 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 apakah ini masalah? Itu tidak bermutasi input, tetapi mutasi ke input di masa depan dapat memutasi output, dan sebaliknya w / mutasi ke output input bermutasi. Namun, untuk apa nilainya, ramda R.merge()memiliki perilaku yang sama.
James Conkling

40

Karena masalah ini masih aktif, inilah pendekatan lain:

  • ES6 / 2015
  • Abadi (tidak mengubah objek asli)
  • Menangani array (menggabungkannya)

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


Ini bagus. Namun ketika kita memiliki array dengan elemen berulang, ini digabungkan (ada elemen berulang). Saya mengadaptasi ini untuk mengambil parameter (array unik: benar / salah).
Astronaut

1
Untuk membuat array unik, Anda dapat mengubah prev[key] = pVal.concat(...oVal);keprev[key] = [...pVal, ...oVal].filter((element, index, array) => array.indexOf(element) === index);
Richard Herries

1
Sangat bagus dan bersih !! Pasti jawaban terbaik di sini!
538ROMEO

Mulia. Yang ini menunjukkan juga bahwa array bisa digabung, itulah yang saya cari.
Tschallacka

Yap, solusi @CplLL dikatakan tidak dapat diubah tetapi menggunakan mutabilitas objek aktual di dalam fungsi saat menggunakan reduce tidak.
Augustin Riedinger

30

Saya tahu sudah ada banyak jawaban dan banyak komentar yang berpendapat itu tidak akan berhasil. Satu-satunya konsensus adalah bahwa itu sangat rumit sehingga tidak ada yang membuat standar untuk itu . Namun, sebagian besar jawaban yang diterima di SO mengekspos "trik sederhana" yang banyak digunakan. Jadi, bagi kita semua seperti saya yang bukan ahli tetapi ingin menulis kode yang lebih aman dengan memahami sedikit lebih banyak tentang kompleksitas javascript, saya akan mencoba menjelaskan.

Sebelum membuat tangan kita kotor, izinkan saya mengklarifikasi 2 poin:

  • [PERNYATAAN] Saya mengusulkan fungsi di bawah ini yang menangani bagaimana kita dalam loop ke objek javascript untuk menyalin dan menggambarkan apa yang umumnya terlalu singkat dikomentari. Ini belum siap produksi. Demi kejelasan, saya sengaja mengesampingkan pertimbangan lain seperti objek melingkar (melacak oleh set atau properti simbol tanpa konflik) , menyalin nilai referensi atau klon dalam , objek tujuan tidak dapat diubah (klon dalam lagi?), Studi kasus per kasus setiap jenis objek , dapatkan / atur properti melalui accessors ... Juga, saya tidak menguji kinerja - walaupun itu penting - karena itu bukan intinya di sini juga.
  • Saya akan menggunakan salinan atau menetapkan persyaratan alih-alih menggabungkan . Karena dalam pikiranku penggabungan bersifat konservatif dan harus gagal pada konflik. Di sini, ketika bertikai, kami ingin sumber menimpa tujuan. Seperti Object.assignhalnya.

Jawaban dengan for..inatau Object.keysmenyesatkan

Membuat salinan yang dalam tampaknya praktik yang sangat mendasar dan umum sehingga kami berharap dapat menemukan satu garis atau, paling tidak, kemenangan cepat melalui rekursi sederhana. Kami tidak berharap kami perlu perpustakaan atau menulis fungsi khusus 100 baris.

Ketika saya pertama kali membaca jawaban Salakar , saya benar-benar berpikir saya bisa melakukan yang lebih baik dan lebih sederhana (Anda dapat membandingkannya dengan Object.assigndi x={a:1}, y={a:{b:1}}). Kemudian saya membaca jawaban the8472 dan saya pikir ... tidak ada jalan keluar dengan mudah, meningkatkan jawaban yang sudah diberikan tidak akan membuat kita jauh.

Mari kita salin yang dalam dan mengesampingkan rekursif sesaat. Perhatikan saja (salah) orang mengurai properti untuk menyalin objek yang sangat sederhana.

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.keysakan menghilangkan properti non-enumerable sendiri, memiliki properti key-keyed dan semua properti prototipe. Ini mungkin baik-baik saja jika objek Anda tidak memilikinya. Tetapi perlu diingat bahwa Object.assignmenangani properti enumerable key-keyed sendiri. Jadi salinan kustom Anda hilang mekar.

for..inakan memberikan properti dari sumber, prototipe dan rantai prototipe lengkap tanpa Anda inginkan (atau menyadarinya). Target Anda mungkin berakhir dengan terlalu banyak properti, mencampur properti prototipe dan properti sendiri.

Jika Anda sedang menulis fungsi tujuan umum dan Anda tidak menggunakan Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames, Object.getOwnPropertySymbolsatau Object.getPrototypeOf, Anda yang paling mungkin melakukannya salah.

Hal yang perlu dipertimbangkan sebelum menulis fungsi Anda

Pertama, pastikan Anda memahami apa itu objek Javascript. Dalam Javascript, sebuah objek dibuat dari propertinya sendiri dan objek prototipe (induk). Objek prototipe pada gilirannya terbuat dari propertinya sendiri dan objek prototipe. Dan seterusnya, mendefinisikan rantai prototipe.

Properti adalah sepasang kunci (string atau symbol) dan deskriptor ( valueatau get/ setaccessor, dan atribut seperti enumerable).

Akhirnya, ada banyak jenis objek . Anda mungkin ingin menangani objek objek secara berbeda dari objek Date atau objek Function.

Jadi, menulis salinan Anda yang dalam, Anda harus menjawab setidaknya pertanyaan-pertanyaan itu:

  1. Apa yang saya anggap dalam (layak untuk tampilan rekursif) atau flat?
  2. Properti apa yang ingin saya salin? (enumerable / non-enumerable, string-keyed / symbol-keyed, properti sendiri / properti prototipe sendiri, nilai / deskripsi ...)

Sebagai contoh saya, saya menganggap bahwa hanya object Objects yang dalam , karena objek lain yang dibuat oleh konstruktor lain mungkin tidak layak untuk tampilan yang mendalam. Disesuaikan dari SO ini .

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

Dan saya membuat optionsobjek untuk memilih apa yang akan disalin (untuk tujuan demo).

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

Fungsi yang diusulkan

Anda dapat mengujinya di plunker ini .

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

Itu bisa digunakan seperti ini:

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

13

Saya menggunakan lodash:

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

2
Perhatikan bahwa penggabungan akan mengubah objek, jika Anda menginginkan sesuatu yang tidak bermutasi objek, maka _cloneDeep(value1).merge(value2)
tokek

3
@ tokek Anda dapat melakukan _.merge ({}, value1, value2)
Spenhouet

10

Berikut ini adalah implementasi 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);
};

Dan Unit Tes:

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

Berikut ini adalah solusi ES6 lainnya, yang bekerja dengan objek dan array.

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
apakah ini diuji dan / atau bagian dari perpustakaan, terlihat bagus tetapi ingin memastikan itu agak terbukti.


8

Saya ingin menyajikan alternatif ES5 yang cukup sederhana. Fungsi mendapat 2 parameter - targetdan sourceitu harus bertipe "objek". Targetakan menjadi objek yang dihasilkan. Targetmenyimpan semua properti aslinya tetapi nilainya dapat dimodifikasi.

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

kasus:

  • jika targettidak punyasource properti, targetdapatkan;
  • jika targetmemang memiliki sourceproperti dan target& sourcebukan keduanya objek (3 kasus dari 4),target properti akan ditimpa;
  • jika targetmemang memiliki sourceproperti dan keduanya adalah objek / array (1 kasing sisa), maka rekursi terjadi menggabungkan dua objek (atau gabungan dua array);

pertimbangkan juga hal berikut :

  1. array + obj = array
  2. obj + array = obj
  3. obj + obj = obj (digabung secara rekursif)
  4. array + array = array (concat)

Dapat diprediksi, mendukung tipe primitif serta array dan objek. Juga karena kita dapat menggabungkan 2 objek, saya pikir kita dapat menggabungkan lebih dari 2 melalui fungsi pengurangan .

lihat sebuah contoh (dan mainkan dengan itu jika Anda mau) :

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

Ada batasan - panjang tumpukan panggilan browser. Peramban modern akan membuat kesalahan pada tingkat rekursi yang sangat dalam (pikirkan ribuan panggilan bersarang). Anda juga bebas untuk memperlakukan situasi seperti array + objek dll sesuai keinginan dengan menambahkan kondisi baru dan ketik cek.



7

Apakah ada cara untuk melakukan ini?

Jika pustaka npm dapat digunakan sebagai solusi, object-merge-advanced dari milik Anda benar-benar memungkinkan untuk menggabungkan objek secara mendalam dan menyesuaikan / menimpa setiap aksi penggabungan menggunakan fungsi panggilan balik yang umum. Gagasan utama lebih dari sekadar penggabungan dalam - apa yang terjadi dengan nilai ketika dua kunci sama ? Pustaka ini menangani hal itu - ketika dua kunci berbenturan, object-merge-advancedmenimbang jenisnya, bertujuan untuk menyimpan data sebanyak mungkin setelah penggabungan:

penggabungan kunci objek dengan menimbang jenis nilai kunci untuk mempertahankan data sebanyak mungkin

Kunci argumen input pertama ditandai # 1, argumen kedua - # 2. Bergantung pada masing-masing jenis, satu dipilih untuk nilai kunci hasil. Dalam diagram, "suatu objek" berarti objek sederhana (bukan array, dll).

Ketika kunci tidak berbenturan, mereka semua memasukkan hasilnya.

Dari cuplikan contoh Anda, jika Anda biasa object-merge-advancedmenggabungkan cuplikan kode Anda:

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

Algoritme ini secara rekursif menelusuri semua kunci objek input, membandingkan dan membangun serta mengembalikan hasil yang digabungkan.


6

Fungsi berikut membuat salinan objek yang dalam, mencakup penyalinan primitif, array serta objek

 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

Solusi sederhana dengan ES5 (menimpa nilai yang ada):

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


apa yang saya butuhkan - es6 menyebabkan masalah dalam pembangunan - alternatif es5 ini adalah bom
danday74

5

Sebagian besar contoh di sini tampaknya terlalu kompleks, saya menggunakan satu di TypeScript yang saya buat, saya pikir itu harus mencakup sebagian besar kasus (saya menangani array sebagai data biasa, hanya menggantinya).

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

Hal yang sama di JS polos, untuk jaga-jaga:

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

Berikut ini adalah contoh pengujian saya untuk menunjukkan bagaimana Anda dapat menggunakannya

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

Tolong beri tahu saya jika Anda pikir saya kehilangan beberapa fungsi.


5

Jika Anda ingin memiliki satu liner tanpa memerlukan perpustakaan besar seperti lodash, saya sarankan Anda menggunakan deepmerge . ( npm install deepmerge)

Lalu, Anda bisa melakukannya

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

mendapatkan

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

Yang menyenangkan adalah ia datang dengan mengetik untuk TypeScript segera. Ini juga memungkinkan untuk menggabungkan array . Solusi serba nyata ini.


4

Kita bisa menggunakan $ .extend (true, object1, object2) untuk penggabungan yang dalam. Value true menunjukkan menggabungkan dua objek secara rekursif, memodifikasi yang pertama.

$ extended (true, target, object)


9
Penanya tidak pernah menunjukkan bahwa mereka menggunakan jquery dan tampaknya meminta solusi javascript asli.
Teh JoE

Ini adalah cara yang sangat sederhana untuk melakukan ini dan berhasil. Solusi yang layak yang akan saya pertimbangkan jika saya yang mengajukan pertanyaan ini. :)
kashiraja

Ini adalah jawaban yang sangat bagus tetapi tidak memiliki tautan ke kode sumber ke jQuery. jQuery memiliki banyak orang yang bekerja pada proyek ini dan mereka telah menghabiskan beberapa waktu agar penyalinan dalam berfungsi dengan baik. Juga, kode sumbernya cukup "sederhana": github.com/jquery/jquery/blob/master/src/core.js#L125 "Sederhana" dalam tanda kutip karena mulai menjadi rumit ketika menggali jQuery.isPlainObject(). Itu memperlihatkan kompleksitas menentukan apakah sesuatu itu objek biasa atau tidak, yang sebagian besar jawaban di sini terlewatkan begitu saja. Tebak bahasa apa jQuery ditulis?
CubicleSoft

4

Di sini lurus ke depan, solusi sederhana yang berfungsi seperti Object.assignhanya deeep, dan bekerja untuk sebuah array, tanpa modifikasi apa pun

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

Contoh

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

Saya mengalami masalah ini saat memuat kondisi redux yang di-cache. Jika saya hanya memuat keadaan cache, saya akan mengalami kesalahan untuk versi aplikasi baru dengan struktur keadaan yang diperbarui.

Sudah disebutkan, bahwa lodash menawarkan mergefungsi, yang saya gunakan:

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

3

Banyak jawaban menggunakan puluhan baris kode, atau perlu menambahkan perpustakaan baru ke proyek, tetapi jika Anda menggunakan rekursi, ini hanya 4 baris kode.

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

Penanganan array: Versi di atas menimpa nilai array lama dengan yang baru. Jika Anda ingin menyimpan nilai array lama dan menambahkan yang baru, cukup tambahkan else if (current[key] instanceof Array && updates[key] instanceof Array) current[key] = current[key].concat(updates[key])blok di atas elsestatament dan Anda siap.


1
Saya suka tetapi perlu cek sederhana yang tidak terdefinisi untuk 'saat ini' atau {foo: undefined} tidak bergabung. Tambahkan saja if (current) sebelum for for.
Andreas Pardeike

Terima kasih atas sarannya
Vincent

2

Ini satu lagi yang baru saja saya tulis yang mendukung array. Itu menyadarkan mereka.

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

Gunakan fungsi ini:

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 yang merupakan pustaka fungsi javascript yang bagus telah mergeDeepLeft dan mergeDeepRight. Semua ini bekerja dengan baik untuk masalah ini. Silakan lihat dokumentasi di sini: https://ramdajs.com/docs/#mergeDeepLeft

Untuk contoh spesifik yang dimaksud, kita dapat menggunakan:

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

Tes unit:

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

Saya hanya menemukan solusi 2 baris untuk mendapatkan penggabungan dalam javascript. Biarkan saya tahu cara kerjanya bagi Anda.

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)

Objek temp akan mencetak {a: {b: 'd', e: 'f', x: 'y'}}


1
Ini tidak melakukan merger mendalam yang sebenarnya. Itu akan gagal dengan merge({x:{y:{z:1}}}, {x:{y:{w:2}}}). Il juga akan gagal memperbarui nilai yang ada di obj1 jika obj2 memilikinya juga, misalnya dengan merge({x:{y:1}}, {x:{y:2}}).
Oreilles

1

Kadang-kadang Anda tidak perlu penggabungan dalam, bahkan jika Anda berpikir demikian. Sebagai contoh, jika Anda memiliki konfigurasi default dengan objek bersarang dan Anda ingin memperluasnya dengan konfigurasi Anda sendiri, Anda bisa membuat kelas untuk itu. Konsepnya sangat sederhana:

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

Anda dapat mengonversinya menjadi fungsi (bukan konstruktor).


1

Ini adalah penggabungan murah yang menggunakan kode sesedikit yang saya bisa pikirkan. Setiap sumber menimpa properti sebelumnya ketika ada.

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

Saya menggunakan fungsi pendek berikut untuk objek penggabungan dalam.
Ini sangat bagus untuk saya.
Penulis sepenuhnya menjelaskan cara kerjanya di sini.

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

};

Meskipun tautan ini dapat menjawab pertanyaan, lebih baik untuk memasukkan bagian-bagian penting dari jawaban di sini dan memberikan tautan untuk referensi. Jawaban hanya tautan dapat menjadi tidak valid jika halaman tertaut berubah. - Dari Ulasan
Chris Camaratta

Hai @ ChrisCamaratta. Tidak hanya bagian penting di sini, itu semua ada di sini - fungsi dan cara menggunakannya. Jadi ini jelas bukan hanya tautan saja jawabannya. Ini adalah fungsi yang saya gunakan untuk menggabungkan objek. Tautan hanya jika Anda ingin penjelasan penulis tentang cara kerjanya. Saya merasa akan merugikan komunitas untuk mencoba dan menjelaskan cara kerjanya lebih baik daripada penulis yang mengajar JavaScript. Terima kasih atas komentarnya.
John Shearing

Hah. Entah saya melewatkannya atau kode tidak muncul di antarmuka resensi ketika saya memeriksanya. Saya setuju ini adalah jawaban yang berkualitas. Akan muncul pengulas lain mengesampingkan penilaian awal saya jadi saya pikir Anda baik-baik saja. Maaf untuk bendera inspirasi.
Chris Camaratta

Bagus! @ ChrisCamaratta, Terima kasih telah membantu saya memahami apa yang terjadi.
John Shearing
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.