데이터 타입 -Core JavaScript

자바스크립트가 데이터를 처리하는 과정을 살펴봄으로써
기본형 타입과 참조형 타입이 서로 다르게 동작하는 이유를 이해하고
이를 적절히 활용할 수 있게 되는 것을 목표로 합니다.
나아가 데이터 타입과 관련된 중요한 개념 몇 가지를 추가로 살펴봅니다.


데이터 타입의 종류

자바스크립트의 데이터 타입에는 크게 두 가지가 있습니다. 바로 기본형과 참조형 입니다.

  • 기본형(원시형, primitive type)
    number, string, boolean, null, undefined, Symbol

  • 참조형(reference type)
    object, Array, Function, Date, RegExp, Map, WeakMap, Set, WeakSet 등이 객체의 하위 분류에 속합니다.

https://tutorial.eyehunts.com/js/javascript-data-types-and-examples

일반적으로 기본형은 할당이나 연산시 복제되고 참조형은 참조된다고 알려져 있습니다.
엄밀히 말하면 둘 모두 복제를 하긴 합니다.
다만 기본형은 값이 담긴 주소값을 바로 복제하는 반면 참조형은 값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제한다는 점이 다릅니다.

  • 기본형은 불변성(immutability)을 뜁니다.
    언뜻 생각해 보면 혼란스러울 수 있습니다.

    불변성을 잘 이해하려면 메모리와 데이터에 대한 지식이 필요하고, 식별자와 변수의 개념을 구분할 수 있어야합니다.
    메모리 영역에서 자바스크립트의 데이터가 처리되는 과정을 알아봅시다.


데이터 타입에 관한 배경 지식


메모리와 데이터

컴퓨터는 모든 데이터를 0 또는 1로 기억합니다.
0 또는 1만 표현할 수 있는 하나의 메모리 조각을 비트(bit)라고 합니다. 각 비트는 고유한 식별자(unique identifier)를 통해 위치를 확인할 수 있습니다.

1byte = 8 bit 입니다.

메모리 용량이 과거보다 월등히 커진 영향으로 자바스크립트에서는 숫자의 경우 정수형인지 부동소수형인지 구분하지 않고 64비트 (8바이트)를 확보합니다.

모든 데이터는 바이트 단위의 식별자, 즉 메모리 주소값(memory address)을 통해 서로 구분하고 연결할 수 있습니다.


식별자와 변수

  • 변수는 “변할 수 있는 데이터” 입니다. (숫자,문자열,객체,배열 모두가 데이터 입니다.)

  • 식별자는 어떤 데이터를 식별하는데 사용하는 이름. 즉 변수명 입니다.


변수 선언과 데이터 할당


변수 선언

변수 선언의 동작 원리를 알아봅니다.

var a;

위 코드를 말로 해석하자면 변할 수 있는 데이터를 만들고, 식별자 이름을 a로 설정합니다.

즉, 엔진에서는 메모리에서 비어있는 공간을 확보하고 이 메모리 영역의 이름(식별자)을 a로 설정합니다.

여기까지가 변수 선언 과정입니다.
이후에 a에 접근하고자 하면 엔진은 메모리 영역에서 a라는 이름을 가진 주소를 검색해 해당 영역에 담긴 데이터를 반환합니다.


데이터 할당

var a = ‘abc’;

a라는 이름의 주소를 검색해서 그곳에 문자열 ‘abc’를 할당할 것 같지만,
실제로 해당 위치에 문자열을 직접 저장하지는 않습니다.

엔진에서는 데이터를 저장하기 위한 별도의 메모리 공간을 확보하여 문자열 ‘abc’를 저장하고,
그 주소를 변수 영역에 저장하는 식으로 이뤄집니다.
메모리 공간변수영역데이터 영역구분지어 값을 저장한다고 생각하면 됩니다.

  • 데이터 할당과 변수 선언 흐름
1
2
3
4
5
6
7
8
9
10
11
var a = 'abc';  
// 위 코드는 아래 코드와 동일 합니다.
var a;
a = 'abc';
// 즉 엔진의 흐름은
1. 변수 영역에서 빈 공간을 확보하고
2. 확보한 공간의 식별자를 a로 지정합니다.
3. 변수 영역이 아닌 데이터 영역의 빈 공간에 문자열 'abc'를 저장합니다.
4. 변수 영역에서 a 식별자를 검색합니다.
5. 데이터 영역에서 생성한 문자열의 주소를
a 식별자로 검색한 변수 영역에 대입합니다.

