ค้นหาเว็บไซต์

javascript - วิธีการรวมแบบลึกแทนการรวมแบบตื้น?


ทั้ง Object.assign และการกระจาย Object ทำเพียงการรวมแบบตื้นเท่านั้น

ตัวอย่างของปัญหา:

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

ผลลัพธ์เป็นไปตามที่คุณคาดหวัง อย่างไรก็ตามถ้าฉันลองทำแบบนี้:

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

แทนที่จะ

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

คุณได้รับ

{ a: { b: 1 } }

x ถูกเขียนทับอย่างสมบูรณ์เพราะไวยากรณ์ spread ทำงานเพียงระดับเดียว สิ่งนี้เหมือนกับ Object.assign()

มีวิธีทำแบบนี้ไหม?

คำตอบ #1

ฉันรู้ว่านี่เป็นปัญหาเก่าอยู่เล็กน้อย แต่วิธีแก้ปัญหาที่ง่ายที่สุดใน ES2015/ES6 ที่ฉันคิดออกนั้นจริง ๆ แล้วค่อนข้างง่าย โดยใช้ Object.assign()

หวังว่านี่จะช่วยได้:

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

ตัวอย่างการใช้งาน:

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

คุณจะพบเวอร์ชันที่ไม่สามารถเปลี่ยนแปลงได้ของสิ่งนี้ในคำตอบด้านล่าง

โปรดทราบว่าสิ่งนี้จะนำไปสู่การวนซ้ำไม่สิ้นสุดในกรณีที่อ้างอิงแบบวงกลม มีคำตอบที่ดีมากในที่นี้เกี่ยวกับวิธีตรวจจับการอ้างอิงแบบวงกลม หากคุณคิดว่าคุณอาจเจอปัญหานี้

คำตอบ #2

คุณสามารถใช้ Lodash merge:

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

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

console.log(_.merge(object, other));
// => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>

คำตอบ #3

ปัญหาจะไม่ใช่เรื่องง่ายเมื่อเกี่ยวข้องกับอ็อบเจ็กต์โฮสต์หรืออ็อบเจ็กต์ประเภทใดก็ตามที่ซับซ้อนกว่าการเป็นเพียงถุงของค่า

  • คุณเรียก getter เพื่อรับค่าหรือคุณก็คัดลอกตัวบ่งชี้คุณสมบัติไป?
  • ถ้าเป้าหมายของการรวมมีตัวตั้งค่า (ไม่ว่าจะเป็นคุณสมบัติของตัวเองหรืออยู่ในโพรโทไทป์เชน) จะพิจารณาค่าดังกล่าวว่าเป็นค่าที่มีอยู่แล้วหรือเรียกตัวตั้งค่าเพื่ออัปเดตค่าปัจจุบัน?
  • คุณเรียกใช้ฟังก์ชันของตัวเองหรือคัดลอกพวกมันไป? ถ้าพวกมันเป็นฟังก์ชันที่ถูกผูกไว้หรือฟังก์ชันลูกศรที่ขึ้นอยู่กับบางสิ่งในห่วงโซ่สโคปของพวกมันในช่วงเวลาที่ถูกกำหนดล่ะ?
  • ถ้ามันเป็นบางอย่างเหมือนกับโหนด DOM ล่ะ? คุณคงไม่อยากจัดการมันเหมือนกับวัตถุธรรมดาแล้วแค่รวมคุณสมบัติทั้งหมดของมันเข้าด้วยกัน
  • จะจัดการกับโครงสร้าง 'เรียบง่าย' อย่างอาร์เรย์หรือแมปหรือเซ็ตอย่างไร? จะถือว่ามีอยู่แล้วหรือรวมเข้าด้วยกันด้วย?
  • วิธีจัดการกับคุณสมบัติของตนเองที่ไม่สามารถนับได้?
  • แล้วเกี่ยวกับต้นไม้ย่อยใหม่ล่ะ? แค่กำหนดโดยการอ้างอิงหรือโคลนลึก?
  • จะจัดการกับวัตถุที่ถูกแช่แข็ง/ปิดผนึก/ไม่สามารถขยายได้อย่างไร?

อีกสิ่งหนึ่งที่ควรคำนึงถึง: กราฟวัตถุที่มีวงจร โดยปกติแล้วไม่ยากที่จะจัดการ - เพียงเก็บ Set ของวัตถุต้นทางที่ได้เยี่ยมชมแล้ว - แต่บ่อยครั้งก็ถูกลืม

