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


339

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().

이 방법이 있습니까?


하나의 객체에서 다른 객체로 속성을 복사하는 것과 깊게 병합됩니까?

2
아니요, 개체 속성을 덮어 쓰지 않아야하므로 대상의 각 자식 개체가 이미 존재하는 경우 같은 자식으로 병합되어야합니다.
Mike

ES6이 완성되었으며 AFAIK라는 새로운 기능이 더 이상 추가되지 않습니다.
kangax


1
@Oriol은 jQuery가 필요하지만 ...
m0meni

답변:


331

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

아니 그렇지 않아.


21
편집 기록을 검토하십시오. 내가 대답했을 때 문제는 ES6 / ES7 사양에 깊은 병합이 있는지 아는 사람이 있습니까? .

37
이 답변은 더 이상이 질문에 적용되지 않습니다-업데이트되거나 삭제되어야합니다
DonVaughn

13
질문은이 정도로 편집되어서는 안됩니다. 편집은 명확하게하기위한 것입니다. 새로운 질문이 게시되었습니다.
CJ 톰슨

171

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

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

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


1
객체 그래프에 무한 재귀로 이어질 사이클이 포함되어있는 경우
the8472

item !== null조건의 시작 부분에서 이미 진실성을 확인 isObject했으므로 내부 에 필요하지 않아야합니다.item
mcont

2
왜 이것을 쓰십시오 : Object.assign(target, { [key]: {} })단순히 될 수 있다면 target[key] = {}?
르그 레니

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

3
에서 일반 이외의 개체는 지원하지 않습니다 target. 예를 들어, mergeDeep({a: 3}, {a: {b: 4}})증강 Number물체 가 생길 수 있으며 이는 분명히 바람직하지 않습니다. 또한 isObject배열 Date은받지 않지만 딥 카피해서는 안되는 다른 기본 객체 유형은 허용합니다 .
riv December

122

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
안녕하세요, 가장 단순하고 아름다운 솔루션입니다. Lodash 끝내, 그들은 핵심 JS 객체로서 포함되어야한다
Nurbol Alpysbayev

11
결과가 아니어야합니까 { 'a': [{ 'b': 2 }, { 'c': 3 }, { 'd': 4 }, { 'e': 5 }] }?
J. Hesters

좋은 질문. Lodash 관리자에게는 별도의 질문이 될 수도 있습니다.
AndrewHenderson

7
{ 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }배열의 요소를 병합하기 때문에 결과 가 정확합니다. 요소 0object.a되고 {b: 2}, 소자 0의은 other.a이다 {c: 3}. 이 둘이 동일한 배열 인덱스를 가지고 있기 때문에 병합되면 결과는입니다 { 'b': 2, 'c': 3 }. 이는 0새 객체 의 요소 입니다.
Alexandru Furculita

나는 이것을 선호 합니다 .6 배 더 작습니다.
솔로

101

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

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

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

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

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


2
안전한 "문서 상태"전송을 구현하지 않는 V8 개발자를위한 변명
neaumusic

당신은 많은 좋은 문제를 제기하고 당신의 추천의 구현을보고 싶어요. 그래서 아래에서 하나 만들려고했습니다. 모양과 의견을 말씀해 주시겠습니까? stackoverflow.com/a/48579540/8122487
RaphaMex

66

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

1
@torazaburo isObject 함수에 대한 저의 이전 게시물보기
Salakar

그것을 업데이트했습니다. 몇 가지 테스트 후 깊이 중첩 된 개체에 버그가 있음을 발견했습니다
CpILL

3
계산 된 속성 이름입니다. 첫 번째는 key속성 이름으로 값을 사용 하고 나중에 속성 키를 "키"로 만듭니다. 참조 : es6-features.org/#ComputedPropertyNames
CpILL에게

2
isObject당신은 확인하지 않아도 && item !== null, 말과 라인 시작하기 때문에 item &&아니?
ephemer

