1. Shallow Copy(얕은 복사)와 Deep Copy(깊은 복사)이해
- 얕은 복사는 객체의 참조 값(주소 값)을 복사하고, 깊은 복사는 객체의 실제 값을 복사한다.
자바스크립트에서 값은 원시값과 참조값 두 가지 데이터 타입의 값이 존재한다.
- 원시타입의 값은 새로운 메모리 공간에 독립적인 값을 저장하기 때문에 깊은 복사가 된다.
- 참조타입의 값은 얕은 복사가 된다.
- 두 타입의 큰 차이점은 원본이 바뀌면 참조 타입은 같이 변경되지만, 원시 타입은 변경되지 않는다.
// 원시 타입의 깊은 복사
let a = '원본 데이터';
let b = a;
a = '수정 데이터';
console.log(a); // '수정 데이터'
console.log(b); // '원본 데이터'
- 원시 타입은 복사 시 값 자체를 담은 독립적인 메모리를 생성하기 때문에 a가 재할당 되더라도 b에 아무런 영향을 주지 않는다.
// 참조 타입의 얕은 복사
let a = {name:'원본 데이터'};
let b = a;
a.name = '수정 데이터';
console.log(a); // '수정 데이터'
console.log(b); // '수정 데이터'
- 새로운 값으로 변수 값을 재할당 하자 변수값도 같이 변경되는것을 확인할 수 있다. 즉, 데이터가 그대로 하나 더 생성된 것이 아닌 해당 데이터의 메모리 주소를 전달하게 돼서, 결국 한 데이터를 공유하게 되는 것이다.
1 - 1. 얕은복사(Shallow Copy)
- 객체를 복사 할 때, 해당 객체만 복사하여 새 객체를 생한다.
- 복사된 객체의 인스턴스 변수는 원본 객체의 인스턴스 변수와 같은 메모리 주소를 참조한다.
- 따라서, 해당 메모리 주소의 값이 변경되면 원본 객체 및 복사 객체의 인스턴스 변수 값은 같이 변경된다.
1) Array.prototype.slice()
- 얕은 복사의 한 예이다. start부터 end 인덱스까지 기존 배열에서 추출하여 새로운 배열을 리턴하는 메소드. 만약 start,end 설정을 하지 않는다면, 기존 배열을 전체 얕은 복사한다.
const original = ['a',2,true,4,"hi"];
const copy = original.slice();
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
copy.push(10);
console.log(JSON.stringify(original) === JSON.stringify(copy)); // false
console.log(original); // [ 'a', 2, true, 4, 'hi' ]
console.log(copy); // [ 'a', 2, true, 4, 'hi', 10 ]
- 기존 배열에는 영향을 끼치지 않아서 깊은 복사로 보일 수 있지만, 원시값을 저장한 1차원 배열일 뿐이다. 원시값은 기본적으로 깊은 복사이다.
const original = [
[1, 1, 1, 1],
[0, 0, 0, 0],
[2, 2, 2, 2],
[3, 3, 3, 3],
];
const copy = original.slice();
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
// 복사된 배열에만 변경과 추가.
copy[0][0] = 99;
copy[2].push(98);
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
console.log(original);
// [ [ 99, 1, 1, 1 ], [ 0, 0, 0, 0 ], [ 2, 2, 2, 2, 98 ], [ 3, 3, 3, 3 ] ]출력
console.log(copy);
// [ [ 99, 1, 1, 1 ], [ 0, 0, 0, 0 ], [ 2, 2, 2, 2, 98 ], [ 3, 3, 3, 3 ] ]출력
- 만약 1차원 배열이 아닌 중첩 구조를 갖는 2차원 배열이면 얕은 복사를 수행한다.
const original = [
{
a: 1,
b: 2,
},
true,
];
const copy = original.slice();
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
// 복사된 배열에만 변경.
copy[0].a = 99;
copy[1] = false;
console.log(JSON.stringify(original) === JSON.stringify(copy)); // false
console.log(original);
// [ { a: 99, b: 2 }, true ]
console.log(copy);
// [ { a: 99, b: 2 }, false ]
- 이 경우는 배열 안에 객체를 수정하고자 할 경우 얕은 복사를 수행하게 된다. 하지만, 원시값은 기본적으로 깊은 복사라서 기존 변수에 있는 값과는 다른 값을 도출하는것을 볼 수 있다.
2 ) Spread 연산자(전개 연산자)
const obj = {
a: 1,
b: {
c: 2,
},
};
const newObj = { ...obj };
newObj.b.c = 3;
console.log(obj); // { a: 1, b: { c: 3 } }
console.log(obj.b.c === newObj.b.c); // true
- 위 예제를 보면 Spread연산자도 얕은 복사를 하는 것을 볼 수 있다.
1 - 2 깊은복사(Deep Copy)
- 깊은 복사를 하는 목적은 기존 객체의 값만 복사본으로 가져와 별로도 활용하기 위함이다.
1 ) JSON.parse & JSON.stringify
- JSON.stringify()는 객체를 json 문자열로 변환하는데 이 과정에서 원본 객체와의 참조가 모두 끊어진다.
- 객체를 json 문자열로 반환 후, JSON.parse()를 이용해 다시 원래 객체로 만들어준다.
const obj = {
a: 1,
b: {
c: 2,
},
};
const newObj = JSON.parse(JSON.stringify(obj));
newObj.b.c = 3;
console.log(obj); // { a: 1, b: { c: 2 } }
console.log(obj.b.c === newObj.b.c); // false
- 이 방법은 간단하고 쉽지만, 다른 방법에 비해 느리고 객체가 function 일 경우, undefined로 처리한다는것이 단점이다.
function deepCopy(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
let copy = {};
for (let key in obj) {
copy[key] = deepCopy(obj[key]);
}
return copy;
}
const obj = {
a: 1,
b: {
c: 2,
},
func: function () {
return this.a;
},
};
const newObj = deepCopy(obj);
newObj.b.c = 3;
console.log(obj); // { a: 1, b: { c: 2 }, func: [Function: func] }
console.log(obj.b.c === newObj.b.c); // false
- 이 문제를 해결하기 위해서는 깊은 복사를 구현하는 커스텀 재귀 함수를 사용하는 것이다. 하지만, 복잡하다.
마무리
=> 얕은 복사와 깊은 복사는 글로만 봤을때는 이해가 잘 되지도 않고, 그게 그거 인것처럼 보인다. 이것들은 다른 곳에서도 많이 활용 되므로, 많은 예제들을 참고해서 공부할 필요가 있어 보인다....ㅠㅠ