이러한 방식은 데이터를 변경(or 삭제/추가)등의 작업을 할때
변경된 데이터의 주소들을 다시 식별자에 연결하여 저장하지 않아도 됩니다.
(엔진이 처리할 연산을 최소한으로 해줍니다.)

효율적으로 문자열 데이터의 변환을 처리하려면 변수와 데이터를 별도의 공간에 나누어 저장하는 것이 최적입니다.

예를 들어 다수의 변수를 생성하여 모든 변수에 숫자 5를 할당한다면,
변수의 개수만큼 변수의 메모리 공간마다 5를 할당해야 합니다.
하지만 5를 별도의 공간(메모리 공간 중 변수 영역이 아닌 데이터 영역)에 한 번만 저장하고 그 주소를 연결한다면? 엔진의 중복 연산 처리 효율이 높아집니다.


기본형 데이터와 참조형 데이터


불변값

변수 와 상수의 차이는 “변경 가능성”입니다.
변수는 변할 수 있는 데이터,
상수는 바뀌지 않습니다.

“변경 가능성”의 기준은 변수 영역의 메모리 입니다.
한 번 데이터 할당이 이뤄진 변수 공간에 다른 데이터를 재할당할 수 있는지 여부가 관건입니다.

반면 상수 와 불변값을 구분짓는 “변경 가능성”의 기준은 데이터 영역 메모리 입니다.

1
2
3
4
5
6
1. var a = 'abc';  
a = a + 'def';

2. var b = 5;
var c = 5;
3. b = 7;
  1. 변수 a 에 문자열 ‘abc’를 할당했다가 아래 코드로 ‘def’를 추가하면
    기존의 ‘abc’‘abcdef’로 바뀌는 것이 아니라 새로운 문자열 ‘abcdef’를 만들어
    그 주소를 변수 a에 저장합니다. ‘abc’‘abcdef’완전히 별개의 데이터 입니다.
  1. 변수 b 에 데이터 영역을 만들어 5를 설정하고 그 주소를 b에 저장합니다.
    다음 코드에서 c에 5를 할당하려고 하면 엔진은 데이터 영역에서 5를 찾습니다.
    이미 만들어진 주소가 있으므로 그 주소를 재활용하여 c에 저장합니다.
  1. 변수 b 의 값을 7로 바꾸려고 합니다. 기존에 저장된 5를 7로 바꾸는 것이 아니라
    데이터 영역에서 7을 찾아 있으면 그 주소를 재활용하고 없다면 새로 만들어 b에 저장합니다.
    결국 5 와 7 모두 다른 값으로 변경할 수 없습니다.

이처럼 문자열 값 과 숫자 값 모두 한 번 만든 값을 바꿀 수 없고, 다른 값으로 변경할 수 없습니다. 변경은 새로 만드는 동작을 통해서만 이뤄집니다. 한 번 만들어진 값은 GC(Garbage Collection) 당하지 않는 한 영원히 변하지 않습니다. 이것이 바로 불변값의 특징입니다.

기본형(원시값, primitive)데이터인 number, string, boolean, null, undefined, Symbol은 모두 불변값입니다.


가변값

참조형(reference) 데이터의 기본적인 성질은 가변값인 경우가 많지만 설정에 따라 변경 불가능한 경우도 있고, 아예 불변값으로 활용하는 방법도 있습니다.

참조형 데이터를 변수에 할당하는 과정부터 확인해 봅니다.

참조형 데이터의 할당
1
2
3
4
var obj1 ={  
a: 1,
b: 'bbb'
};
  1. 엔진은 우선 변수 영역의 빈공간을 확보하고, 그 주소의 이름을 obj로 지정합니다.

  2. {} 내부의 프로퍼티들을 저장하기 위해 별도의 변수 영역을 마련하고, 주소 이름으로 각각 a 와 b라는 프로퍼티 이름을 지정합니다.

  3. 데이터 영역에서 숫자 1을 검색합니다. 검색 결과가 없으므로 데이터 영역에 저장하고 이 주소를 a 주소에 저장합니다. 문자열 ‘bbb’역시 데이터 영역에 저장하고 이 주소를 b 주소에 저장합니다.

