데이터 타입
1. 데이터 타입의 종류 (기본형과 참조형)
자바스크립트에서 값의 타입은 기본형과 참조형으로 구분된다.
기본형과 참조형의 구분 기준은 값의 저장 방식과 불변성 여부이다.
[기본형과 참조형의 구분 기준]
1. 저장 / 복제 방식
가. 기본형 : 값이 담긴 주소값을 바로 복제
나. 참조형 : 값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제
2. 불변성의 여부
가. 기본형 : 불변성을 띔 (=불변하다)
나. 참조형 : 불변성을 띄지 않음 (=불변하지 않다)
기본형이 불변하다는 것에 대해 이의를 제기할 수도 있는데 예를 들어
var a = 6;
a = "abc";
와 같이 값을 재할당해서 a라는 변수의 값이 변했기 때문에 "불변하지 않는 것 아니냐" 라고 할 수 있는데
불변하다라는 말은 메모리 관점에서 봐야 불변한지 아닌지를 판단할 수 있는 것이다.
[메모리와 데이터에 관한 배경지식]
1. 비트
- 컴퓨터가 이해할 수 있는 가장 작은 단위, 0과 1을 가지고 있는 메모리를 구성하기 위한 작은 조각을 의미
2. 바이트
- 0과 1만 표현하는 비트를 모두 찾기는 부담
- 8개의 비트를 묶은 단위
- 바이트는 항상 메모리 주솟값을 갖고 있다. 그렇기에 서로 구분될 수 있다.
3. 메모리
- 바이트 단위로 구성
- 모든 데이터는 바이트 단위의 식별자인 메모리 주소값을 통해서 서로 구분된다.
2. 변수 선언과 데이터 할당
/** 선언과 할당을 풀어 쓴 방식 */
var str;
str = 'test!';
/** 선언과 할당을 붙여 쓴 방식 */
var str = 'test!';
변수 영역 |
주소 | ... | 1002 | 1003 | 1004 | 1005 | ... |
데이터 | 이름 : str 데이터 : @5004 |
||||||
데이터 영역 |
주소 | ... | 5002 | 5003 | 5004 | 5005 | ... |
데이터 | 1234 | 'abcd' | 'test!' |
값을 바로 변수에 대입하지 않는 이유 (= 1002 주소 데이터에 'test!'를 바로 입력하지 않는 이유)
1. 자유로운 데이터 변환
-> 숫자는 항상 8byte로 고정이지만 문자는 고정이 아니다. 그래서 이미 1002 주소에 할당된 데이터를 변환하려고 할때 훨씬 더 큰 데이터를 저장하려면 1003 이후부터 저장되어 있는 모든 데이터를 오른쪽으로 다 ~~ 미뤄야 한다.
2. 효율적인 메모리 관리
-> 1만개의 변수를 생성해서 모든 변수에 숫자 1을 할당하는 상황이라면 1만개의 변수 공간을 확보해야 한다.
4. 기본형 데이터와 참조형 데이터
가. 메모리를 기준으로 다시 한번 생각해보는 두가지 주요 개념
a. 변수 vs 상수
1) 변수 : 변수 영역 메모리를 변경할 수 있다.
2) 상수 : 변수 영역 메모리를 변경할 수 없다.
b. 불변하다 vs 불변하지 않다
1) 불변하다 : 데이터 영역 메모리를 변경할 수 없다.
2) 불변하지 않다 : 데이터 영역 메모리를 변경할 수 있다.
나. 불변값과 불변성(with 기본형 데이터)
// a라는 변수가 abc에서 abcdef가 되는 과정을 통해 불변성을 유추해봅시다!
// 'abc'라는 값이 데이터영역의 @5002라는 주소에 들어갔다고 가정할게요.
var a = 'abc';
// 'def'라는 값이 @5002라는 주소에 추가되는 것이 아니죠!
// @5003에 별도로 'abcdef'라는 값이 생기고 a라는 변수는 @5002 -> @5003
// 즉, "변수 a는 불변하다." 라고 할 수 있습니다.
// 이 때, @5002는 더 이상 사용되지 않기 때문에 가비지컬렉터의 수거 대상이 됩니다.
a = a + 'def';
가비지컬렉터 (GC, Garbage Collector)
더 이상 사용되지 않는 객체를 자동으로 메모리에서 제거하는 역할을 한다.
자바스크립트는 가비지 컬렉션을 수행함으로서 개발자가 명시적으로 메모리 관리를 하지 않아도 되도록 지원한다.
자바스크립트 엔진에서 내부적으로 수행되며, 개발자는 가비지 컬렉션에 대한 직접적인 제어를 할 수 없다.
다. 가변값과 가변성 (with 참조형 데이터)
a. 참조형 데이터의 변수 할당 과정
// 참조형 데이터는 별도 저장공간(obj1을 위한 별도 공간)이 필요합니다!
var obj1 = {
a: 1,
b: 'bbb,
};
변수 영역 |
주소 | 1001 | 1002 | 1003 | 1004 |
데이터 | 이름 : obj1 데이터 : @7103~ |
||||
데이터 영역 |
주소 | 5001 | 5002 | 5003 | 5004 |
데이터 | 1 | 'bbb' |
obj1을 위한 별도 공간 |
주소 | 7103 | 7104 | 7105 | 7106 |
데이터 | 이름 : a 데이터 : @5001 |
이름 : b 데이터 : @5002 |
b. ⭐️⭐️⭐️기본형 데이터의 변수 할당과정과의 차이점은 객체의 변수(프로퍼티) 영역의 별도 존재 여부⭐️⭐️⭐️
c. 참조형 데이터가 불변하지 않다(=가변하다)고 하는 이유
var obj1 = {
a: 1,
b: 'bbb',
};
// 데이터를 변경해봅시다.
obj1.a = 2;
위 코드에서 데이터 변경과정은 아래와 같을 것이다.
1. 변경할 값인 숫자 2를 데이터 영역에서 검색
2. 없으니 2를 새로 추가하고 해당 주소(ex.@5003)를 obj1을 위한 별도 영역(@7103)에 갈아껴준다.
3. 데이터 영역에 저장된 값은 여전히 계속 불변값이지만 obj1을 위한 별도 영역은 얼마든지 변경이 가능하기 때문에
4. 참조형 데이터를 흔히 "불변하지 않다(=가변하다)" 라고 한다.
d. 중첩객체의 할당
자바스크립트에서 중첩객체란, 객체 안에 또 다른 객체가 들어가는 것을 의미한다.
객체는 배열, 함수 등을 모두 포함하는 상위 개념이기 때문에 배열을 포함하는 객체도 중첩 객체라고 할 수 있다.
var obj = {
x: 3,
arr: [3, 4, 5],
}
// obj.arr[1]의 탐색과정은 어떻게 될까요? 작성하신 표에서 한번 찾아가보세요!
주소 | 1001 | 1002 | 1003 | 1004 | 1005 |
데이터 | 이름 : obj 데이터 : @7103~ |
||||
주소 | 5001 | 5002 | 5003 | 5004 | 5005 |
데이터 | 3 | 4 | 5 |
주소 | 7103 | 7104 | ... |
obj를 위한 별도 공간 | 이름 : x 데이터 : @5001 |
이름 : arr 데이터 : @8104~ |
주소 | 8104 | 8105 | 8106 | ... |
arr을 위한 별도 공간 | 이름 : 0 데이터 : @5001 |
이름 : 1 데이터 : @5002 |
이름 : 2 데이터 : @5003 |
e. 참조 카운트가 0인 메모리 주소의 처리
객체를 참조하는 변수나 다른 객체의 수를 나타내는 값.
참조 카운트가 0인 객체는 더 이상 사용되지 않으므로 가비지 컬렉터에 의해 메모리에서 제거된다.
라. 변수 복사의 비교
// STEP01. 쭉 선언을 먼저 해볼께요.
var a = 10; //기본형
var obj1 = { c: 10, d: 'ddd' }; //참조형
// STEP02. 복사를 수행해볼께요.
var b = a; //기본형
var obj2 = obj1; //참조형
주소 | 1001 | 1002 | 1003 | 1004 | 1005 |
데이터 | 이름 : a 데이터 : @5001 |
이름 : obj1 데이터 : @7103~ |
이름 : b 데이터 : @5001 |
이름 : obj2 데이터 : @7103~ |
|
주소 | 5001 | 5002 | 5003 | 5004 | 5005 |
데이터 | 10 | 'ddd' |
주소 | 7103 | 7104 | ... |
obj1을 위한 별도 공간 | 이름 : c 데이터 : @5001 |
이름 : d 데이터. : @5002 |
마. 복사 이후 값 변경(객체의 프로퍼티 변경)
// STEP01. 쭉 선언을 먼저 해볼께요.
var a = 10; //기본형
var obj1 = { c: 10, d: 'ddd' }; //참조형
// STEP02. 복사를 수행해볼께요.
var b = a; //기본형
var obj2 = obj1; //참조형
b = 15;
obj2.c = 20;
주소 | 1001 | 1002 | 1003 | 1004 | 1005 |
데이터 | 이름 : a 데이터 : @5001 |
이름 : obj1 데이터 : @7103~ |
이름 : b 데이터 : @5001 -> @5003 |
이름 : obj2 데이터 : @7103~ |
|
주소 | 5001 | 5002 | 5003 | 5004 | 5005 |
데이터 | 10 | 'ddd' | 15 | 20 |
주소 | 7103 | 7104 | |
obj1을 위한 별도 공간 | 이름 : c 데이터 : @5001 -> @5004 |
이름 : d 데이터 : @5002 |
복사 이후 변경된 값만 보라색으로 표현을 해놨는데 이 상태에서 보면
a와 b는 지금 전혀 다른 값을 갖고 있다.
그런데 obj1과 obj2는 같은 주소를 바라보고 있다.
그러므로 이렇게 주소를 바로 복사하면 obj2를 변경했음에도 obj1의 값도 변경된다는 것이다!
기본형
- 숫자 15라는 값을 데이터 영역에서 검색 후 없다면 생성
- 검색한 결과 주소 또는 생성한 주소를 변수 영역 b에 갈아끼움
- a와 b는 서로 다른 데이터 영역의 주소를 바라보고 있기 때문에 영향 없음
참조형
- 숫자 20이라는 값을 데이터 영역에서 검색 후 없다면 생성
- 검색한 결과주소 또는 생성한 주소 obj2에게 지정되어 있는 별도 영역에 갈아끼움
- obj1도 똑같은 주소를 바라보고 있기 때문에 obj1까지 변경됨
// 기본형 변수 복사의 결과는 다른 값! a !== b; // 참조형 변수 복사의 결과는 같은 값!(원하지 않았던 결과😭) obj1 === obj2;
바. 복사 이후 값 변경 (객체 자체를 변경)
//기본형 데이터
var a = 10;
var b = a;
//참조형 데이터
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
b = 15;
obj2 = { c: 20, d: 'ddd'};
주소 | 1001 | 1002 | 1003 | 1004 | 1005 |
데이터 | 이름 : a 데이터 : @5001 |
이름 : b 데이터 : @5001 -> @5003 |
이름 : obj1 데이터 : @7103 ~ |
이름 : obj2 데이터 : @7103 ~ -> @8103 ~ |
|
주소 | 5001 | 5002 | 5003 | 5004 | 5005 |
데이터 | 10 | 'ddd' | 15 | 20 |
주소 | 7103 | 7104 | 7105 |
obj1을 위한 별도 영역 | 이름 : c 데이터 : @5001 |
이름 : d 데이터 : @5002 |
주소 | 8103 | 8104 | 8105 |
obj2를 위한 별도 영역 | 이름 : c 데이터 : @5004 |
이름 : d 데이터 : @5002 |
obj2 변수는 참조형 데이터이고 참조형 데이터의 값을 변경한 것임에도 불구하고 이전 케이스와는 달리 obj1과는 바라보는 데이터 메모리 영역의 값이 달라졌다.
[예시코드]
⭐️⭐️⭐️즉, 참조형 데이터가 '가변값' 이라고 할 때의 '가변'은 참조형 데이터 자체를 변경하는 경우가 아니라 그 내부의 프로퍼티를 변경할 때 성립한다고 할 수 있다.⭐️⭐️⭐️
[개선된 코드]
changeName 함수 내부 로직만 바꿔서 새로운 객체를 반환하게끔 개선했다.
그러므로 user1과 user2는 서로 다른 객체가 되었다!
그러나 이 방법이 최선은 아니다. 만약 객체 프로퍼티가 100개라고 가정하면 하드 코딩해야할 부분이 많아지기 때문에 유지보수에 방해가 된다.
이를 해결하기 위해 등장한 방법이 얕은 복사, 깊은 복사이다.
5. 불변 객체
1. 불변 객체의 정의
앞서 "가변하다" 와 "불변하다"의 개념을 배웠고 객체를 예로 들면
객체의 프로퍼티에 접근해서 값을 변경하면 "가변"이 성립했다.
반면에 객체 데이터 자체를 변경(새로운 데이터를 할당) 하고자 한다면 기존 데이터는 변경되지 않았다. 즉, "불변하다"라고 볼 수 있었다.
다음 예시를 통해 객체 가변성에 따른 문제점을 알 수 있었고
// user 객체를 생성
var user = {
name: 'wonjang',
gender: 'male',
};
// 이름을 변경하는 함수, 'changeName'을 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티(속성)에 접근해서 이름을 변경했네요! -> 가변
var changeName = function (user, newName) {
var newUser = user;
newUser.name = newName;
return newUser;
};
// 변경한 user정보를 user2 변수에 할당하겠습니다.
// 가변이기 때문에 user1도 영향을 받게 될거에요.
var user2 = changeName(user, 'twojang');
// 결국 아래 로직은 skip하게 될겁니다.
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // twojang twojang
console.log(user === user2); // true
위 예시 코드를 아래와 같이 개선할 수 있었다
// user 객체를 생성
var user = {
name: 'wonjang',
gender: 'male',
};
// 이름을 변경하는 함수 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티에 접근하는 것이 아니라, 아에 새로운 객체를 반환 -> 불변
var changeName = function (user, newName) {
return {
name: newName,
gender: user.gender,
};
};
// 변경한 user정보를 user2 변수에 할당하겠습니다.
// 불변이기 때문에 user1은 영향이 없어요!
var user2 = changeName(user, 'twojang');
// 결국 아래 로직이 수행되겠네요.
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // wonjang twojang
console.log(user === user2); // false 👍
3. 더 나은 방법 : 얕은 복사
//이런 패턴은 어떨까요?
var copyObject = function (target) {
var result = {};
// for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근할 수 있습니다.
// 하드코딩을 하지 않아도 괜찮아요.
// 이 copyObject로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경하면
// 되겠죠!?
for (var prop in target) {
result[prop] = target[prop];
}
return result;
}
var copyObject = function (target) {
var result = {};
for (var prop in target) {
result[prop] = target[prop];
}
return result;
};
//위 패턴을 우리 예제에 적용해봅시다.
var user = {
name: "wonjang",
gender: "male",
};
var user2 = copyObject(user);
user2.name = "twojang";
if (user !== user2) {
console.log("유저 정보가 변경되었습니다.");
}
console.log(user.name, user2.name);
console.log(user === user2);
하지만 얕은 복사 패턴도 여전히 문제가 있다.
왜냐하면 중첩된 객체에 대해서는 완벽한 복사를 할 수 없기 때문이다.
즉, 바로 아래 단계의 값만 복사 할 수 있고 중첩된 객체의 경우 참조형 데이터가 저장된 프로퍼티를 복사할 때, 주소값만 복사하기 때문이다.
var user = {
name: 'wonjang',
urls: {
portfolio: 'http://github.com/abc',
blog: 'http://blog.com',
facebook: 'http://facebook.com/abc',
}
};
var user2 = copyObject(user);
user2.name = 'twojang';
// 바로 아래 단계에 대해서는 불변성을 유지하기 때문에 값이 달라지죠.
console.log(user.name === user2.name); // false
// 더 깊은 단계에 대해서는 불변성을 유지하지 못하기 때문에 값이 같아요.
// 더 혼란스러워 지는거죠 ㅠㅠ
user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio); // true
// 아래 예도 똑같아요.
user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog); // true
위 코드에서도 보다시피 결국 user.urls 프로퍼티도 불변 객체로 만들어야 한다.
⭐️⭐️⭐️ 그러므로 깊은 복사를 할 수 있어야 근본적인 원인을 해결할 수 있다.⭐️⭐️⭐️
깊은 복사 : 내부의 모든 값들을 하나하나 다 찾아서 모두 복사하는 방법
중첩 객체에 대한 깊은 복사
var user = {
name: 'wonjang',
urls: {
portfolio: 'http://github.com/abc',
blog: 'http://blog.com',
facebook: 'http://facebook.com/abc',
}
};
// 1차 copy
var user2 = copyObject(user);
// 2차 copy -> 이렇게까지 해줘야만 해요..!!
user2.urls = copyObject(user.urls);
user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio);
user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog);
결론 : 객체의 프로퍼티 중 기본형 데이터는 그대로 복사 + 참조형 데이터는 다시 그 내부의 프로퍼티를 복사 => 재귀적 수행
재귀적 수행
함수나 알고리즘이 자기 자신을 호출하여 반복적으로 실행되는 것을 말한다.
[결론]을 적용한 코드
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;
}
[JSON]을 이용하는 방법도 존재하나 완벽한 방법은 아니다.
장점 :
1. JSON.stringify() 함수를 사용하여 객체를 문자열로 변환한 후, 다시 JSON.parse()함수를 사용하여 새로운 객체를 생성하기 때문에 원본 객체와 복사본 객체가 서로 독립적으로 존재한다. 따라서 복사본 객체를 수행해도 원본 객체에 영향을 미치지 않는다.
단점 :
JSON을 이용한 깊은 복사는 원본 객체가 가지고 있는 모든 정보를 복사하지 않는다. 예를 들어 함수나 undefined와 같은 속성 값은 복사되지 않는다. 또한, 중첩 객체의 경우에도 이 방법으로는 복사할 수 없다.
6. undefined와 null
두 개의 타입 모두 없음을 나타내는 타입이다. 하지만 미세하게 다르고 그 목적 또한 다르다.
1. undefined
보통 사용자가 직접 지정하는 경우보다도 자바스크립트가 다음 케이스에 자동으로 부여한다.
1. 변수에 값이 지정되지 않은 경우, 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 경우
2. . 이나 [] 로 접근하려 할 때, 해당 데이터가 존재하지 않는 경우
3. return문이 없거나 호출되지 않는 함수의 실행 결과
var a;
console.log(a); // (1) 값을 대입하지 않은 변수에 접근
var obj = { a: 1 };
console.log(obj.a); // 1
console.log(obj.b); // (2) 존재하지 않는 property에 접근
// console.log(b); // 오류 발생
var func = function() { };
var c = func(); // (3) 반환 값이 없는 function
console.log(c); // undefined
2. null
'없다'를 명시적으로 표현할 때 사용한다.
주의!!! : typeof null
typeof null 이 object인 것은 유명한 자바스크립트 자체 버그이다.
var n = null;
console.log(typeof n); // object
//동등연산자(equality operator)
console.log(n == undefined); // true
console.log(n == null); // true
//일치연산자(identity operator)
console.log(n === undefined);
console.log(n === null);