คุณน่าจะเขียนฟังก์ชันการรวมแบบลึกที่คาดหวังเฉพาะค่าพื้นฐานและวัตถุเรียบง่าย - อย่างมากที่สุดคือตัวประเภทที่อัลกอริทึมการโคลนแบบมีโครงสร้างสามารถจัดการได้ - เป็นแหล่งรวม หากพบสิ่งที่ไม่สามารถจัดการได้ ให้ขว้างข้อผิดพลาดหรือเพียงแค่กำหนดโดยการอ้างอิงแทนการรวมแบบลึก

พูดอีกอย่างก็คือ ไม่มีอัลกอริทึมที่เหมาะกับทุกกรณี คุณต้องสร้างเองหรือหาวิธีจากไลบรารีที่ครอบคลุมกรณีการใช้งานของคุณ

คำตอบ #4

อัปเดต 2022:

ฉันสร้าง mergician ขึ้นมาเพื่อตอบสนองความต้องการผสาน/โคลนที่หลากหลายตามที่พูดคุยในความคิดเห็นและจัดการกับสถานการณ์ขั้นสูงมากขึ้น มันอิงตามแนวคิดเดียวกับคำตอบต้นฉบับของฉัน (ด้านล่าง) แต่ให้แนวทางแก้ปัญหาที่แข็งแกร่งขึ้นเมื่อจำเป็น:

ไม่เหมือนกับวิธีการพื้นฐานและยูทิลิตี้อื่น ๆ Mergician จะทำการโคลนและรวมวัตถุอย่างซื่อสัตย์โดยการจัดการค่าตัวบรรยาย ฟังก์ชันตัวเข้าถึง และคุณสมบัติของโปรโตไทป์อย่างถูกต้อง พร้อมทั้งให้ตัวเลือกขั้นสูงสำหรับการปรับแต่งกระบวนการโคลน/รวม

อย่างน่าทึ่ง Mergician มีขนาดเล็กกว่ามาก (1.5k min+gzip) เมื่อเทียบกับเครื่องมือที่คล้ายกันอย่าง lodash.merge (5.1k min+gzip)

  • GitHub: https://github.com/jhildenbiddle/mergician
  • NPM: https://www.npmjs.com/package/mergician
  • เอกสาร: https://jhildenbiddle.github.io/mergician/

คำตอบต้นฉบับ:

เนื่องจากปัญหานี้ยังคงอยู่ นี่คือวิธีการอีกอย่าง:

  • ES6/2015
  • ไม่สามารถเปลี่ยนแปลงได้ (ไม่แก้วัตถุเดิม)
  • จัดการอาเรย์ (รวมเข้าด้วยกัน)

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

คำตอบ #5

นี่คือเวอร์ชันที่ไม่สามารถเปลี่ยนแปลงได้ (ไม่แก้ไขข้อมูลนำเข้า) ของคำตอบของ @Salakar มีประโยชน์หากคุณกำลังทำสิ่งที่เกี่ยวข้องกับรูปแบบการเขียนโปรแกรมเชิงฟังก์ชัน

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

คำตอบ #6

ฉันรู้ว่ามีคำตอบมากมายแล้ว และมีความคิดเห็นจำนวนมากที่โต้แย้งว่ามันจะใช้ไม่ได้ ข้อสรุปเดียวคือ มันซับซ้อนมากจนไม่มีใครสร้างมาตรฐานสำหรับมัน อย่างไรก็ตาม คำตอบส่วนใหญ่ที่ได้รับการยอมรับใน SO เปิดเผย "เทคนิคง่าย ๆ" ที่ถูกใช้กันอย่างแพร่หลาย ดังนั้น สำหรับพวกเราทุกคนที่เหมือนฉันซึ่งไม่ใช่ผู้เชี่ยวชาญ แต่ต้องการเขียนโค้ดให้ปลอดภัยมากขึ้นโดยเข้าใจความซับซ้อนของจาวาสคริปต์มากขึ้นเล็กน้อย ฉันจะพยายามส่องแสงให้เห็นบางส่วน