2
소스가 대상보다 깊게 중첩 된 하위 객체를 가지고 있다면 해당 객체는 여전히 mergedDeep출력 에서 동일한 값을 참조합니다 (제 생각에). 예 : 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 이것이 문제입니까? 입력을 변경하지는 않지만 이후의 입력에 대한 변경은 출력을 변경시킬 수 있으며, 그 반대로는 입력을 변경하는 출력을 변경할 수도 있습니다. 그러나 가치가있는 것은 람다의 R.merge()행동이 동일합니다.
James Conkling

40

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

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


이거 좋은데. 그러나 반복되는 요소가있는 배열이 있으면 연결됩니다 (반복 된 요소가 있습니다). 나는 이것을 매개 변수 (고유 한 배열 : 참 / 거짓)를 취하도록 조정했습니다.
우주 비행사

1
배열을 독특하게 만들기 위해 다음과 같이 변경할 수 있습니다 prev[key] = pVal.concat(...oVal);.prev[key] = [...pVal, ...oVal].filter((element, index, array) => array.indexOf(element) === index);
Richard Herries

1
너무 좋고 깨끗합니다! 확실히 최고의 답변입니다!
538ROMEO

거룩한. 이것은 배열이 병합되어 내가 찾고있는 것을 보여줍니다.
Tschallacka

그러나 @CplLL 솔루션은 불변이라고 말하지만 사용 reduce 하지 않는 동안 함수 내에서 실제 객체 변경 가능성을 사용 합니다.
Augustin Riedinger

30

나는 이미 많은 답변이 있으며 많은 의견이 효과가 없다고 주장합니다. 유일한 합의는 너무 복잡해서 아무도 표준을 만들지 않았다는 것입니다 입니다. 그러나 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 } }

13

나는 lodash를 사용합니다.

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

2
객체를 변경하지 않는 것을 원하는 경우 병합은 객체를 변경합니다. _cloneDeep(value1).merge(value2)
geckos

3
@geckos 당신은 _.merge ({}, value1, value2)를 할 수 있습니다
Spenhouet

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

9

다음은 객체 및 배열과 함께 작동하는 또 다른 ES6 솔루션입니다.

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
이 테스트 및 / 또는 라이브러리의 일부입니까, 멋지지만 다소 입증 된 것을 확인하고 싶습니다.


8

매우 간단한 ES5 대안을 제시하고 싶습니다. -이 기능은이 개 매개 변수를 얻을 수 targetsource그 유형 "개체"이어야합니다. 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이 없다면sourcetarget 그것을 얻는다;
  • 경우 target가지고 source속성을하고 targetsource두 객체 (4 만점에 3 건)하지 않은, target의 속성을 오버라이드 (override)됩니다;
  • 만약 target가지고 source건물 둘 다 객체 / 배열 (1 남은 경우), 다음 재귀 (또는 2 개 어레이 연결) 두 객체를 병합하는 일이있다;

또한 다음을 고려하십시오 .

  1. 배열 + obj = 배열
  2. obj + 배열 = obj
  3. obj + obj = obj (재귀 적으로 병합)
  4. 배열 + 배열 = 배열 ​​(연결)

예측 가능하고 기본 유형과 배열 및 객체를 지원합니다. 또한 2 개의 객체를 병합 할 수 있으므로 reduce 기능을 통해 2 개 이상을 병합 할 수 있다고 생각 합니다.

예를 살펴보십시오 (원한다면 원한다면 놀아보십시오) .

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

브라우저의 호출 스택 길이에는 제한이 있습니다. 현대의 브라우저는 정말로 깊은 수준의 재귀에서 오류를 발생시킵니다 (수천 개의 중첩 된 호출을 생각하십시오). 또한 새로운 조건과 유형 검사를 추가하여 원하는대로 배열 + 객체와 같은 상황을 자유롭게 처리 할 수 ​​있습니다.



7

이 방법이 있습니까?

npm 라이브러리 를 솔루션으로 사용할 수 있다면 객체 병합 고급 기능을 사용하면 친숙한 콜백 함수를 사용하여 객체를 깊게 병합하고 모든 단일 병합 작업을 사용자 정의 / 재정의 할 수 있습니다. 이것의 주된 아이디어는 단지 깊이 병합하는 것 이상입니다. 두 키가 동일 할 때 값은 어떻게됩니까 ? 이 라이브러리는 두 키가 충돌 할 때 object-merge-advanced유형을 측정하여 병합 후 가능한 많은 데이터를 유지하는 것을 목표로합니다.

