(이미지 출처 : https://velog.io/@imjkim49/자바스크립트-데이터-타입-정리)
데이터 타입은 크게 기본형과 참조형으로 나뉜다.
나누는 기준은 크게 2가지로, 복제 방식과 불변성이다.
기본형 : 값이 담긴 주소값을 복제
참조형 : 값이 담긴 주소값들로 이루어진 묶음들의 주소값을 복제
기본형 : 불변성 (= 데이터 영역 메모리 변경 불가)
참조형 : 가변성 (= 데이터 영역 메모리 변경 가능)
🙋🏻♀️ 64bit 정수는 메모리에 어떻게 저장합니까?
: 64bit == 8byte이므로 각 바이트를 메모리에 저장하면 된다. 즉, 8개의 연속된 byte에 저장된다.
다음 내용으로 가기 전에, 단어를 정리하고 가자면
let a = 3 에서 데이터인 3은 변수이고, 변수명인 a는 식별자이다.
즉 let a(식별자
) = 3(변수
)
let a = 'hi'
a라는 식별자에 'hi'라는 데이터를 할당할 때, 어떤 식으로 데이터가 저장되는지 아래와 같은 사진을 보고 설명하겠다.
위 사진을 크게 두 박스로 나눠서 윗 박스를 식별자 영역으로 생각하고, 아래 박스를 데이터 영역으로 봐야한다.
1. 식별자 a를 식별자 영역의 빈 공간 @1002에 저장
2. 실제 데이터 'hi'를 데이터 영역의 빈 공간 @5002에 저장
3. 식별자 영역의 데이터 부분에 실제 데이터의 주소인 @5002를 저장
이 단계를 통해 변수와 실제 데이터가 메모리에 저장이 된다.
굳이 실제 데이터가 아닌 데이터가 담긴 주소를 식별자 영역의 데이터에 저장하는 이유는 아래와 같다.
자유로운 데이터 변환을 위하여
숫자는 8byte로 그 크기가 고정되어 있지만 문자는 그렇지 않다.(영어는 1byte, 한글은 2byte) 따라서 실제 데이터 문자가 길어진다면 새로운 공간을 사용해야 하는 번거로움이 있다. 따라서 데이터 변환을 자유롭게 하기 위해 실제 데이터가 아닌 데이터의 주소를 담는다.
이번에는 기본형 데이터가 아닌 참조형 데이터의 할당 과정을 알아보자.
기본형과 다르게 참조형의 경우 참조형 data 영역이 하나 더 있다.
따라서 식별자인 obj1은 실제 데이터가 있는 주소를 저장하고 있는 참조형 영역의 주소들을 참조하게 된다.
만약 참조형 데이터 안에 또 다른 참조형 데이터가 있다면 어떨까?
이를 중첩객체라고 하는데, 중첩객체의 데이터 할당 과정을 알아보자
참조형 데이터 obj 영역 말고도 중첩객체인 arr의 영역도 있다.
배열 객체를 가진 arr는 0번(객체의 key 값이자, 배열의 index)에 실제 데이터가 담긴 주소가 들어간다. 다시 obj 영역에서 arr라는 변수에는, 데이터가 담긴 주소를 담고 있는 주소를 참조한다.
기본형 = 실제 데이터 담긴 주소를 식별자 영역에서 참조
참조형 = 실제 데이터 주소를 참조하는 주소(참조형 데이터 영역의 주소)를 식별자 영역에서 참조
기본형은 1번에서 알 수 있듯이, 불변성을 가진다고 했다. a에 값을 재할당 하는 예시로 자세하게 설명하겠다.
let a = 'hi'
일 때, a의 데이터를 'hijisu'로 변경한다면 어떻게될까? 사진처럼 a는 더 이상 5002번 주소를 담고 있지 않고, 새로운 메모리 주소인 5003을 식별자 영역의 데이터에 저장하고 있다. 즉 과정은 아래와 같다.
1. 데이터 영역의 빈 공간 @5003에 데이터 'hiJisu' 저장
2. a의 데이터 부분에 실제 데이터 주소인 @5003 저장
3. 더 이상 사용하지 않는 @5002는 가비지 콜렉터 수거 대상이 됨
즉, 데이터 자체('hi')를 'hiJisu'로 변경하지 않고 새로운 데이터 주소를 저장하기 때문에, 데이터를 변경하지 않음 = 불변하다고 표현한다.
이번에는 참조형 데이터의 가변성에 대해 알아보겠다.
----- 내용 추가하기 ----
참조형 데이터가 ‘가변값’이라고 할 때의 ‘가변’은 참조형 데이터 자체를 변경할 경우가 아니라, 그 내부의 프로퍼티를 변경할 때 성립한다고 할 수 있음.
// user 객체를 생성
var user = {
name: 'jisu',
gender: 'female',
};
var changeName = function (user, newName) {
var newUser = user; // 객체를 그대로 복사함
newUser.name = newName; // 객체의 프로퍼티(속성)에 접근해서 이름을 변경 -> 원본이 참조하는 주소도 변경
return newUser;
};
var user2 = changeName(user, '차은우');
console.log(user.name, user2.name); // 차은우 차은우
console.log(user === user2); // true
보시다 싶이, var newUser = user
구문처럼 객체를 그대로 할당해서 복사하는 방식을 사용하고, 원본의 프로퍼티에 접근하여 값을 변경하면 원본이 참조하는 주소 또한 변경되어 두 유저의 이름이 newName
의 값이 된다.
참조형 타입의 경우 obj의 데이터 주소를 가져가고 데이터 자체는 변경하지 않지만(새로운 데이터를 만들지만)
obj의 데이터를 참조하는 주소가 변경되면 그 바뀐 주소를 가리키는 주소를 원본과 복사본 모두 가지게됨
var copyObject = function (target) {
var result = {};
for (let prop in target) {
result[prop] = target[prop];
}
return result;
};
var user = {
name: "차은우",
gender: "male",
};
var user2 = copyObject(user);
user2.name = "공유";
if (user !== user2) {
console.log("유저 정보가 변경되었습니다.");
}
console.log(user.name, user2.name); // 차은우 공유
console.log(user === user2); // false
그렇기 때문에 얕은 복사를 사용하여 이 문제를 해결할 수 있음
copyObject()는 객체를 매개변수로 받아서 for in문을 통해 새로운 객체에 복사할 객체의 key(해당 코드에서는 prop이라는 변수로 사용) 값을 통해 프로퍼티를 만든다. 이 경우 완전히 새로운 객체를 생성한 것이기 때문에, 복사한 객체의 프로퍼티를 변경해도 원본의 프로퍼티에는 영향이 가지 않는다.
하지만 중첩객체(객체 안에 또 객체가 있는 형태)의 경우 여전히 참조형 데이터가 저장된 프로퍼티를 복사할 때, 주소값만 복사하기 때문에 복사한 객체의 중첩된 객체의 프로퍼티를 수정하면 원본의 중첩 객체 또한 변경된다.
// 깊은 복사
var copyObjectDeep = function (target) {
var result = {};
if (typeof target === "object" && target !== null) {
for (var prop in target) {
result[prop] = copyObjectDeep(target[prop]);
console.log("here prop : ", prop, "result[prop] :", result[prop]);
}
} else {
result = target;
}
return result;
};
//결과 확인
var obj = {
a: 1,
b: {
c: null,
d: [1, 2],
},
};
var obj2 = copyObjectDeep(obj);
obj2.a = 3;
obj2.b.c = 4;
obj2.b.d[1] = 3;
console.log(obj); // { a: 1, b: { c: null, d: [ 1, 2 ] } }
console.log(obj2); // { a: 3, b: { c: 4, d: { '0': 1, '1': 3 } } }
copyObjectDeep()에서 for in문을 돌 때, 콘솔 구문을 하나 추가해봤는데 아래와 같이 찍힌다.
here prop : a result[prop] : 1
here prop : c result[prop] : null
here prop : 0 result[prop] : 1
here prop : 1 result[prop] : 2
here prop : d result[prop] : { '0': 1, '1': 2 }
here prop : b result[prop] : { c: null, d: { '0': 1, '1': 2 } }
아!
그러니까 for in문 안의 재귀함수 사용(자기를 다시 호출)을 통해서 중첩된 객체가 있을 때는 그 안의 객체를 다시 for in의 prop으로 넘기는거다.
쟤를 순서대로 정리하면
obj의 a : 1
이 prop으로 들어간다. 따라서 a : 1
obj의 b: {c : null, d : [1,2]}
이 prop으로 들어간다. 이때 재귀함수에 의해서 해당 target[prop](= b를 키로한 밸류) 전체가 다시 copyObjectDeep 함수의 매개변수로 들어가게 된다.
즉, copyObjectDeep( {c : null, d : [1,2]} )
을 수행한다. 따라서 첫 번째 prop으로 c가 들어가고, 두번째 prop으로 d가 들어간다.
c의 경우 객체가 없기때문에 재귀 함수에서 빠져나오고, d는 또 다시 배열을 가지고 있으므로 재귀함수를 통해 copyObjectDeep( [1,2] )
를 수행한다. 배열의 key는 index이기 때문에(for..in은 임의의 순서로 돌기때문에 index와 무관하게 반복문을 실행한다) 첫 번째 prop으로 0이 들어가고, 두번재 prop으로 2가 들어간다.
따라서 중첩 객체를 재귀함수의 매개변수로 넣어 이러한 과정을 반복하고 최종적으로 깊은 복사를 통해 원본에 영향을 주지 않는 불변한 객체를 복사할 수 있게 된 것이다.
값이 없음을 명시해 줄 때는, undefined이 아닌 null
을 반드시 사용하자
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);
주의할 점은, null의 typeof는 object
이다. 이는 자바스크립트의 자체 버그이다!
따라서 해당 값이 null인지 확인할 때는 typeof가 아닌 ===
일치 연산자를 통해 확인하는 습관을 들이자.
과제
1번
var user = {
name: "john",
age: 20,
};
var getAged = function (user, passedTime) {
let agedUser = {};
for (let i in user) {
agedUser[i] = user[i];
}
agedUser.age += passedTime;
return agedUser;
};
console.log(getAged(user, 6));
var agedUser = getAged(user, 6);
var agedUserMustBeDifferentFromUser = function (user1, user2) {
if (!user2) {
console.log("Failed! user2 doesn't exist!");
} else if (user1 !== user2) {
console.log("Passed! If you become older, you will be different from you in the past!");
} else {
console.log("Failed! User same with past one");
}
};
agedUserMustBeDifferentFromUser(user, agedUser);
getAged 함수를 채워서 Passed~ 문장을 출력해주면 된다.
user 객체를 for in을 돌려서 각각의 prop을 복사해서 getAged로 만들어주면 된다.