ก่อนที่เราจะลงมือทำ ให้ฉันชี้แจงสองประเด็นก่อน:

  • [ข้อจำกัดความรับผิดชอบ] ฉันเสนอฟังก์ชันด้านล่างซึ่งจัดการกับวิธีที่เราทำ deep loop ในอ็อบเจ็กต์ของ JavaScript เพื่อคัดลอกและอธิบายสิ่งที่โดยทั่วไปมักถูกคอมเมนต์อย่างสั้นเกินไป ซึ่งไม่พร้อมสำหรับการใช้งานจริง เพื่อความชัดเจน ฉันได้ตั้งใจละเว้นพิจารณาอื่นๆ เช่น อ็อบเจ็กต์แบบวงกลม (ติดตามโดยชุดหรือสมบัติสัญลักษณ์ที่ไม่ซ้ำกัน), การคัดลอกค่าการอ้างอิงหรือการโคลนเชิงลึก, การสร้างอ็อบเจ็กต์เป้าหมายที่คงที่ (โคลนเชิงลึกอีกครั้ง?), การศึกษากรณีต่อกรณีของแต่ละประเภทของอ็อบเจ็กต์, การรับ/ตั้งสมบัติผ่านตัวเข้าถึง... นอกจากนี้ ฉันยังไม่ได้ทดสอบประสิทธิภาพ - แม้ว่าสำคัญ - เพราะมันไม่ใช่ประเด็นที่นี่เช่นกัน
  • ฉันจะใช้คำว่า copy หรือ assign แทน merge เพราะในความคิดของฉัน merge นั้นเป็นแบบอนุรักษ์นิยมและควรล้มเหลวเมื่อเกิดความขัดแย้ง ที่นี่ เมื่อเกิดความขัดแย้ง เราต้องการให้ต้นทางเขียนทับปลายทาง เหมือนที่ Object.assign ทำ

คำตอบที่ใช้ for..in หรือ Object.keys อาจทำให้เข้าใจผิด

การสร้างสำเนาลึกดูเหมือนเป็นเรื่องพื้นฐานและเป็นแนวปฏิบัติทั่วไปมากจนเราคาดหวังว่าจะเจอคำสั่งเพียงบรรทัดเดียว หรืออย่างน้อยก็ได้ผลลัพธ์อย่างรวดเร็วผ่านการเรียกซ้ำแบบง่าย ๆ เราไม่คาดหวังว่าจะต้องใช้ไลบรารีหรือต้องเขียนฟังก์ชันกำหนดเองที่มี 100 บรรทัด

เมื่อฉันอ่านคำตอบของ Salakar เป็นครั้งแรก ฉันคิดจริง ๆ ว่าฉันน่าจะทำได้ดีกว่าและง่ายกว่านี้ (คุณสามารถเปรียบเทียบกับ Object.assign บน x={a:1}, y={a:{b:1}}) แล้วฉันก็อ่านคำตอบของ the8472 และฉันคิดว่า... ไม่มีทางหลีกเลี่ยงได้ง่าย ๆ การปรับปรุงคำตอบที่มีอยู่แล้วจะไม่พาเราไปไกล

ลองวางเรื่องการคัดลอกเชิงลึกและการเรียกซ้ำไว้ข้างหนึ่งสักครู่ แค่พิจารณาว่าผู้คนมักจะตีความคุณสมบัติผิดพลาดอย่างไรเมื่อต้องการคัดลอกวัตถุที่เรียบง่ายมาก

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.keys จะข้ามคุณสมบัติที่เป็นของตัวเองที่ไม่สามารถนับได้, คุณสมบัติที่มีคีย์เป็นสัญลักษณ์ของตัวเอง และคุณสมบัติทั้งหมดของโปรโตไทป์ ซึ่งอาจไม่เป็นไรถ้าวัตถุของคุณไม่มีสิ่งเหล่านั้น แต่ควรจำไว้ว่า Object.assign จัดการคุณสมบัติที่สามารถนับได้และมีคีย์เป็นสัญลักษณ์ของตัวเอง ดังนั้นการคัดลอกแบบกำหนดเองของคุณจึงสูญเสียความสมบูรณ์ของมัน

for..in จะให้คุณสมบัติของตัวต้นทาง ของต้นแบบของมัน และของสายต้นแบบทั้งหมดโดยที่คุณไม่ได้ต้องการ (หรือไม่รู้ตัว) เป้าหมายของคุณอาจจะมีคุณสมบัติมากเกินไป ผสมกันระหว่างคุณสมบัติของต้นแบบและคุณสมบัติของตัวเอง