가능한 많은 데이터를 유지하기 위해 계량 키 값 유형을 병합하는 객체 키

첫 번째 입력 인수 키는 # 1, 두 번째 인수-# 2로 표시됩니다. 각 유형에 따라 결과 키 값으로 하나가 선택됩니다. 다이어그램에서 "개체" 는 일반 개체를 의미 합니다 (배열이 아님)를 .

키가 충돌하지 않으면 모두 결과에 입력됩니다.

object-merge-advanced코드 스 니펫을 병합 하는 데 사용한 예제 스 니펫에서 :

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

알고리즘은 모든 입력 객체 키를 재귀 적으로 순회하고 새로운 병합 결과를 비교 및 ​​빌드하고 반환합니다.


6

다음 함수는 객체의 딥 카피를 생성합니다. 프리미티브, 배열 및 객체 복사를 포함합니다.

 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

ES5를 사용한 간단한 솔루션 (기존 값 덮어 쓰기) :

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


그냥 내가 필요한 것 - ES6 빌드에서 문제를 일으키는 -이 ES5의 대안은 폭탄이다
danday74

5

여기에있는 대부분의 예제는 너무 복잡해 보입니다. 내가 만든 TypeScript에서 하나를 사용하고 있습니다. 대부분의 경우를 다루어야한다고 생각합니다 (배열을 일반 데이터로 처리하고 교체 만합니다).

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

다음과 같은 경우를 대비하여 일반 JS에서도 마찬가지입니다.

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

사용 방법을 보여주는 테스트 사례는 다음과 같습니다.

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

일부 기능이 누락되었다고 생각되면 알려주십시오.


5

당신이 필요없이 하나의 라이너를 원한다면 거대한 lodash 같은 라이브러리를 내가 사용에 당신을 제안 deepmerge . ( npm install deepmerge)

그런 다음 할 수 있습니다

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

얻을

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

좋은 점은 TypeScript에 대한 타이핑이 즉시 제공된다는 것입니다. 또한 배열 을 병합 할 있습니다. 이것이 진정한 만능 솔루션입니다.


4

심층 병합을 위해 $ .extend (true, object1, object2) 를 사용할 수 있습니다 . true 값 은 두 객체를 재귀 적으로 병합하여 첫 번째 객체를 수정 함을 나타냅니다.

$ extend (true, target, object)


9
asker는 jquery를 사용하고 있으며 네이티브 자바 스크립트 솔루션을 요구하는 것으로 보지 않았습니다.
Teh JoE

이것은 이것을 수행하는 매우 간단한 방법이며 작동합니다. 내가이 질문을하는 사람이라면 고려할만한 해결책입니다. :)
kashiraja

이것은 좋은 대답이지만 jQuery에 대한 소스 코드에 대한 링크가 없습니다. jQuery에는 많은 사람들이 프로젝트를 진행하고 있으며 깊은 복사 작업을 제대로 수행하는 데 시간을 보냈습니다. 또한 소스 코드는 상당히 "간단합니다": github.com/jquery/jquery/blob/master/src/core.js#L125 "간단한"은을 파고들 때 복잡해지기 때문에 따옴표로 묶습니다 jQuery.isPlainObject(). 그것은 무언가가 평범한 물체인지 아닌지를 결정하는 복잡성을 드러냅니다. 여기서 대부분의 대답은 오랫동안 잃었습니다. jQuery가 어떤 언어로 작성되어 있는지 생각하십니까?
CubicleSoft

4

여기서는 간단하고 간단하게 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)
// x will be
x ==  {
  "a": {
    "a": 1,
    "b": 1
  },
  "b": [    1,    2,    null,    4  ],
  "c": 3
}


3

캐시 된 redux 상태를로드 할 때이 문제가 발생했습니다. 캐시 된 상태 만로드하면 상태 구조가 업데이트 된 새 앱 버전에 오류가 발생합니다.

