안녕하세요, 이번 시간에는 객체의 복사에 대해서 알아보겠습니다.
일반 문자열, 숫자, 불린 같은 경우는
var string = 'hello';
var copy = string;
console.log(copy); // 'hello'
하면 바로 복사가 되는데요. 객체(배열, 일반 객체, 함수)도 마찬가지이긴 합니다만, 복사된 값을 조작할 때 차이가 있습니다. 다시 위의 예를 보면
var string = 'hello';
var copy = string;
console.log(copy); // 'hello'
copy = 'hi';
console.log(string); // 'hello'
이번엔 copy 값을 hi로 바꿔봤습니다. 기존 string 값은 변화가 없겠죠? copy에 값만 복사해 줬을 뿐 더이상 연관이 없으니까요. 객체의 경우를 볼까요?
var array = ['a', 'b', 'c'];
var ref = array;
ref[0] = 'd';
console.log(array); // ['d', 'b', 'c']
ref변수에 array 배열을 대입했는데요. ref의 첫 번째 항목을 변경했더니 array의 첫 번째 항목도 같이 변경되었습니다. 이상하죠? 이게 객체의 특징입니다. 문자열, 숫자, 불린을 제외한 객체는 다른 변수에 대입할 때 값을 복사하는 게 아니라 참조(메모리의 주소)를 복사합니다.
변수는 모두 메모리에 저장됩니다. 그리고 대입을 하면 변수의 이름은 저장된 메모리의 주소를 가리키게 됩니다. 메모리 어딘가에 ['a', 'b', 'c']
를 저장한 게 있다면 그것을 array와 ref변수 두 개가 가리키고 있는겁니다. 값은 하나인데 변수는 여러 개일 수 있는거죠. 이걸 참조라고 부릅니다. 따라서 ref변수가 바뀌면 array 값도 같이 바뀝니다. 이걸 방지하려면 메모리에 ['a', 'b', 'c']
두 개를 만들어서 따로따로 운영되게 만들어주어야 합니다.
var array = ['a', 'b', 'c'];
var copy = Array.prototype.slice.call(array);
copy[0] = 'd';
console.log(array); // ['a', 'b', 'c']
좀 독특한 방법인데요. 지난 시간에 배운 call 함수를 사용했습니다. copy(복사)라고 하며, 이제는 copy변수가 바뀌어도 array 변수에 영향을 미치지 않습니다. slice함수는 배열을 자르는 함수인데 어떻게 복사가 되냐고요? array.slice(0)
이라고 생각하면 됩니다. array를 자르는 데 0개만큼 자르니까 결국 그대로 반환하는거죠.
위의 경우는 Array.prototype.slice.call(array)
대신 array.slice(0)
을 해도 되지만, arguments 같은 것(유사배열이라서 배열의 메소드를 사용할 수 없습니다)을 복사할 때를 생각하면 Array.prototype.slice.call
로 통일하는 게 좋습니다.
copy에는 두 가지가 있습니다. shallow copy(얕은 복사)와 deep copy(깊은 복사)인데요. shallow copy는 가장 상위 객체만 새로 생성되고 내부 객체들은 참조 관계인 경우를 의미합니다. deep copy는 내부 객체까지 모두 새로 생성된 것을 의미합니다. 위에 copy 변수는 내부에 객체가 없기 때문에 shallow인지 deep인지 정하기 애매합니다.
var array = [{ name: 'a' }, { name: 'b' }, { name: 'c' }];
var shallow = Array.prototype.slice.call(array);
shallow[0].name = 'd';
shallow[1] = 'e';
console.log(array); // [{ name: 'd' }, { name: 'b' }, { name: 'c' }]
위의 예제에서는 shallow[0].name
은 array에 적용되지만, shallow[1]
은 적용되지 않습니다. 바로 이게 얕은 복사입니다. 가장 상위 객체에 직접 변경하는 것은 적용되지 않지만 내부 객체들을 참조로 이어져있기 때문에 적용되는 겁니다. 쉽게 외우시려면 가장 바깥 껍데기만 복사되는 복사가 얕은 복사라고 생각하시면 됩니다.
배열을 깊은 복사하는 방법은 아래의 객체 깊은 복사에서 함께 다룹니다.
일반 객체를 복사하는 것에도 shallow copy와 deep copy가 있습니다. shallow는 그냥 대입만 해주면 되니까 deep copy하는 방법을 살펴보겠습니다. 상속이 없는 일반 객체만 해당됩니다.
function copyObj(obj) {
var copy = {};
if (Array.isArray(obj)) {
copy = obj.slice().map((v) => {
return copyObj(v);
});
} else if (typeof obj === 'object' && obj !== null) {
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) {
copy[attr] = copyObj(obj[attr]);
}
}
} else {
copy = obj;
}
return copy;
}
var obj = { a: 1, b: 2, c: [{ d: null, e: 'f' }] };
var obj2 = copyObj(obj);
obj2.a = 3;
obj2.c[0].d = true;
console.log(obj.a); // 1
console.log(obj.c[0].d); // null
copyObj이란 함수를 새로 만들었습니다. 결과를 보면 obj과 obj2가 따로 놀죠. 여기서 for~in과 hasOwnProperty를 처음 보실 겁니다. for ~ in 은 obj안의 키를 순서대로 반복합니다. (주의할 점은 키가 숫자면 순서대로 반복되지 않는다는 겁니다. 숫자로된 키가 없을 때 사용하세요) 문제는 prototype에 있는 상속된 객체의 속성도 반복되기 때문에 obj.hasOwnProperty(keyName)
메소드로 상속되지 않은 자기의 속성만 반복되도록 제한하는 겁니다. hasOwnProperty 부분을 빼고 복사하면, 왜 그 부분이 필요한지 이유를 알 수 있을 겁니다.
함수는 복사할 때 bind를 하면 됩니다. this를 기존 함수와 같게 하면 똑같게 함수가 복사됩니다.
var func = function () {
alert('hi');
};
func2 = func.bind(this);
func2(); // 'hi'
이상으로 객체의 복사편을 마치겠습니다. 다음 시간에는 디자인 패턴에 대해서 알아보겠습니다!