[javascript] 얕은 병합 대신 딥 병합하는 방법?

Object.assignObject spread는 모두 얕은 병합 만 수행합니다.

문제의 예 :

// 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를 완전히 덮어 씁니다. 와 동일합니다 Object.assign().

이 방법이 있습니까?



답변

ES6 / ES7 사양에 딥 병합이 있는지 아는 사람이 있습니까?

아니 그렇지 않아.


답변

나는 이것이 오래된 문제라는 것을 알고 있지만 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] } } }

아래 답변에서 불변 버전을 찾을 수 있습니다.

이로 인해 순환 참조에서 무한 재귀가 발생합니다. 이 문제에 직면했다고 생각되면 순환 참조를 감지하는 방법에 대한 훌륭한 답변이 있습니다.


답변

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


답변

호스트 객체 또는 값의 백보다 복잡한 모든 종류의 객체에 관해서는 문제가 아닙니다.

  • getter를 호출하여 값을 얻거나 특성 설명자를 복사합니까?
  • 병합 대상에 세터 (자체 속성 또는 프로토 타입 체인)가있는 경우 어떻게해야합니까? 값을 이미 존재하는 것으로 간주하거나 현재 값을 업데이트하기 위해 세터를 호출합니까?
  • 자체 속성 함수를 호출하거나 복사합니까? 정의 시점에 스코프 체인의 무언가에 따라 함수 또는 화살표 함수에 바인딩 된 경우 어떻게합니까?
  • DOM 노드와 같은 것이라면? 당신은 확실히 그것을 간단한 객체로 취급하고 싶지 않고 모든 속성을 깊이 병합합니다.
  • 배열, 맵 또는 세트와 같은 “간단한”구조를 다루는 방법? 이미 존재한다고 생각하거나 병합합니까?
  • 열거 불가능한 자체 속성을 처리하는 방법은 무엇입니까?
  • 새로운 서브 트리는 어떻습니까? 참조 또는 딥 클론으로 간단히 할당 하시겠습니까?
  • 냉동 / 밀봉 / 비 확장 물체를 다루는 방법?

명심해야 할 또 다른 사항은 사이클을 포함하는 객체 그래프입니다. 일반적으로 다루기가 어렵지 않습니다. 단순히Set 이미 방문한 소스 객체 않지만 종종 잊혀집니다.

아마도 기본 값과 간단한 객체 만 기대하는 심층 병합 함수를 작성해야합니다. 구조적 클론 알고리즘이 처리 할 수 병합 소스로 합니다. 처리 할 수 ​​없거나 심층 병합 대신 참조로 할당 할 수있는 항목이있는 경우 발생합니다.

다시 말해, 한 가지 크기에 맞는 알고리즘이 없기 때문에 자신의 롤을 사용하거나 사용 사례를 다루는 라이브러리 방법을 찾아야합니다.


답변

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


답변

이 문제는 여전히 진행 중이므로 다른 접근 방식이 있습니다.

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


답변

나는 이미 많은 답변이 있으며 많은 의견이 효과가 없다고 주장합니다. 유일한 합의는 너무 복잡해서 아무도 표준을 만들지 않았다는 것입니다 입니다. 그러나 SO에서 허용되는 대부분의 답변은 널리 사용되는 “간단한 트릭”을 나타냅니다. 따라서 전문가는 아니지만 자바 스크립트의 복잡성에 대해 조금 더 이해하여 더 안전한 코드를 작성하려는 저와 같은 우리 모두에게 약간의 빛을 비추려고 노력할 것입니다.

손이 더러워지기 전에 2 가지 점을 명확히하겠습니다.

  • [부인] 난 우리가 어떻게 태클 아래의 기능 제안 깊은 루프자바 스크립트 객체 사본을 일반적으로 너무 짧게 논평 무엇인지 설명한다. 생산 준비가되어 있지 않습니다. 명확성을 기하기 위해 원형 객체 (세트 또는 충돌하지 않는 심볼 속성으로 추적) , 참조 값 또는 딥 클론 복사 , 불변의 대상 객체 (다시 복제?), 사례 별 연구 와 같은 다른 고려 사항을 의도적 으로 제외했습니다. 객체의 각 유형은 얻을 / 세트 속성을 통해 접근 … 또한, 나는 그 중 하나를 여기에 포인트 아니기 때문에이 중요 – 야 – 비록 테스트 성능하지 않았다.
  • 내가 사용합니다 복사 또는 할당 대신 조건을 병합 . 내 마음에 합병 은 보수적이며 갈등에 실패해야하기 때문입니다. 여기서 충돌 할 때 소스가 대상을 덮어 쓰기를 원합니다. 그렇습니다 Object.assign.

답변 for..in또는 Object.keys오해의 소지가있는

딥 카피를 만드는 것은 매우 기본적이고 일반적인 관행으로 보이므로 간단한 재귀를 통해 하나의 라이너 또는 적어도 빠른 승리를 기대할 수 있습니다. 우리는 라이브러리가 필요하거나 100 줄의 사용자 정의 함수를 작성해야한다고 기대하지 않습니다.

Salakar의 답변을 처음 읽었을 때 , 나는 더 좋고 간단하게 할 수 있다고 생각했습니다 ( Object.assignon 과 비교할 수 있음 x={a:1}, y={a:{b:1}}). 그런 다음 나는 8472의 대답을 읽었고 생각했습니다 … 너무 쉽게 벗어날 수는 없으며 이미 주어진 대답을 개선해도 멀지 않습니다.

딥 카피와 재귀를 즉시 보자. 사람들이 속성을 구문 분석하여 매우 간단한 객체를 복사하는 방법을 고려하십시오.

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에서 객체는 자체 속성과 (부모) 프로토 타입 객체로 구성됩니다. 프로토 타입 객체는 자체 속성과 프로토 타입 객체로 구성됩니다. 그리고 프로토 타입 체인을 정의합니다.

속성은 키 ( string또는 symbol)와 설명자 ( value또는 get/ set접근 자 및 같은 속성 ) 쌍입니다 enumerable.

마지막으로 많은 유형의 객체가 있습니다. 객체 날짜 또는 객체 함수와 객체 객체를 다르게 처리 할 수 ​​있습니다.

따라서 딥 카피를 작성하면 최소한 다음 질문에 대답해야합니다.

  1. 딥 (재귀 적 조회에 적합) 또는 평평하다고 생각하는 것은 무엇입니까?
  2. 어떤 속성을 복사하고 싶습니까? (열거 가능 / 열거 불가능, 문자열 키 / 기호 키, 자체 속성 / 프로토 타입 자체 속성, 값 / 설명자 …)

예를 들어, 다른 생성자에 의해 생성 된 다른 객체는 심도있는 모양에 적합하지 않을 수 있기 때문에 object Objects 만 이라고 생각합니다 . 이 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};

제안 된 기능

이 플 런커 에서 테스트 할 수 있습니다 .

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