01. 데이터 타입의 종류
자바스크립트 데이터 타입에는 크게 2가지가 있다.
기본형
숫자, 문자열, 불리언, null, undefined 등이 있다. (ES6에서 심볼이 추가됨.)
참조형
객체, 배열, 함수, 날짜, 정규표현식 등이 있다. (ES6에서 Map, WeakMap, Set, WeakSet이 추가됨.)
구분 조건
기본형은 값이 담긴 주솟값을 바로 복제하는 반면, 참조형은 값이 담긴 주솟값들로 이루오진 묶음을 가리키는 주솟값을 복제한다.
02. 데이터 타입에 관한 배경지식
모든 데이터는 메모리 주솟값을 통해 서로 구분하고 연결할 수 있다.
식별자와 변수
변수: 변할 수 있는 무언가(데이터), 변경 가능한 데이터가 담길 수 있는 공간
식별자: 어떤 데이터를 식별하는 데 사용하는 이름, 즉 변수명
03. 변수 선언과 데이터 할당
3-1. 변수 선언
// 변할 수 있는 데이터를 만든다. 이 데이터의 식별자는 a로 한다.
var a;
비어 있는 메모리 공간 하나를 확보하고, 이 공간의 이름(식별자)을 a로 지정한다. 여기까지가 변수 선언 과정이다.
이후에 사용자가 a에 접근하고자 하면 컴퓨터는 메모리에서 a라는 이름을 가진 주소를 검색해 해당 공간에 담긴 데이터를 반환한다.
3-2. 데이터 할당
var a; // 변수 a 선언
a = 'abc'; // 변수 a에 데이터 할당
var a = 'abc'; // 변수 선언과 할당을 한 문장으로 표현
a라는 이름을 가진 주소를 검색해서 그 곳에 문자열 'abc'를 할당하면 될 것 같지만, 실제로는 해당 위치에 문자열 'abc'를 직접 저장하지 않는다.
데이터를 저장하기 위한 별도의 메모리 공간을 다시 확보해서 문자열 'abc'를 저장하고, 그 주소를 변수 영역에 저장하는 식으로 이루어진다.
1. 변수 영역에 빈 공간(@1003)을 확보한다.
2. 확보한 공간의 식별자를 a로 지정한다.
3. 데이터 영역의 빈 공간(@5004)에 문자열 'abc'를 저장한다.
4. 변수 영역에서 a라는 식별자를 검색한다(@1003).
5. 앞서 저장한 문자열의 주소(@5004)를 @1003의 공간에 대입한다.
변수 영역에 값을 직접 대입하지 않는 이유는, 데이터 변환을 자유롭게 할 수 있게 함과 동시에 메모리를 효율적으로 관리하기 위한 고민의 결과이다.
데이터 변경
문자열 'abc'의 마지막에 'def'를 추가하려고 하는 상황에서는
'abc'가 저장된 공간에 'abcdef'를 할당하는 대신, 'abcdef'라는 문자열을 새로 만들어 별도의 공간에 저장하고, 그 주소를 변수 공간에 연결한다.
즉, 기존 문자열에 어떤 변화를 가하든 상관 없이 무조건 새로 만들의 별도의 공간에 저장한다.
이처럼 변수 영역과 데이터 영역을 분리하면 중복된 데이터에 대한 처리 효율이 높아지게 된다.
04. 기본형 데이터와 참조형 데이터
4-1. 불변값
변수: 바꿀 수 있음.
상수: 바꿀 수 없음.
변수와 상수를 구분 짓는 대상은 변수 영역의 메모리이다. 한 번 데이터 할당이 이뤄진 변수 공간에 다른 데이터를 재할당할 수 있는지에 관한 여부이다.
반면 불변성를 구분 짓는 대상은 데이터 영역의 메모리이다.
그래서 기본형 데이터인 숫자, 문자열, boolean, null, undefined, Symbol은 모두 불변값이다. (기본형 = 불변값)
var a = 'abc';
a = a + 'def'; // 새로운 문자열을 만들어 변수 a에 저장
var b = 5;
var c = 5;
b = 7;
변수 b에 숫자 5를 할당한다. 그러면 데이터 영역에 5를 찾고, 없으면 데이터 공간을 하나 만들어 저장한다. 그 주소를 b에 저장한다. 변수 c에서 다시 같은 수인 5를 할당하려고 한다. 컴퓨터는 데이터 영역에서 5를 찾고, 이미 만들어진 값이 있기 때문에 그 주소를 재활용한다.
변수 b의 값을 7로 바꾸고자 할 때에는 기존에 저장된 5 자체를 7로 바꾸는 것이 아니라, 기존에 저장했던 7을 찾아서 있으면 재활용하고, 없으면 새로 만들어서 b에 저장한다.
즉, 기본값에서는 데이터 영역 값을 바꿀 수 없고 이것을 바로 불변값의 성질이라고 한다.
4-2. 가변값
기본형 데이터는 모두 불변값이며, 참조형 데이터는 기본적으로 가변값인 경우가 많다.
데이터 할당
var obj1 = {
a: 1,
b: 'bbb'
};
참조형 데이터는 기본형 데이터와 달리 '객체의 변수(프로퍼티) 영역'이 별도로 존재한다는 점이다.
1. 변수 영역 빈 공간(@1002)을 확보하고, 그 주소의 이름을 obj1로 지정한다.
2. 임의의 데이터 저장 공간(@5001)에 데이터를 저장하려고 보니 여러 개의 프로퍼티로 이뤄진 데이터 그룹이다.
2-1. 이 그룹 내부의 프로퍼티들을 저장하기 위해 별도의 변수 영역을 마련한다.
2-2. 그 영역의 주소(@7103 ~ ?)를 @5001에 저장한다.
3. @7103 및 @7104에 각각 a와 b라는 프로퍼티 이름을 지정한다.
4. 데이터 영역에 숫자 1을 검색한다. 검색 결과가 없으므로 임의로 @5003에 저장하고, 이 주소를 @7103에 저장한다.
5. 문장열 'bbb' 역시 임의로 @5004에 저장하고, 이 주소를 @7104에 저장한다.
프로퍼티 재할당
var obj1 = {
a: 1,
b: 'bbb'
};
obj1.a = 2;
데이터 영역에 2가 없으므로 빈 공간인 @5005에 저장하고, 이 주소를 @7103에 저장한다.
변수 obj1이 바라보고 있는 주소는 @5001로 변하지는 않는다. 즉 '새로운 객체'가 만들어지는 것이 아니라 기존의 객체 내부의 값만 바뀐 것이다.
중첩 객체
참조형 데이터의 프로퍼티에 다시 참조형 데이터를 할당하는 경우를 중첩 객체라고 한다.
var obj = {
x: 3,
arr: [ 3, 4, 5 ]
};
배열은 데이터 그룹으로, 이 그룹 내부의 프로퍼티를 저장하기 위해 별도의 변수 영역을 마련하고(@8104 ~ ?), 그 영역의 주소를 @7104에 저장한다. 배열의 요소가 총 3개이므로 3개의 변수 공간을 확보하고 각각 인덱스를 부여한다.
참조 카운트
obj.arr = 'str';
재할당 명령을 내리면, @5006에 문자열 'str'를 저장하고 그 주소를 @7104에 저장한다. 그러면 @5003은 더 이상 자신의 주소를 참조하는 변수가 하나도 없게 된다.
어떤 데이터에 대해 자신의 주소를 참조하는 변수의 개수를 참조 카운트라고 한다.
그래서 @5003의 참조 카운트는 1이었다가, @7104에 @5006이 저장되는 순간 0이 된다. (연쇄적으로 @8104 ~? 도 참조 카운트가 0이 됨.)
참조 카운트가 0인 메모리 주소는 가비지 컬렉터의 수거 대상이 된다. 가비지 컬렉터는 런타임 환경에 따라 특정 시점이나 메모리 사용량이 포화 상태에 임박할 때마다 자동으로 수거 대상들을 수거한다. 그러면 수거된 메모리는 다시 새로운 값을 할당할 수 있는 빈 공간이 된다.
4-3. 변수 복사 비교
기본형 데이터와 참조형 데이터는 복사할 때 차이가 존재한다.
변수 복사 과정
// 기본형 데이터
var a = 10;
var b = a;
// 참조형 데이터
var obj1 = { c: 10, d: 'ddd' };
var obj2 = ojb1;
기본형 데이터는 b가 식별자 a를 검색해 @1001에 저장된 값인 @5001을 @1002에 값으로 대입한다.
반면, 참조형 데이터는 obj2가 식별자 obj1을 검색해 그 값인 @5002를 @1004에 값으로 대입한다.
기본형과 참조형 모두 같은 주소를 바라보게 되는 점에서 동일하다. 그러나 데이터 할당 과정에서 차이가 있기 때문에, 변수 복사 이후의 동작에서 큰 차이가 발생한다.
변수 복사 이후 값 변경
var a = 10;
var b = a;
var obj1 = { c: 10, d: 'ddd' };
var obj2 = ojb1;
// 값 변경
b = 15;
obj2.c = 20;
8번째 줄에서는 데이터 영역에 15가 없기 때문에 새로운 공간을 저장하고(@5004), 그 주소를 식별자가 b인 주소를 찾아 값으로 대입한다.
9번째 줄에서는 20이 없기 때문에 새로운 공간에 저장하고(@5005), 변수 영역에서 obj2를 찾고(@1004), obj2의 값인 @5002가 가리키는 변수 영역에서 다시 c를 찾아(@7103) 그곳에 @5005를 대입한다.
기본형 데이터를 복사한 변수 b의 값을 바꿨더니 @1002의 값이 달라진 한편, 참조현 데이터를 복사한 변수 obj2의 프로퍼티의 값을 바꾸었더니 @1004의 값은 달라지지 않았다.
즉, 변수 a와 b는 서로 다른 주소를 바라보게 됐으나, 변수 obj1과 obj2는 여전히 같은 객체를 바라보고 있는 상태이다.
a !== b
obj1 === obj2
이게 바로 기본형과 참조형의 큰 차이점이다.
기본형은 주솟값을 복사하는 과정이 한 번만 이뤄지고, 참조형은 한 단계 더 거치게 된다는 차이가 있다는 것이다.
새로운 객체 할당
var a = 10;
var b = a;
var obj1 = { c: 10, d: 'ddd' };
var obj2 = ojb1;
b = 15;
obj2 = { c: 20, d: 'ddd' }; // 새로운 객체 할당
만약 새로운 객체를 할당함으로써 값을 직접 변경하면, 데이터 영역의 새로운 공간에 새 객체가 저장되고 그 주소를 변수 영역의 obj2 위치에 저장된다.
05. 불변 객체
5-1. 불변 객체를 만드는 간단한 방법
참조형 데이터의 '가변'은 내부 프로퍼티를 변경할 때만 성립되고, 데이터 자체를 변경(새로운 데이터 할당)하고자 하면 기본형 데이터와 마찬가지로 기존 데이터는 변하지 않는다.
객체 가변성에 따른 문제점
var changeName = function (user, newName) {
var newUser = user; // 그냥 복사하면 객체 참조 데이터도 같아짐
newUser.name = newName;
return newUser;
};
객체의 가변성에 따른 문제점의 해결 방법
var changeName = function (user, newName) {
// 새로운 객체 반환
return {
name: newName;
gender: user.gender;
};
};
새로운 객체를 반환하도록 수정하면, 다른 객체가 생성되기 때문에 문제를 해결할 수 있다. 그러나 변경할 필요가 없는 기존 객체의 프로퍼티를 하드코딩으로 입력했다. 만약 객체에 정보가 많을수록, 직접 입력해야 하는 수고가 늘어날 것이다.
기존 정보를 복사해서 새로운 객체를 반환하는 함수(얕은 복사)
var copyObject = function (target) {
var result = {}
for (var prop in target) {
result[prop] = target[prop];
}
return result;
};
간단하게 객체를 복사하고 내용을 수정할 수 있다. 그러나 얕은 복사만 수행한다는 단점이 있다.
5-2. 얕은 복사와 깊은 복사
얕은 복사: 바로 아래 단계의 값만 복사하는 방법
깊은 복사: 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법
그래서 copyObject 함수는 얕은 복사만 수행하기 때문에, 중첩된 객체에서 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주솟값만 복사한다는 의미이다. 그러면 원본과 사본 모두 동일한 참조형 데이터의 주소를 가리키게 되어, 서로 값에 영향을 미치게 된다.
객체의 깊은 복사를 수행하는 범용 함수
var copyObjectDeep = function (target) {
var result = {};
// null의 type이 object이기 때문에 target !== null 조건 추가
if (typeof target === 'object' && target !== null) {
for (var prop in target) {
result[prop] = copyObjectDeep(target[prop]); // 재귀적으로 수행
}
} else {
result = target;
}
return result;
}
객체인 경우에는 내부 프로퍼티를 순회하며 함수를 재귀적으로 호출하였다. 그러면 원본과 사본이 서로 완전히 다른 객체를 참조하기 때문에 어느 쪽의 프로퍼티를 변경하더라도 다른 쪽에 영향을 주지 않게 된다.
JSON을 활용한 간단한 깊은 복사
var copyObjectViaJSON = function (target) {
return JSON.parse(JSON.stringify(target));
};
객체를 JSON 문법으로 표현된 문자열로 전환했다가 다시 JSON 객체로 바꾸는 방법이 있다. 다만 함수는 무시하기 때문에 복사 되지 않는다.
06. undefined와 null
undefined와 null은 모두 '없음'을 나타내지만, 의미가 미세하게 다르고 사용하는 목적 또한 다르다.
6-1. undefined
사용자가 명시적으로 지정할 수도 있지만, 값이 존재하지 않을 때 자바스크립트 엔진이 자동으로 부여한다.
- 값을 대입하지 않는 변수, 즉 데이터 영역의 메모리 주소를 지정하지 않은 식별자를 접근할 때
- 객체 내부의 존재하지 않는 프로퍼티에 접근하려고 할 때
- return문이 없거나 호출되지 않는 함수의 실행 결과
// (1) 값을 대입하지 않은 변수에 접근
var a;
console.log(a);
// (2) 존재하지 않는 프로퍼티에 접근
var obj = { a: 1 }
console.log(obj.b);
// (3) return 값이 undefined를 반환한 것으로 간주
var func = function() { };
var c = func();
console.log(c);
(1) 값을 대입하지 않은 경우에 대해 배열의 경우에는 특이한 동작을 한다.
배열에 3개의 빈 요소를 확보했지만, 각 요소에는 어떤 값도 심지어 undefined조차도 할당돼 있지 않다.
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]
이처럼 '비어있는 요소'와 'undefined를 할당한 요소'는 출력 결과부터 다르다.
'비어있는 요소'는 배열 메서드들의 순회 대상에서 제외된다.
var arr1 = [undefined, 1];
var arr2 = [];
arr2[1] = 1;
arr1.forEach(function (v, i) { console.log(v,i); }); // undefined 0 / 1 1
arr2.forEach(function (v, i) { console.log(v,i); }); // 1 1 (empty 무시)
'undefined'가 비록 '비어있음'을 의미하긴 하지만 하나의 값으로 동작하기 때문에 순회의 대상이 된다.
6-2. null
자바스크립트 엔진이 반환하는 경우에는 우리의 통제 범위를 벗어나므로 직접 undefined를 할당하지 않는다.
그래서 '비어있음'을 명시적으로 나타내고 싶을 때는 null을 쓰면 된다.
그러나 null의 type은 object라는 점이다. 이는 자바스크립트 자체 버그이며, 어떤 변수의 값이 null인지 여부를 판별하기 위해서는 typeof 대신 다른 방식으로 접근해야 한다.
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
'💜 프론트엔드 > JavaScript' 카테고리의 다른 글
[코어자바스크립트] 03. this (0) | 2025.01.15 |
---|---|
[코어자바스크립트] 02. 실행 컨텍스트 (1) | 2025.01.11 |
[JavaScript] 이벤트 버블링, 캡쳐링, 위임 & HTTP 메소드 (0) | 2024.04.07 |
[JavaScript] var, let, const 중복 선언 허용, 스코프, 호이스팅 (0) | 2024.03.31 |
[JavaScript] 자바스크립트 얕은 복사 VS 깊은 복사 (0) | 2024.03.29 |
FE 개발자가 되고 싶은 짱잼이
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!