데이터 타입의 종류
- 기본형(Primitive Type) : 값이 담긴 주소값을 바로 복제
- Number, String, Boolean, null, undefiend, Symbol
- 참조형(Reference Type) : 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주솟값을 복제
- Object
- Array, Function, Date, RegExp, Map, WeakMap, Set, WeakSet
- Object
데이터 타입에 관한 배경지식
메모리와 데이터
C/C++, JAVA 등의 정적 타입 언어는 메모리 낭비를 최소화하기 위해 데이터 타입별로 할당할 메모리 영역을 2바이트, 4바이트 등으로 나누어 정해놓았습니다. (ex. short = 2바이트)
하지만 자바스크립트는 메모리 용량이 과거보다 월등히 커진 상황에서 등장했기 때문에 메모리 공간을 좀 더 넉넉하게 할당하여 Number의 경우 정수형인지 부동소수형인지 구분하지 않고 8바이트를 확보합니다.
각 비트는 고유한 식별자(identifier)를 통해 메모리 상의 위치를 확인할 수 있습니다. 바이트 역시 비트의 식별자로 위치를 파악할 수 있습니다. 자바스크립트에서 모든 데이터는 바이트 단위의 식별자, 메모리 주솟값(memory address)을 통해 서로 구분하고 연결할 수 있습니다.
식별자와 변수
- 변수(variable) = 변할 수 있는 데이터
- 식별자(identifier) = 데이터를 식별하는 데 사용하는 이름, 변수명
변수 선언과 데이터 할당
변수 선언
var a;
변수란 변경 가능한 데이터가 담길 수 있는 공간 또는 그릇
- 위의 명령을 받은 컴퓨터는 메모리에서 비어있는 공간 하나를 확보하고, 이 공간의 이름(identifier)을 a 라고 지정합니다.
- 이후 사용자가 a에 접근하고자 하면 컴퓨터는 메모리에서 a라는 이름을 가진 주소를 검색해 해당 공간에 담긴 데이터를 반환할 것입니다.
데이터 할당
var a;
a = 'abc';
var a = 'abc';
- 선언과 할당을 나누어 명령하든, 한 문장으로 명령하든 자바스크립트 엔진은 결국 같은 동작을 수행합니다.
- a라는 이름을 가진 주소를 검색해서 그곳에 문자열 ‘abc’ 할당? → NO
- 데이터를 저장하기 위한 별도의 메모리 공간을 다시 확보해서 문자열 ‘abc’를 저장하고, 그 주소를 변수 영역에 저장 (변수 영역이란 말은 공식 명칭 아님)
💡 다시 정리하면
1. 변수 영역에서 빈 공간을 확보한다.
2. 확보한 공간의 식별자를 a로 지정한다.
3. 데이터 영역의 빈 공간에 문자열 ‘abc’를 저장한다.
4. 변수 영역에서 a라는 식별자를 검색한다
5. 앞서 저장한 문자열의 주소를 a 공간에 대입한다.
이는 데이터 변환을 자유롭게 할 수 있게 함과 동시에 메모리를 더욱 효율적으로 관리하기 위함입니다.
자바스크립트에서 문자열은 특별히 정해진 규격이 없습니다. 만약 미리 확보한 공간 내에서만 데이터 변환을 할 수 있다면 변환한 데이터를 다시 저장하기 위해서는 ‘확보된 공간을 변환된 데이터 크기에 맞게 늘리는 작업’이 선행되어야 합니다. 그런데 만약 해당 공간이 메모리의 중간에 있다면 해당 공간보다 뒤에 저장된 데이터들을 전부 뒤로 옮기고, 이동시킨 주소를 각 식별자에 다시 연결하는 작업을 해야 하므로 컴퓨터가 처리해야 할 연산이 많아집니다. 따라서 효율적으로 문자열 데이터의 변환을 처리하기 위해 변수와 데이터를 별도의 공간에 나누어 저장하는 것입니다.
🤔 그렇다면 데이터 영역에서 데이터 변환을 하려면 어떻게 할까요?
문자열 ‘abc’의 마지막에 ‘def’를 추가한다고 하면 컴퓨터는 ‘abcdef’라는 문자열을 새로 만들어 별도의 공간에 저장하고 그 주소를 변수 공간에 연결합니다. 기존의 ‘abc’ 데이터는 자신의 주소를 저장하는 변수가 하나도 없으면 가비지 컬렉터(garbage collector)의 수거 대상이 됩니다.
이러한 방법은 또한 중복된 데이터에 대한 처리 효율을 높일 수 있습니다.
500개의 변수를 생성해서 모든 변수에 숫자 5를 할당하는 경우를 생각해봅시다. 5를 별도의 공간에 한 번만 저장하고 해당 주소만 변수 공간에 저장한다면 훨씬 적은 메모리를 차지할 것입니다.
기본형 데이터와 참조형 데이터
불변값
불변값 !== 상수
변수와 상수를 구분 짓는 변경 가능성의 대상은 변수 영역 메모리이지만, 불변성 여부를 구분할 때의 변경 가능성 대상은 데이터 영역 메모리입니다.
기본형 데이터인 숫자, 문자열, boolean, null, undefiend, Symbol은 모두 불변값입니다. 앞선 파트에서 보았듯이 문자열 ‘abc’가 할당된 변수 a에 ‘abcdef’를 재할당하는 경우, ‘abcdef’라는 새로운 문자열을 만들어 데이터 공간에 저장하고 그 주소를 변수 a에 저장합니다. 변경은 새로 만드는 동작을 통해서만 이뤄집니다. 한 번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않습니다.
var b = 5;
var c = 5;
b = 7;
첫 번째 줄에서, 컴퓨터는 먼저 데이터 영역에서 5를 찾고 없으면 그제서야 데이터 공간을 새로 만들어 5를 저장하고 그 주소를 b에 저장합니다. 다음 줄에서는 같은 수인 5를 할당하려고 하는데, 컴퓨터는 이전에 이미 만들어 놓은 데이터 공간의 주소를 재활용합니다. 다음 줄에서는 기존에 저장된 5 자체를 7로 바꾸는 것이 아니라 기존에 저장했던 7을 찾아서 있으면 재활용하고, 없으면 새로 만들어서 b에 저장합니다.
가변값
참조형 데이터는 모두 가변값? NO!
Object.defineProperty
, Object.freeze
등 설정에 따라 변경 불가능한 경우도 있고 아예 불변값으로 활용하는 방안도 있습니다.
var obj1 = {
a: 1,
b: 'bbb',
}
참조형 데이터는 객체의 변수 영역이 별도로 존재.
- 컴퓨터는 변수 영역의 빈공간을 확보하고 해당 공간의 주소의 이름을 obj1로 지정
- 데이터가 여러 개의 프로퍼티로 이루어져 있으므로 (object) 그룹 내부 프로퍼티를 저장하기 위해 별도의 변수 영역을 다시 확보하고 그 영역의 주소를 obj1에 저장.
- 프로퍼티를 저장하기 위해 확보한 변수 영역에 각각 a, b라는 이름을 지정
- 데이터 영역에 이미 존재하는 값인 경우 해당 주소를 저장하고 아닌 경우 새로 할당해서 그 주소를 a, b에 저장
객체가 별도로 할애한 영역은 변수 영역일 뿐 ‘데이터 영역’은 기존의 메모리 공간(불변값에서와 동일)을 그대로 활용하고 있습니다. 데이터 영역에 저장된 값은 모두 불변값이지만, 변수에는 다른 값을 얼마든지 대입할 수 있는 것입니다. 이 맥락에서 참조형 데이터는 가변값입니다.
참조형 데이터의 값을 변경하는 예제를 살펴봅시다.
var obj1 = {
a: 1,
b: 'bbb',
};
obj1.a = 2;
먼저 컴퓨터는 데이터 영역에서 2를 검색합니다. 만약 검색 결과가 없다면 빈 공간을 확보해서 2를 저장하고 해당 주소를 a 이름을 가진 주소의 데이터에 저장합니다.
이때 중요한 점은 변수 obj1이 바라보고 있는 주소는 변하지 않는다는 것입니다. 즉 ‘새로운 객체’가 만들어진 것이 아니라 기존의 객체 내부의 값만 바뀐 것입니다.
중첩 객체
객체 안에 또 다른 객체가 들어 있는 경우의 예제를 살펴봅시다.
var obj1 = {
x: 3,
arr: [1, 2, 3],
};
기본 객체를 생성하는 경우와 동일하게 진행하다가, arr 이름을 가진 주소에 다시 내부 프로퍼티 ([1, 2, 3])를 저장하기 위한 변수 영역을 확보하고 그 주소를 arr 주소의 데이터로 저장합니다. 내부 프로퍼티를 저장하기 위해 확보한 변수 영역에 인덱스 부여(0, 1, 2) 하고 해당 변수 영역의 데이터값에는 데이터 영역의 주소 저장합니다.
만약 obj1.arr[1] 을 찾는다면 컴퓨터는 obj1부터 쭉 타고타고 내려오면서 값을 찾아나갈 것입니다.
obj.arr = 'str'
위와 같이 값을 변경하는 경우에는 ‘str’을 데이터 영역에 저장하고 그 주소를 arr이름을 가진 주소의 데이터에 저장합니다.
이때, 기존의 arr값을 저장하기 위해 확보했던 변수 영역(인덱스 0, 1, 2) 메모리는 참조 카운트가 0이 되고, 해당 변수 영역이 저장했던 데이터 영역(1, 2) 또한 참조 카운트가 0이 되어 가비지 컬렉터의 수거 대상이 됩니다.
💡 참조 카운트: 어떤 데이터에 대해 자신의 주소를 참조하는 변수의 개수
- 참조 카운트가 0인 메모리 주소는 가비지 컬렉터의 수거 대상이 된다. 가비지 컬렉터는 런타임 환경에 따라 특정 시점이나 메모리 사용량이 포화 상태에 임박할 때마다 자동으로 수거 대상들을 수거하고, 수거된 메모리는 다시 새로운 값을 할당할 수 있는 빈 공간이 된다.
- 참조 카운트는 연쇄적으로 사라짐.
변수 복사 비교
var a = 10;
var b = a;
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
b = 15;
obj2.c = 20;
변수를 복사하는 과정은 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보게 되는 점에서 동일하지만, 데이터 할당 과정에서 이미 차이가 있기 때문에 변수 복사 이후의 동작에도 큰 차이가 발생합니다.
7번째 줄의 실행 결과 변수 a와 b는 서로 다른 주소를 바라보게 되지만, 8번째 줄의 실행결과에서 변수 obj1와 obj2는 여전히 같은 객체를 바라보고 있는 상태입니다.
a !== b
obj1 === obj2
이 결과가 바로 기본형과 참조형 데이터의 가장 큰 차이점입니다.
대부분의 자바스크립트 책에서 ‘기본형은 값을 복사하고 참조형은 주솟값을 복사한다’고 설명하고 있지만, 사실은 어떤 데이터 타입이든 변수에 할당하기 위해서는 주솟값을 복사해야 하기 때문에, 엄밀히 말하면 자바스크립트의 모든 데이터 타입은 참조형 데이터일 수밖에 없습니다. 다만 기본형은 주솟값을 복사하는 과정이 한 번만 이루어지고, 참조형은 한 단계를 더 거치게 된다는 차이가 있는 것입니다.
앞서 살펴 본 예제에서 객체 내부 프로퍼티가 아닌 변수의 값을 직접 변경하는 경우에는 어떻게 동작할까요?
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
obj2 = { c: 20, d: 'ddd' };
위의 경우에는 obj2에 새로운 객체를 할당함으로써 값을 직접 변경했기 때문에 메모리의 데이터 영역의 새로운 공간에 새 객체가 저장되고 그 주소를 변수 영역의 obj2 위치에 저장합니다. 객체에 대한 변경임에도 obj1과 obj2가 다른 객체를 바라보게 되는 것입니다.
즉, 참조형 데이터가 ‘가변값’이라고 설명할 때의 ‘가변’은 참조형 데이터 자체를 변경할 경우가 아니라 그 내부의 프로퍼티를 변경할 때만 성립합니다.
불변 객체
불변 객체를 만드는 간단한 방법
앞서 참조형 데이터의 ‘가변’은 데이터 자체가 아닌 내부 프로퍼티를 변경할 때만 성립한다고 말했습니다. 데이터 자체를 변경하고자 하면(새로운 데이터를 할당하고자 하면) 기본형 데이터와 마찬가지로 기존 데이터는 변하지 않습니다.
따라서 내부 프로퍼티를 변경할 필요가 있을 때마다 매번 새로운 객체를 만들어 재할당하기로 규칙을 정하거나 자동으로 새로운 객체를 만드는 도구를 활용한다면 객체 역시 불변성을 확보할 수 있을 것입니다.
불변 객체가 필요한 경우?
값으로 전달받은 객체에 변경을 가하더라도 원본 객체는 변하지 않아야 하는 경우가 종종 발생합니다.
var user = {
name: 'Suji',
gender: 'female'
};
var changeName = function (user, newName) {
var newUser = user;
newUser.name = newName;
return newUser;
};
var user2 = changeName(user, 'Yoon');
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // Yoon Yoon
console.log(user === user2); // true
var user = {
name: 'Suji',
gender: 'female'
};
var changeName = function (user, newName) {
return { // 새로운 객체 생성
name: newName,
gender: user.gender
};
};
var user2 = changeName(user, 'Yoon');
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.'); // 유저 정보가 변경되었습니다.
}
첫 번째 예제의 경우, 객체를 복사해서 내부의 프로퍼티를 변경하기 때문에 user와 user2가 같은 객체를 바라봅니다. 하지만 두 번째 예제의 경우 changeName 함수가 새로운 객체를 반환하도록 수정했기 때문에 user와 user2는 서로 다른 객체를 바라보게 됩니다.
하지만 변경할 필요가 없는 기존 객체의 프로퍼티를 하드코딩으로 입력해야 하기 때문에 변경해야 할 정보가 많은 경우 비효율적인 코드입니다. 따라서 아래와 같이 대상 객체의 프로퍼티 개수에 상관 없이 모든 프로퍼티를 복사하는 함수를 사용하는 편이 더 좋습니다.
var copyObject = function (target) {
var result = {};
for (var prop in target) {
result[prop] = target[prop];
}
return result;
}
copyObject는 for in 문법을 이용해 result 객체에 target 객체의 프로퍼티들을 복사하는 함수입니다.
프로토타입 체이닝 상의 모든 프로퍼티를 복사하는 점, getter/setter는 복사하지 않는 점, 얕은 복사만을 수행한다는 점 등이 아쉬운 부분이지만 이 문제를 모두 보완한다면 함수가 무거워질 수밖에 없습니다.
var user = {
name: 'Suji',
gender: 'female'
};
var user2 = copyObject(user);
user2.name = 'Yoon';
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.'); // 유저 정보가 변경되었습니다.
}
이제 모든 개발자들이 user 객체 내부의 변경이 필요할 때는 무조건 copyObject 함수를 사용하기로 합의하고 그 규칙을 지킨다는 전제하에서 user 객체는 곧 불변 객체라고 볼 수 있습니다.
하지만 그보다 시스템적으로 제약을 거는 편이 더 안전한데 이때 immutable.js, baobab.js 등의 라이브러리를 사용할 수 있습니다. 해당 라이브러리는 라이브러리 자체에서 불변성을 지닌 별도의 데이터 타입과 그에 따른 메소드 제공합니다.
얕은 복사와 깊은 복사
얕은 복사(shallow copy)는 바로 아래 단계의 값만 복사하는 방법, 깊은 복사(deep copy)는 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법입니다.
얕은 복사의 경우 중첩된 객체에서 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주솟값만 복사한다는 의미입니다. 해당 프로퍼티에 대해 원본과 사본이 모두 동일한 참조형 데이터의 주소를 가리킵니다(기존 데이터를 그대로 참조).
어떤 객체 내부의 모든 값을 복사해서 완전히 새로운 데이터를 만들고자 할 때, 객체의 프로퍼티 중에서 그 값이 기본형 데이터일 경우에는 그대로 복사하면 되지만 참조형 데이터는 다시 그 내부의 프로퍼티들을 복사해야 합니다. 이 과정을 참조형 데이터가 있을 때마다 재귀적으로 수행해야만 비로소 깊은 복사가 되는 것입니다.
var copyObjectDeep = function(target) {
var result = {};
if (typeof target === 'object' && target !== null) {
for (var prop in target) {
result[prop] = copyObjectDeep(target[prop]);
}
} else {
result = target;
}
return result;
}
copyObjectDeep 함수는 target이 객체인 경우에는 내부 프로퍼티들을 순회하며 copyObjectDeep 함수를 재귀적으로 호출하는 함수입니다. typeof 명령어가 null에 대해서도 ‘object’를 반환하는 자바스크립트 자체 버그 때문에 target ≠= null 조건을 추가했습니다.
간단하게 깊은 복사를 처리할 수 있는 또 다른 방법으로 객체를 JSON 문법으로 표현된 문자열로 전환했다가 다시 JSON 객체로 바꾸는 방법이 있습니다. 다만 메소드나 getter/setter 등 JSON으로 변경할 수 없는 프로퍼티는 무시하기 때문에 httpRequest로 받은 데이터를 저장한 객체를 복사할 때 등 순수한 정보를 다룰 때 활용하기 좋은 방법입니다.
var copyObjectViaJSON = function(target) {
return JSON.parse(JSON.stringify(target));
};
추가로 hasOwnProperty 메소드를 활용해 프로토타입 체이닝을 통해 상속된 프로퍼티를 복사하지 않게끔 할 수도 있습니다. ES5의 getter/setter를 복사하는 방법은 안타깝게도 ES6의 Object.getOwnPropertyDescriptor 또는 ES2017의 Object.getOwnPropertyDescriptors 외에는 마땅한 방법이 없습니다.
undefined 와 null
undefined
- 사용자가 명시적으로 지정
- 값이 존재하지 않을 때 자바스크립트 엔진이 자동으로 부여하는 경우
- 객체 내부의 존재하지 않는 프로퍼티에 접근하려고 할 때 (1)
- return 문이 없거나 호출되지 않는 함수의 실행 결과 (2)
- 값을 대입하지 않은 변수, 즉 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때 (3)
var a;
console.log(a); // (1) undefiend, 값을 대입하지 않은 변수에 접근
var obj = { a: 1 };
console.log(obj.a); // 1
console.log(obj.b); // (2) undefiend, 존재하지 않는 프로퍼티에 접근
console.log(b) // c.f) ReferenceError: b is not defined
var func = function() { };
var c = func(); // (3) 반환(return) 값이 없으면 undefined를 반환한 것으로 간주
console.log(c); // undefined
그런데 (1)의 경우가 배열의 경우에는 조금 특이한 동작을 확인할 수 있습니다.
var arr1 = [];
arr1.length = 3;
console.log(arr1); // [empty x 3]
var arr2 = new Array(3);
console.log(arr2); // [empty x 3]
var arr3 = [undefined, undefined, undefined];
console.log(arr3); // [undefined, undefined, undefined]
[empty x 3]은 배열에 3개의 빈 요소를 확보했지만 확보된 각 요소에는 문자 그대로 어떤 값도, 심지어 undefined 조차도 할당돼 있지 않음을 의미합니다.
이처럼 비어있는 요소와 undefined를 할당한 요소는 출력 결과부터 다릅니다. 비어있는 요소는 순회와 관련된 많은 배열 메소드들의 순회 대상에서 제외됩니다.
var arr1 = [undefined, 1];
var arr2 = [];
arr2[1] = 1; // arr2 = [empty, 1]
arr1.forEach(function (v, i) { console.log(v, i); }); // undefiend 0 / 1 1
arr2.forEach(function (v, i) { console.log(v, i); }); // 1 1
arr1.map(function (v, i) { return v + i; }); // [NaN, 2]
arr2.map(function (v, i) { return v + i; }); // [empty, 2]
arr1.filter(function (v) { return !v; }); // [undefined]
arr2.filter(function (v) { return !v; }); // []
arr1.reduce(function (p, c, i) { return p + c + i; }, ''); // 'undefined011'
arr2.reduce(function (p, c, i) { return p + c + i; }, ''); // 11
arr2에 대한 결과를 보면 각 메소드들이 비어 있는 요소에 대해서는 어떠한 처리도 하지 않고 건너뛰었음을 알 수 있습니다.
배열은 무조건 length 프로퍼티의 개수만큼 빈 공간을 확보하고 각 공간에 인덱스를 이름으로 지정할 것이라고 생각하기 쉽지만, 실제로는 객체와 마찬가지로 특정 인덱스에 값을 지정할 때 비로소 빈 공간을 확보하고, 인덱스를 이름으로 지정하고 데이터의 주소값을 저장하는 등의 동작을 합니다. 즉, 값이 지정되지 않은 인덱스는 ‘아직은 존재하지 않는 프로퍼티’에 지나지 않는 것입니다.
사용자가 명시적으로 부여한 undefined는 그 자체로 하나의 값으로 동작하기 때문에 이때의 프로퍼티나 배열의 요소는 고유의 키값(프로퍼티 이름)이 실존하게 되고, 따라서 순회의 대상이 될 수 있습니다. 한편 자바스크립트 엔진이 반환하는 undefined는 해당 프로퍼티 내지 배열의 키 값(인덱스) 자체가 존재하지 않음을 의미합니다. 값으로써 어딘가에 할당된 undefined는 실존하는 데이터인 반면, 자바스크립트 엔진이 반환해주는 undefined는 문자 그대로 값이 없음을 나타내는 것입니다.
💡 TC39의 ECMAScript 명세서에서는 다음과 같이 설명한다.
”var 변수는 environmentRecord가 인스턴스화될 때 생성되면서 undefined로 초기화됩니다.”
한편 ES6에서 등장한 let, const에 대해서는 undefined를 할당하지 않은 채로 초기화를 마치며, 이후 실제 변수가 평가되기 전까지는 해당 변수에 접근할 수 없다. 명세서에서는 ”let 과 const 변수는 environmentRecord가 인스턴스화될 때 생성되지만 실제 변수가 평가되기 전까지는 접근할 수 없다”고 설명한다.
그렇다면 두 경우의 혼돈을 피하기 위해서는 직접 undefined를 할당하지 않으면 됩니다. ‘비어있음’을 명시적으로 나타내고 싶을 때는 undefined가 아닌 null을 사용하도록 합니다. 이런 규칙을 따른다면 undefined는 오직 ‘값을 대입하지 않은 변수에 접근하고자 할 때 자바스크립트 엔진이 반환해주는 값’ 으로서만 존재할 수 있습니다.
null
var n = null;
console.log(typeof n); // object
console.log(n == undefined); // true
console.log(n == null); // true
console.log(n === undefined); // false
console.log(n === null); // true
자바스크립트는 자체 버그로 typeof null을 object로 판단하니 주의해야 합니다.
동등 연산자(==)로 비교할 경우 null과 undefined는 같다고 판단되기 때문에 어떤 변수가 실제로 null인지 undefined인지는 동등 연산자가 아닌 일치 연산자(===)를 써야만 정확히 판별할 수 있습니다.
정리
- 자바스크립트 데이터 타입: 기본형과 참조형, 일반적으로 기본형은 불변값 참조형은 가변값
- 변수: 변경 가능한 데이터가 담길 수 있는 공간
- 식별자: 그 변수의 이름
- 변수를 선언하면 컴퓨터는
- 1. 메모리의 빈 공간에 식별자를 저장하고, 그 공간에 자동으로 undefined 할당
- 2. 이후 그 변수에 기본형 데이터를 할당하려고 하면 별도의 공간에 데이터를 저장하고 그 공간의 주소를 변수의 값 영역에 할당
- 3. 참조형 데이터를 할당하고자 하는 경우에는 참조형 데이터 내부 프로퍼티들을 위한 변수 영역을 별도로 확보해서 확보된 주소를 변수에 연결
- 4. 다시 앞서 확보한 변수 영역에 각 프로퍼티의 식별자를 저장하고, 데이터를 별도의 공간에 저장해서 그 주소를 식별자들과 매칭시킴
- 참조형 데이터를 불변값으로 사용하기 위해서는 내부 프로퍼티들을 일일이 복사(깊은 복사)하거나 라이브러리를 사용하는 방법이 있음
- ‘없음’을 나타내는 값은 undefined와 null 두 가지가 있음.
- undefined는 어떤 변수에 값이 존재하지 않을 경우를 의미, null은 사용자가 명시적으로 ‘없음’을 표현하기 위해 대입한 값 → 사용자가 명시적으로 undefined를 대입하는 것은 지양