기본형 데이터와의 차이는 “객체의 변수(프로퍼티) 영역”이 별도로 존재한다는 점입니다.
객체가 별도로 할애한 영역은 변수 영역일 뿐 “데이터 영역”은 기존의 메모리 공간을 그대로 활용하고 있습니다.
데이터 영역에 저장된 값은 모두 불변값입니다. 그러나 변수에는 얼마든지 다른 값을 대입할 수 있습니다.
바로 이 부분 때문에 흔히 참조형(reference)데이터는 가변값이라고 하는 것입니다.

참조형 데이터의 프로퍼티 재할당
1
2
3
4
5
var obj1 ={  
a: 1,
b: 'bbb'
};
obj.a = 2;
  • obj1a 프로퍼티에 숫자 2를 할당하려고 합니다. 데이터 영역에서 숫자 2를 검색합니다.
    존재하지 않으므로 데이터 영역을 새로만들어 2를 저장합니다. 이 주소를 프로퍼티 영역의 a 주소에 저장합니다.
    변수 obj1바라보고 있는 주소는 데이터 영역으로 변하지 않았습니다. 즉 새로운 Object가 만들어진 것이 아니라 Object 내부의 프로퍼티 영역 a의 값만 바뀐 것 입니다.

다음은 참조형 데이터의 프로퍼티에 다시 참조형 데이터를 할당하는 경우 입니다.
이러한 경우를 중첩객체(nested object)라고 합니다.

중첩된 참조형 데이터(객체)의 프로퍼티 할당
1
2
3
4
var obj = {  
x : 3,
arr : [ 3, 4, 5 ]
};
  1. 엔진은 우선 변수 영역의 빈공간을 확보하고, 그 주소의 이름을 obj로 지정합니다.
  1. 데이터 저장 공간에 데이터를 저장하려는데( {} ), 이 데이터는 여러개의 프로퍼티와 값들을 모아놓은 그룹(객체)입니다. 각 프로퍼티들을 저장하기 위해 별도의 프로퍼티 영역을 마련하고 그 주소를 {}의 주소에 저장합니다.
  1. 각 프로퍼티 영역에 xarr 를 주소 이름으로 지정합니다.
  1. 데이터 영역에서 숫자 3을 검색합니다. 없으므로 새로 만들고 이 주소를 프로퍼티 영역 x의 주소에 저장합니다.
  1. 데이터 영역에 arr 로 저장할 값은 Array로써 별도의 Array 영역을 만들어 저장하고 프로퍼티 영역 arr의 주소에 저장합니다.
  1. 배열의 요소가 총 3개 이므로 Array 영역에 공간을 확보하고 각각 인덱스를 부여하고 주소이름으로 사용합니다.
  1. 데이터 영역에서 숫자 3을 검색하여 그 주소를 인덱스 0의 주소에 저장합니다.
  1. 데이터 영역에 숫자 4가 없으므로 새로운 데이터 영역을 만들고 그 주소를 인덱스 1의 주소에 저장합니다.
  1. 데이터 영역에 숫자 5도 없으므로 생성한 주소를 인덱스 2의 주소에 저장합니다.

이제 obj.arr[1]을 검색하고자 하면 엔진의 메모리에서는 다음과 같은 과정을 거칩니다.

1. obj라는 식별자를 가진 주소를 찾습니다.
2. obj 주소에 연결된 데이터 영역의 주소로 이동합니다.
3. 데이터 영역에 연결된 프로퍼티 영역의 주소로 이동합니다.
4. 프로퍼티 영역에서 arr이라는 식별자를 가진 주소를 찾습니다.
5. arr 주소에 연결된 Array 영역의 주소로 이동합니다.
6. Array 영역에서 인덱스 1(==식별자 이름)을 검색하여 해당하는 주소로 이동합니다.
7. 해당하는 주소의 값 숫자형 데이터 4를 반환합니다.

만약 이 상태에서 다음과 같이 재할당을 하면 다음과 같은 과정을 거칩니다.

obj.arr = “str”;

데이터 영역에 문자열 “str”을 저장하고, 그 주소를 프로퍼티 영역에 저장합니다.(재할당)

그러면 기존에 있던 Array 영역의 인덱스 주소를 저장하던 데이터 영역(공간)은 자신의 주소를 참조하는 변수가 하나도 없게 됩니다. (프로퍼티 영역인 arr 주소의 값이 재할당 되므로 데이터 영역의 str을 주소로 갖음)