ถ้าคุณกำลังเขียนฟังก์ชันทั่วไปและคุณไม่ได้ใช้ Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames, Object.getOwnPropertySymbols หรือ Object.getPrototypeOf คุณอาจกำลังทำผิดพลาดอยู่

สิ่งที่ควรพิจารณาก่อนเขียนฟังก์ชันของคุณ

ก่อนอื่น ตรวจสอบให้แน่ใจว่าคุณเข้าใจว่าออบเจ็กต์ใน Javascript คืออะไร ใน Javascript ออบเจ็กต์ประกอบด้วยคุณสมบัติของมันเองและออบเจ็กต์โปรโตไทป์ (ต้นแบบ) ของมันเอง ออบเจ็กต์โปรโตไทป์นั้นก็ประกอบด้วยคุณสมบัติของมันเองและออบเจ็กต์โปรโตไทป์ และต่อไปเรื่อย ๆ กำหนดเป็นสายโซ่โปรโตไทป์

คุณสมบัติ (property) คือคู่ของคีย์ (string หรือ symbol) และตัวระบุลักษณะ (value หรือตัวเข้าถึง get/set และคุณลักษณะต่าง ๆ เช่น enumerable)

สุดท้ายแล้ว มีหลายประเภทของวัตถุ คุณอาจต้องการจัดการวัตถุ Object แตกต่างจากวัตถุ Date หรือวัตถุ Function

ดังนั้น การเขียนสำเนาลึกของคุณ คุณควรตอบอย่างน้อยคำถามเหล่านั้น:

  1. ฉันพิจารณาสิ่งใดว่าลึก (เหมาะสำหรับการค้นหาแบบเรียกซ้ำ) หรือแบน?
  2. ฉันต้องการคัดลอกคุณสมบัติใดบ้าง? (สามารถนับได้/นับไม่ได้, ใช้คีย์เป็นสตริง/ใช้คีย์เป็นสัญลักษณ์, คุณสมบัติของตัวเอง/คุณสมบัติของโปรโตไทป์, ค่าหรือคำอธิบาย…)

สำหรับตัวอย่างของฉัน ฉันพิจารณาว่าเฉพาะ object Object เท่านั้นที่เป็น ลึก เพราะวัตถุอื่นที่สร้างโดยตัวสร้างอื่นอาจไม่เหมาะสำหรับการดูอย่างละเอียด ปรับแต่งจาก 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);
}

และฉันสร้างออบเจ็กต์ options เพื่อเลือกสิ่งที่จะคัดลอก (เพื่อวัตถุประสงค์การสาธิต)

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

ฟังก์ชันที่เสนอ

คุณสามารถทดสอบมันใน plunker นี้ได้

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

นั่นสามารถใช้ได้แบบนี้:

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

คำตอบ #7

ถ้าคุณต้องการมีบรรทัดเดียวโดยไม่ต้องใช้ไลบรารีขนาดใหญ่เช่น lodash ผมแนะนำให้คุณใช้ deepmerge (npm install deepmerge) หรือ deepmerge-ts (npm install deepmerge-ts)

deepmerge ยังมาพร้อมกับ typings สำหรับ TypeScript และมีความเสถียรมากกว่า (เนื่องจากมันเก่ากว่า) แต่ deepmerge-ts ก็สามารถใช้กับ Deno ได้เช่นกัน และเร็วกว่าตามการออกแบบ แม้ว่าจะเขียนด้วย TypeScript ตามที่ชื่อบอก

เมื่อถูกนำเข้าแล้วคุณสามารถทำ

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

ได้รับ

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

สิ่งนี้ทำงานได้ดีเยี่ยมกับวัตถุที่ซับซ้อนและ อาเรย์ นี่คือทางออกที่ครบเครื่องจริงๆ

คำตอบ #8

ที่นี่ ตรงไปตรงมา;

วิธีแก้ปัญหาแบบง่ายที่ทำงานเหมือน Object.assign แต่ลึกกว่า และใช้งานได้กับอาเรย์ โดยไม่ต้องปรับแก้อะไรเลย

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
}

x = { a: { a: 1 }, b: [1,2] }
y = { a: { b: 1 }, b: [3] }
z = { c: 3, b: [,,,4] }
x = deepAssign(x, y, z)

