shallow copy , deep copy

Creating the dots·2021년 7월 4일
0

Javascript

목록 보기
15/24

shallow copy

반복문을 통해 객체의 값을 복사해온다. 만약 객체의 값이 객체인 경우, 객체의 주소를 복사한다.

이전에 원시자료형과 참조자료형을 공부하면서, 원시자료형은 값이 복사되고, 참조자료형은 주소가 복사된다는 것을 배웠다.

//원본이 원시자료형인 경우
let primitive = 10;
let copiedPri = primitive;

copiedPri = 20; //copiedPri에 20 재할당
copiedPri; //20
primitive; //10 

//원본이 참조자료형인 경우
const arr = [1,2,3];
const copiedArr = arr; //arr의 주소가 복사된다.

copiedArr.push('추가');
copiedArr; //[1,2,3,'추가']
arr; //[1,2,3,'추가']

그래서, 참조자료형의 '값'만 복사하는 방법들도 배웠는데,
slice(), assign(대상, 출처), spread 연산자 등을 통해 값만 복사해올 수 있었다.

Array.prototype.slice

const arr = [1,2,3];
const copiedArr = arr.slice();

copiedArr.push('추가');
copiedArr; //[1,2,3,'추가']
arr; //[1,2,3]

slice 메소드를 사용한 경우, '값'이 복사되어 온 것을 확인할 수 있다. 따라서 복사본에 엘리먼트를 추가, 삭제하더라도 원본에 영향을 끼치지 않는다.

그러나, 다음과 같은 경우는 복사본을 변경할 때, 원본도 변경된다.

const arr = [1,2,3,[4]];
const copiedArr = arr.slice();

copiedArr[3].push('추가');
copiedArr; //[1,2,3,[4,'추가']]
arr; //[1,2,3,[4,'추가']]

위의 상황에서 확인된 것처럼 Array.prototype.slice는 원본이 중첩구조(배열 안에 배열이 있는 구조)인 경우, 중첩된 엘리먼트는 '값'으로 복사되는 것이 아니라 주소가 복사된다. 즉, slice는 얕은 복사(shallow copy)를 수행하므로 모든 값을 독립적으로 복사할 수 없는 것이다.

spread operator

ES6 문법으로 추가된 spread operator를 사용하면 비교적 간단하게 복사할 수 있다.

spread operator가 호출되면, 내부적으로는 iterator-looping을 수행한다. 어떤 객체에 대해 반복문을 수행할 수 있다는 것은 해당 객체에 [Symbol.iterator]프로퍼티가 존재한다는 것을 의미한다. 즉, 어떤 객체에 [Symbol.iterator]프로퍼티를 갖고 있다면 그 객체는 반복문을 실행할 수 있다.

const a = [1,2,3];
const b = [...a];
b; //[1,2,3]

//내면에서 일어나는 과정
if(![Typeof(a) is Iterable)){
  throw TypeError
}

const b = [];
for(let i=0;i<a.length;i++){
  b.push(b[i]);
}

객체에 [Symbol.iterator]프로퍼티가 있다는 사실이 확인되면, for 반복문을 실행해 요소를 하나씩 옮겨담는 것이다. 옮겨담는 요소의 깊이는 1-dpt이다.

따라서 slice와 spread operator 모두 내부적으로 반복문을 실행하여 엘리먼트를 하나씩 복사해오는데 배열 또는 객체가 엘리먼트로 들어있는 경우 해당 엘리먼트가 가리키는 주소를 복사해오는 것이다.

Object.assign

//Copy
const obj = {a:1};
const copy = Object.assign({},obj);

console.log(copy); //{a:1}
console.log(obj===copy); //false

//Merge -기존 객체에 합치기
const obj1 = {a:1};
const obj2 = {b:2};
const obj3 = {c:3};

const merge1 = Obejct.assign(obj1,obj2,obj3);

console.log(merge1); //{a:1, b:2, c:3}
console.log(obj1); //{a:1, b:2, c:3}

//Merge -빈 객체에 합치기
const obj4 = {d:4};
const obj5 = {e:5};
const obj6 = {f:6};

const merge2 = Object.assign({},obj4,obj5,obj6);

console.log(merge2); //{d:4, e:5, f:6}
console.log(obj4); //{d:4}

//Nested Object가 있는 객체 복사하기
const original = {
  name: 'kim',
  age: 20,
  city: 'seoul',
  todos: ['study','exercise','clean','wash']
}
const copied = Object.assign({},original);

copied.todos.push('추가');
console.log(copied.todos); //['study','exercise','clean','wash', '추가']
console.log(original.todos); //['study','exercise','clean','wash', '추가']

Object.assign(대상, 출처) 역시 마찬가지로 속성의 값을 복사하기 때문에 출처객체의 키값이 객체에 대한 참조인 경우(객체 또는 배열인 경우), 참조를 복사하게 된다.

다시 말해, slice, spread syntax, Object.assign 모두 shallow copy를 수행하는 것이다.

deep copy

shallow copy에서 Nested object에 대해 참조를 복사한 것과는 다르게, 새로운 메모리 공간을 확보해 복사본에 Nested object가 새로운 참조를 갖게 된다.

그렇다면 키값이 참조인 경우, 참조를 복사할 수밖에는 없는걸까? 그렇지 않다. 제한적이지만 가장 쉬운 방법이 바로 JSON객체를 사용하는 것이다.

JSON.parse & JSON.stringify

JSON.stringify를 사용해서 배열을 문자열로 변경했고, 다시 JSON.parse를 사용해 객체로 바꾸었다.

여기서 문자열로 바꾸는 이유는, 자바스크립트에서 문자열은 다른 언어와는 다르게 immutable한 속성을 갖는 원시자료형(primitive data type)이므로 참조가 저장되는 것이 아니라 그 값 자체가 저장되기 때문이다.이후에 다시 배열로 만들면, nested object도 새로운 참조를 갖게 되는 것이다. 배열을 순간 얼렸다가(문자열) 다시 녹인다(배열)고 생각하면 쉬울 것 같다.

따라서 위와 같은 과정을 통해 배열을 복사해온 경우, 복사본에 변경해도 원본이 바뀌지 않는 것을 확인할 수 있다.

과정을 하나씩 정리해보자면,

  1. stack에 stringified 이름표가 붙은 공간에 "[1,2,[3,4]]"가 할당된다.
  2. stack에 parsed 이름표가 붙은 공간에 객체에 대한 참조가 저장된다. 여기서 참조는 기존 arr의 참조와 다른 것이다.
  3. 따라서 arr과 parsed는 서로 다른 주소를 참조한다.

단, 이와 같은 방법은 JSON 객체의 stringify 메소드는 function의 경우 undefined로 처리하는 등 JSON 객체에서 사용가능한 자료형이 제한적이라는 단점이 있다. 따라서 jquery, loadsh등의 다른 방법을 이용할 수 있다.

참고자료:깊은 복사와 얕은 복사에 대한 심도있는 이야기
Javascript: shallow and deep copy

profile
어제보다 나은 오늘을 만드는 중

0개의 댓글