참조 카운트(자신을 참조하는 변수의 개수)가 0인 메모리는 garbage collector의 수거 대상이 됩니다.

즉, Array 영역의 주소를 저장하던 데이터 영역은 GC의 수거 대상이며,

GC가 처리되는 과정에서 연쇄적으로 Array 영역의 참조 카운트0이 되고 GC의 대상이되어 제거됩니다.

제거된 메모리 영역은 다시 새로운 값을 할당할 수 있는 빈 공간이 됩니다.


변수 복사 비교

변수를 복사할 때
기본형(원시값, primitive) 데이터 와 참조형(reference) 데이터의 차이

변수 복사
1
2
3
4
5
6
7
// primitive  
var a = 10;
var b = a;

// reference
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
  1. 변수 영역의 빈 공간을 확보하고 식별자를 a로 지정합니다.
    숫자 10을 데이터 영역에서 검색하여, 없으면 데이터 영역에 새로 만들어 저장하고 주소를 a의 주소에 저장합니다.

  2. 변수 영역의 빈 공간을 확보하고 식별자를 b로 지정합니다. 그 후에 변수 영역에서 a식별자를 검색해 a에 저장된 데이터 주소(10)를 b 변수 영역에 저장합니다.

  3. 변수 영역의 빈 공간을 확보하고 식별자를 obj1로 지정합니다. 데이터 영역을 새로 만들고 데이터 그룹을 담을 프로퍼티 영역을 만들어 이 주소를 데이터 영역에 저장합니다.
    각각의 프로퍼티 영역에 식별자 이름으로 c , d으로 사용하고 각각의 값 (10, ‘ddd’)을 데이터 영역에서 검색합니다. 10은 이미 있으므로 기존의 주소를 프로퍼티 영역에 연결하고, ‘ddd’는 새로 만들어 프로퍼티 영역에 연결합니다.

  4. 변수 영역의 빈 공간을 확보하고 식별자를 obj2로 지정합니다. 그 후에 변수 영역에서 식별자 obj1을 검색해 obj1에 저장된 데이터 주소를 obj2 변수 영역에 저장합니다.

변수를 복사하는 과정은 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보게 되는 점에서 동일합니다. 복사 과정은 동일하지만 데이터 할당 과정에서 이미 차이가 있기 때문에 변수 복사 이후의 동작에서 큰 차이가 발생합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 변수 복사이후 값 변경 결과 비교(1)  
// object 프로퍼티 변경 시

// primitive
var a = 10;
var b = a;

// reference
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;

// 프로퍼티 값 변경
b = 15;
obj2.c = 20;
  • b = 15 코드는 데이터 영역에 15가 없으므로 새로운 데이터 영역에 저장하고 그 주소를 변수 영역 식별자 b인 주소에 저장합니다.

  • obj.c = 20 코드는 데이터 영역에 20이 없으므르 새로운 데이터 영역에 저장하고 그 주소를 변수 영역 식별자 obj2의 주소를 찾고 이어서 obj2에 저장되있는 프로퍼티 영역의 c주소를 찾아 그 곳에 주소를 저장합니다.

기본형(원시값, primitive) 데이터를 복사한 변수b의 값을 바꾸면 데이터영역 b 주소의 값이 달라집니다.

참조형(reference) 데이터를 복사한 변수 obj2의 프로퍼티 값을 바꾸면 바라보는 obj2의 프로퍼티 주소는 달라지지 않습니다. 값만 변합니다.

즉, 변수 a와 b는 서로 다른 주소를 바라보게 됐으나, 변수 obj1 과 obj2는 여전히 같은 객체를 바라보고 있는 상태입니다. (a !== b), (obj1 === obj2)

이 결과가 바로 기본형과 참조형 데이터의 가장 큰 차이점 입니다. 대부분의 자바스크립트 책에서 ‘기본형은 값을 복사하고 참조형은 주솟값을 복사한다’고 설명하고 있지만, 사실은 어떤 데이터 타입이든 변수에 할당하기 위해서는 주솟값을 복사해야 하기 때문에 엄밀히 따지면 자바스크립트의 모든 데이터 타입은 참조형 데이터일 수 밖에 없습니다. 다만 기본형은 주솟값 복사 과정이 한 번만 이뤄지고, 참조형은 한 단계를 더 거치게 된다는 차이가 있는 것입니다.