console.log(JSON.stringify(x) === JSON.stringify({
  "a": {
    "a": 1,
    "b": 1
  },
  "b": [ 1, 2, null, 4 ],
  "c": 3
}))

แก้ไข: ผมตอบในที่อื่นเกี่ยวกับวิธีใหม่สำหรับการเปรียบเทียบเชิงลึกของวัตถุ 2 ตัว วิธีนั้นสามารถใช้สำหรับการรวมเชิงลึกได้ด้วย หากคุณต้องการการติดตั้ง ให้คอมเมนต์ https://stackoverflow.com/a/71177790/1919821

คำตอบ #9

หลายคำตอบใช้โค้ดหลายสิบบรรทัด หรือจำเป็นต้องเพิ่มไลบรารีใหม่เข้าไปในโปรเจกต์ แต่ถ้าคุณใช้การเรียกซ้ำ จะใช้เพียง 4 บรรทัดของโค้ดเท่านั้น

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

การจัดการอาร์เรย์: เวอร์ชันด้านบนจะเขียนทับค่าของอาร์เรย์เก่าด้วยค่าใหม่ หากคุณต้องการให้เก็บค่าของอาร์เรย์เก่าไว้และเพิ่มค่าของอาร์เรย์ใหม่เข้าไป ให้เพิ่มบล็อก else if (current[key] instanceof Array && updates[key] instanceof Array) current[key]=current[key].concat(updates[key]) ไว้เหนือคำสั่ง else และคุณก็พร้อมใช้งาน

คำตอบ #10

นี่คือการใช้งาน 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);
};

และการทดสอบหน่วย:

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

คำตอบ #11

แพ็กเกจ npm deepmerge ดูเหมือนจะเป็นไลบรารีที่ใช้กันอย่างแพร่หลายที่สุดในการแก้ปัญหานี้: https://www.npmjs.com/package/deepmerge

คำตอบ #12

ฉันอยากจะแนะนำตัวเลือก ES5 ที่ค่อนข้างเรียบง่าย ฟังก์ชันนี้รับพารามิเตอร์ 2 ตัว - target และ source ที่ต้องเป็นชนิด "object" Target จะเป็นออบเจ็กต์ผลลัพธ์ Target จะเก็บคุณสมบัติเดิมทั้งหมดไว้ แต่ค่าของคุณสมบัติอาจถูกแก้ไขได้

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

กรณี:

  • ถ้า target ไม่มีคุณสมบัติ source, target จะได้รับมัน;
  • ถ้า target มีคุณสมบัติ source และ target กับ source ไม่ใช่วัตถุทั้งคู่ (3 กรณีจาก 4) คุณสมบัติของ target จะถูกแทนที่;
  • หาก target มีคุณสมบัติ source และทั้งคู่เป็นวัตถุ/อาร์เรย์ (กรณีที่เหลือ 1 กรณี) จะเกิดการเรียกซ้ำเพื่อรวมวัตถุทั้งสอง (หรือการต่ออาร์เรย์ทั้งสอง);

โปรดพิจารณาต่อไปนี้ด้วย:

  1. อาเรย์ + อ็อบเจ็กต์=อาเรย์
  2. วัตถุ + อาเรย์=วัตถุ
  3. obj + obj=obj (รวมแบบเรียกซ้ำ)
  4. อาเรย์ + อาเรย์=อาเรย์ (ต่อกัน)

มันสามารถคาดเดาได้ รองรับประเภทข้อมูลพื้นฐานรวมถึงอาเรย์และวัตถุ นอกจากนี้เมื่อเราสามารถรวมวัตถุ 2 ตัวได้ ฉันคิดว่าเราสามารถรวมมากกว่า 2 ตัวผ่านฟังก์ชัน reduce

ลองดูตัวอย่าง (และลองปรับเล่นตามต้องการ):

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

มีข้อจำกัด - ความยาวของ call stack ของเบราว์เซอร์ เบราว์เซอร์สมัยใหม่จะเกิดข้อผิดพลาดเมื่อมีการเรียกซ้ำที่ลึกมาก (คิดถึงการเรียกซ้อนกันเป็นพันครั้ง) นอกจากนี้คุณสามารถจัดการกับสถานการณ์เช่น array + object เป็นต้น ตามที่คุณต้องการโดยการเพิ่มเงื่อนไขและการตรวจสอบประเภทใหม่

บทความที่เกี่ยวข้อง