lodash는 이미 언급 한 merge기능을 제공한다고 언급했습니다 .

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

3

많은 답변이 수십 줄의 코드를 사용하거나 프로젝트에 새 라이브러리를 추가해야하지만 재귀를 사용하는 경우 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])하려면 elseStatament 위에 블록을 추가하기 만하면 됩니다.


1
마음에 들지만 'current'에 대한 간단한 정의되지 않은 검사가 필요합니다. 그렇지 않으면 {foo : undefined}가 병합되지 않습니다. for 루프 앞에 if (현재)를 추가하십시오.
Andreas Pardeike

제안에 감사드립니다
Vincent

2

다음은 방금 작성한 배열을 지원하는 또 다른 것입니다. 그것들을 연결합니다.

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

이 기능을 사용하십시오 :

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에는 mergeDeepLeft 및 mergeDeepRight가 있습니다. 이들 중 어느 것도이 문제에 대해 잘 작동합니다. https://ramdajs.com/docs/#mergeDeepLeft 에서 설명서를 살펴보십시오.

문제의 특정 예를 위해 다음을 사용할 수 있습니다.

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

단위 테스트 :

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

Javascript에서 심층 병합을위한 2 줄 솔루션 만 찾았습니다. 이것이 어떻게 작동하는지 알려주세요.

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)

임시 객체는 {a : {b : 'd', e : 'f', x : 'y'}}를 인쇄합니다.}}


1
이것은 실제 깊은 병합을 수행하지 않습니다. 로 실패합니다 merge({x:{y:{z:1}}}, {x:{y:{w:2}}}). 또한 obj2에 값이 있으면 obj1의 기존 값도 업데이트하지 못합니다 (예 :) merge({x:{y:1}}, {x:{y:2}}).
Oreilles

1

때로는 그렇게 생각하더라도 깊은 병합이 필요하지 않습니다. 예를 들어 중첩 된 객체가 포함 된 기본 구성이 있고 자체 구성으로 깊이 확장하려는 경우 해당 클래스를 만들 수 있습니다. 개념은 매우 간단합니다.

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

생성자가 아닌 함수로 변환 할 수 있습니다.


1

이것은 내가 생각할 수있는만큼 작은 코드를 사용하는 저렴한 심층 병합입니다. 각 소스는 이전 특성이 존재할 때 겹쳐 씁니다.

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

객체를 깊이 병합하기 위해 다음과 같은 짧은 기능을 사용하고 있습니다.
그것은 나를 위해 잘 작동합니다.
저자는 여기서 어떻게 작동하는지 완전히 설명합니다.

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

};

이 링크가 질문에 대한 답변을 제공 할 수 있지만 여기에 답변의 필수 부분을 포함시키고 참조 용 링크를 제공하는 것이 좋습니다. 링크 된 페이지가 변경되면 링크 전용 답변이 유효하지 않을 수 있습니다. - 리뷰에서
Chris Camaratta

@ChrisCamaratta 님 안녕하세요. 여기서 필수적인 부분 일뿐만 아니라 모든 기능과 사용법이 여기에 있습니다. 따라서 이것은 링크 전용 답변이 아닙니다. 이것은 객체를 깊게 병합하는 데 사용했던 기능입니다. 링크는 작성자가 작동 방식에 대한 설명을 원하는 경우에만 해당됩니다. JavaScript를 가르치는 저자보다 작업 내용을 더 잘 설명하고 설명하는 것이 커뮤니티의 장애라고 생각합니다. 의견 주셔서 감사합니다.
John Shearing

허. 검토하지 않았거나 검토 할 때 코드가 검토 자 인터페이스에 나타나지 않았습니다. 나는 이것이 품질 답변이라는 데 동의합니다. 다른 검토자가 내 초기 평가를 무시한 것처럼 보이므로 괜찮습니다. 영감 깃발이 유감입니다.
Chris Camaratta '12

큰! @ChrisCamaratta, 무슨 일이 있었는지 이해하도록 도와 주셔서 감사합니다.
John Shearing
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.