Object 자체를 변경 시
1
2
3
4
5
6
7
8
9
10
11
12
13
// 변수 복사이후 값 변경 결과 비교(2)  
// object 변경 시

// primitive
var a = 10;
var b = a;

// reference
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;

b = 15;
obj2 = { c: 20, d: 'ddd' }; //object 자체를 변경
  • obj2에도 새로운 객체를 할당함으로써 값을 직접 변경했습니다.
    데이터 영역에 새로운 공간에 새 객체가 저장되고 그 주소를 변수 영역의 obj2 주소에 저장하게 됩니다.
    즉, 기본형 데이터의 값 변경처럼 참조형 데이터가 바라보는 주소가 달라지게 되는 것입니다.
    따라서 참조형 데이터가 “가변값”이라고 설명할 때의 “가변”은 참조형 데이터 내부의 프로퍼티 값을 변경할 때의 성질입니다. 참조형 데이터 자체를 변경하는 경우는 성립되지 않습니다.

불변 객체


불변 객체를 만드는 간단한 방법

참조형 데이터의 “가변”은 데이터 자체가 아닌 내부 프로퍼티를 변경할 때 성립되므로
내부 프로퍼티를 변경할 필요가 있을 때 마다 매번 새로운 객체를 만들어 재할당하기로 규칙을 정하거나
자동으로 새로운 객체를 만드는 도구를 활용하면 객체 역시 불변성을 확보할 수 있습니다.
대표적으로 immutable.js, immer.js, immutability-helper 등의 라이브러리가 있고,
ES6의 spread operator, Object.assign 메서드 등도 새로운 객체를 만드는 도구 목적으로 활용할 수 있습니다.

ex)값으로 전달받은 객체를 변경하더라도 원본 객체는 변하지 않게 하고 싶을 경우 불변 객체가 필요합니다.

가변성의 문제점
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var user = {  
name: 'Jaenam',
gender: 'male',
};

var changeName = function(user, newName) {
var newUser = user;
newUser.name = newName;
return newUser;
};

var user2 = changeName(user, 'Jung');

if (user !== user2) {
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // Jung Jung
console.log(user === user2); // true
  • user 객체의 name 프로퍼티를 ‘Jung’으로 바꾸는 함수를 호출해 반환 값을 user2 변수에 담습니다.
    그 결과 useruser2name 프로퍼티가 모두 ‘Jung’으로 출력되고 (user === user2)가 true로 같다는 것을 알 수 있습니다.

원본 데이터를 변경하고 싶지 않은 경우 각 변수마다 다른 객체를 바라보게 만들어야겠습니다.

서로 다른 객체를 바라보게
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var user = {  
name: 'Jaenam',
gender: 'male',
};

var changeName = function(user, newName) {
return {
name: newName,
gender: user.gender,
};
};

var user2 = changeName(user, 'Jung');

if (user !== user2) {
console.log('유저 정보가 변경되었습니다.'); // 유저 정보가 변경되었습니다.
}
console.log(user.name, user2.name); // Jaenam Jung
console.log(user === user2); // false
  • changeName() 함수가 새로운 Object를 반환하도록 한 간단한 예시 입니다.
    user 와 user2는 서로 다른 Object가 됩니다.

얕은 복사와 깊은 복사

얕은 복사(shallow copy)는 바로 아래 단계의 값만 복사하는 방법이고,
깊은 복사(deep copy)는 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법입니다.

얕은 복사는 중접된 object에서 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주솟값만 복사합니다.
그러므로 해당 프로퍼티에 대해 원본과 사본이 모두 동일한 참조형 데이터 주소를 가리키게 됩니다.
즉, 얕은 복사는 사본을 바꾸면 원본도 바뀌고, 원본을 바꾸면 사본도 바뀝니다.

중첩된 객체에 대한 얕은 복사
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//복사해서 새로운 object를 반환하는 함수
var copyObject = function(target) {
var result = {};
for (var prop in target) {
result[prop] = target[prop];
}
return result;
};

var user = {
name: 'Jaenam',
urls: {
portfolio: 'http://github.com/abc',
blog: 'http://blog.com',
facebook: 'http://facebook.com/abc',
},
};
var user2 = copyObject(user);

1. user2.name = 'Jung';
console.log(user.name === user2.name); // false

2. user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio); // true

3. user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog); // true
  1. 사본인 user2name 프로퍼티 값을 바꿔도 원본 username 프로퍼티 값은 바뀌지 않습니다.

  2. 2번 과 3번 코드는 원본과 사본 어느 쪽을 바꾸더라도 다른 한쪽의 값 역시 바뀐 것을 확인할 수 있습니다.

즉, user 객체에 직접 속한 프로퍼티에 대해서는 복사하여 새로운 데이터가 만들어진 반면,
한 단계 더 들어간 urls object의 내부 프로퍼티들은 기존 데이터를 참조합니다.

이런 현상을 막기 위해서는 한 단계 더 들어가는 프로퍼티들도 불변 객체로 만들 필요가 있습니다.

중첩된 객체에 대한 깊은 복사
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//복사해서 새로운 object를 반환하는 함수
var copyObject = function(target) {
var result = {};
for (var prop in target) {
result[prop] = target[prop];
}
return result;
};

var user = {
name: 'Jaenam',
urls: {
portfolio: 'http://github.com/abc',
blog: 'http://blog.com',
facebook: 'http://facebook.com/abc',
},
};

var user2 = copyObject(user);
1. user2.urls = copyObject(user.urls);

user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio); // false

user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog); // false
  1. 사본 user2urls프로퍼티에 copyObject() 함수를 실행하여 원본 userurls프로퍼티의 내부까지 복사하여 새로운 object를 만들어 할당했습니다. 그 결과 원본과 사본의 참조형 데이터 프로퍼티 값이 서로 다른 결과를 얻어냈습니다.

정리

정리하자면 객체를 복사할 때 객체 내부의 모든 값을 복사하여 새로운 객체를 만들고자 할때.
객체의 프로퍼티가 기본형 데이터인 경우 그대로 복사하면 되지만,
참조형 데이터는 내부의 프로퍼티들을 다시 복사하는 과정을 거쳐야 깊은 복사가 되는 것입니다.

아래는 이 개념을 바탕으로 원본데이터를 깊은 복사 방식으로 새로운 객체를 반환하는 코드입니다.


깊은 복사로 복사하는 방법

객체의 깊은 복사를 도와주는 범용 함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var copyObjectDeep = function(target) {
var result = {};
/* target이 object인 경우 내부 프로퍼티들을 순회하여
copyObjectDeep 함수를 재귀적으로 호출합니다.*/
if (typeof target === 'object' && target !== null) {
for (var prop in target) {
result[prop] = copyObjectDeep(target[prop]);
}
//object가 아닌 경우 target을 그대로 지정합니다.
} else {
result = target;
}
return result;
};
  • target === object 뒤에 target !== null 조건이 붙은 이유는
    typeof 메서드가 null 값에 대해 ‘object’를 반환하는 일종의 버그성? 때문입니다.

  • 이 함수를 사용해 객체를 복사하면 원본과 사본이 서로 다른 주소를 참조하게 되어 한 쪽의 프로퍼티를 변경하더라도 다른 한 쪽에 영향을 주지 않습니다.


객체를 JSON 문법으로 표현된 문자열로 변환했다가 다시 JSON객체로 바꾸는 방법도 있습니다.

JSON을 이용한 간단한 깊은 복사
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var copyObjectViaJSON = function(target) {
return JSON.parse(JSON.stringify(target));
};
var obj = {
a: 1,
b: {
c: null,
d: [1, 2],
func1: function() {
console.log(3);
},
},
func2: function() {
console.log(4);
},
};
var obj2 = copyObjectViaJSON(obj);

obj2.a = 3;
obj2.b.c = 4;
obj.b.d[1] = 3;

console.log(obj); // { a: 1. b: { c: null, d: [1, 3], func1: f() }, func2: f() }
console.log(obj2); // { a: 3. b: { c: 4, d: [1, 2] } }

이 방법은 단순하고 잘 동작합니다. 하지만 메서드(함수)나 숨겨진 프로퍼티인 __proto__나 getter/setter 등과 같이 JSON으로 변경할 수 없는 프로퍼티들은 모두 무시됩니다.
httpReaquest로 받은 데이터를 저장한 object를 복사할 경우 등 순수한 정보만을 다루는 object 복사에 좋은 방법입니다.


undefined 와 null

자바스크립트에 “없음”을 나타내는 두 값 "undefined""null"
미세하게 다른 둘의 의미와 사용하는 목적을 살펴봅시다.


자바스크립트는 다음과 같은 경우 엔진에서 undefined 값을 자동으로 부여합니다.
  1. 값을 대입하지 않은 변수,즉 데이터 영역에 메모리 주소를 지정하지 않은 식별자에 접근할 때

  2. 존재하지 않는 프로퍼티에 접근할 때

  3. return 문이 없거나 호출되지 않는 함수의 실행 결과

엔진에서 undefined를 부여하는 경우 예시
1
2
3
4
5
6
7
8
9
10
11
12
var a;
console.log(a); // (1) undefined. 값을 대입하지 않은 변수에 접근
// a 는 데이터 영역의 메모리 주소가 지정되지 않은 식별자

var obj = { a: 1 };
console.log(obj.a); // 1
console.log(obj.b); // (2) 존재하지 않는 프로퍼티에 접근
console.log(b); // c.f) ReferenceError: b is not defined

var func = function() {};
var c = func(); // (3) 반환(return)값이 없으면 undefined를 반환한 것으로 간주.
console.log(c); // undefined

값을 대입하지 않은 변수가 빈 배열일 경우

값을 대입하지 않은 변수가 빈 배열일 경우 특이한 결과를 확인할 수 있습니다.

undefined 와 빈 배열
1
2
3
4
5
6
7
8
9
1. var arr1 = [];
arr1.length = 3;
console.log(arr1); // [empty x 3]

2. var arr2 = new Array(3);
console.log(arr2); // [empty x 3]

3. var arr3 = [undefined, undefined, undefined];
console.log(arr3); // [undefined, undefined, undefined]
  1. 빈 배열을 만들고 배열의 length 값을 3으로 지정하고 console에 찍으니 [empty x 3]이 출력됩니다.
    이는 배열에 3개의 빈 배열 요소를 확보했지만 확보된 요소에 어떤 값도 할당되어 있지 않음을 의미합니다.
    (undefined 조차 할당되어 있지 않습니다.)
  1. new 연산자를 사용해 Array 생성자 함수를 호출하고 length 값을 3으로 지정했습니다. 위에 1번과 같은 결과를 갖습니다.
  1. 배열을 만들고 각 요소에 undefined 값을 지정했습니다. 출력시 [undefined, undefined, undefined]가 출력됩니다.

    undefined 와 null

이처럼 “비어있는 요소”(empty)와 “undefined를 할당한 요소는” 출력 결과부터 다릅니다.

empty !=== undefined

"비어있는 요소"는 이터러블과 관련된 많은 배열 메서드들의 대상에서 제외됩니다.

사실은 Arrayobject임을 생각해보면 자연스러운 현상입니다.

존재하지 않는 프로퍼티를 이터러블 메서드로 순회할 수 없는 것이 당연합니다.

배열은 length 프로퍼티의 개수만큼 빈 공간을 확보하고 각각의 인덱스를 이름으로 지정할 것이라고 생각할 수 있지만,

실제로는 object와 마찬가지로 특정 인덱스에 값을 지정할 때 비로소 빈 공간을 확보하고 인덱스를 이름으로 지정하고 데이터의 주솟값을 저장하는 동작을 합니다.

즉, 값이 지정되지 않은 인덱스는 프로퍼티값이 지정되지 않은 빈 공간입니다.

위 처럼 undefined는 엔진이 부여하는 경우와 사용자가 값으로 할당한 실존하는 데이터인 경우로 나뉠수 있습니다.
이러한 방식은 혼란을 가져올 수 있습니다.
사용자가 “비어 있음”을 나타내고 싶어 할당한 값과, 엔진이 부여하는 “해당 값이 없음”
이 두가지를 확실히 구분하기 위해 사용하는 것이 바로 null 입니다.


null 사용 뜻

사용자가 "비어있음"을 뜻하는 용도로 사용하고 싶을 때는 null을 사용하도록 합니다.
null을 이런 용도에 사용하는 규칙을 지킨다면, undefined“값을 대입하지 않은 변수에 접근할 때” 엔진이 반환해 주는 값으로만 존재할 수 있습니다.

애초에 null이 이런 용도로 만들어진 데이터 타입입니다.