자바스크립트에서 데이터 타입은 크게 기본형(Primitive Type)과 참조형(Reference Type)으로 구분됩니다. 기본형과 참조형의 구분 기준은 값의 저장 방식과, 불변성 여부입니다.
Number, String, Boolean, null, undefined, Symbol
복제 방식(저장 방식)
값이 담긴 주소값을 바로 복제
불변성 여부
불변성을 띔(메모리의 관점에서 불변성을 판단해야 함)
Object → Array, Function, Date, RegExp, Map, WeakMap, Set, WeakSet
복제 방식(저장 방식)
값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제
불변성 여부
불변성을 띄지 않음(메모리의 관점에서 불변성을 판단해야 함)
1 0 1 1 0 0 0에서 비트는 1하나, 0하나1011000 / 8bits = 1byte / 8개의 비트가 모여 1바이트를 이룸java, c는 javascript와 다른 메모리 관리 방식을 가지고 있습니다.
java, c언어가 등장했던 60년대에는 메모리의 크기가 넉넉하지 않아 개발자가 변수를 지정할때 변수의 메모리 크기도 고려해서 지정해야 했습니다.
예시) 변수 8을 저장하는 방법(feat. 정수형)
var testValue = 3 일때,
testValue : 식별자 = 변수명
3 : 변수 = 데이터
/** 선언과 할당을 풀어 쓴 방식 */
var str;
str = "test!";
/** 선언과 할당을 붙여 쓴 방식 */
var str = "test!";
이미 입력한 문자열이 길어질 경우를 고려
예시
숫자는 항상 8byte로 고정이지만, 문자는 고정이 아닙니다(영문 : 1byte, 한글 : 2byte)
1003 주소에 할당된 데이터를 변환하고, 지금 가진 데이터보다 큰 데이터를 저장하려 한다고 가정하면
: 1004 주소 이후에 저장되어 있는 모든 데이터를 다 오른쪽으로 밀어야 합니다. 이런 경우가 생기기 때문에 값을 바로 변수영역 데이터(변수영역 주소값)에 대입하지 않고 데이터영역 데이터(데이터영역 주소값)에 입력합니다.
똑같은 데이터를 여러번 저장하는 경우를 고려
예시
1만개의 변수를 생성해서 모든 변수에 숫자 1을 할당하는 상황을 가정. 모든 변수를 별개로 인식한다고 한다면, 1만개의 변수 공간을 확보해야 합니다.
- 바로 변수영역에 대입하는 경우 (숫자형은 8바이트 고정)
변수영역 : 8byte * 1만개(변수명과 숫자값 1) = 총 8만 byte 사용
- 변수영역, 데이터영역을 나눠서 별도 저장하는 경우 (변수영역엔 2바이트를 저장한다고 가정)
변수영역 : 2byte 1만개(변수명) = 2만바이트
데이터영역 : 8byte 1개(숫자값 1) = 8바이트
= 총 2만 8바이트 사용
변수 : 변수 영역 메모리를 변경할 수 있음(O)
상수 : 변수 영역 메모리를 변경할 수 없음(X)
불변하다 : 데이터 영역 메모리를 변경할 수 없음(X)
불변하지 않다 : 데이터 영역 메모리를 변경할 수 있음(O)
var a = "abc";
// 데이터를 변경합니다.
a = "abcdef";
a라는 변수에 'abc'를 'abcdef'로 변경해 보겠습니다.
"abc"라는 값이 데이터영역의 @5002주소에 들어갔다고 가정합니다.
a = a + "def" (X)
"def"라는 값이 @5002주소에 추가되는 것이 아닙니다.
a = "abcdef" (O)
@5003에 별도로 "abcdef"라는 값이 생기고 a라는 변수는 @5002 → @5003로 주소를 변경합니다.
이 때, @5002는 더 이상 사용되지 않기 때문에 가비지컬렉터의 수거 대상이 됩니다.
참조 카운트가 0인 메모리 주소의 처리
참조 카운트가 0인 객체는 더 이상 사용되지 않으므로, 가비지컬렉터에 의해 메모리에서 제거됩니다.
참조카운트
객체를 참조하는 변수나 다른 객체의 수를 나타내는 값입니다.
가비지컬렉터(GC, Garbage Collector)
더 이상 사용되지 않는 객체를 자동으로 메모리에서 제거하는 역할을 합니다. 자바스크립트는 가비지컬렉션을 수행함으로써 개발자가 명시적으로 메모리 관리를 하지 않아도 되도록 지원합니다. 자바스크립트 엔진에서 내부적으로 수행되며, 개발자는 가비지컬렉션에 대한 직접적인 제어를 할 수 없습니다.
변수영역 주소 : @1002 ,변수영역 데이터(값) : a/@5002 →변경됨→ a/@5003
데이터영역 주소 : @5002 ,데이터영역 데이터(값) : 'abc'
데이터영역 주소 : @5003 ,데이터영역 데이터(값) : 'abcdef' →새로운 데이터
변수영역 데이터에 데이터주소가 변경되었을 뿐,
데이터영역의 데이터가 변경되지는 않습니다.
즉, "변수 a는 불변하다." 라고 할 수 있습니다.
var obj1 = {
a: 1,
b: "bbb",
};
// 데이터를 변경합니다.
obj1.a = 2;
obj1.a의 값을 숫자 1에서 숫자 2로 변경해 보겠습니다.
변경할 값인 숫자 2는 데이터 영역에 없으므로 숫자 2를 추가합니다.
obj1을 위한 별도 영역(프로퍼티영역) @7103에 데이터를 a/@5001 → a/@5003로 변경합니다.
참조형 데이터는 별도 저장공간(변수영역, 데이터영역 이외의 객체의 변수(프로퍼티-a, b)을 위한 별도 공간)이 필요합니다.
이것이 기본형 데이터와 변수 할당 과정의 차이점입니다.
변수영역 주소 : @1001, 변수영역 데이터(값) : obj1/@7103~
데이터영역 주소 : @5001 ,데이터영역 데이터(값) : 1
데이터영역 주소 : @5002 ,데이터영역 데이터(값) : 'bbb'
데이터영역 주소 : @5003 ,데이터영역 데이터(값) : 2 →새로운 데이터
프로퍼티영역 주소 : @7103, 프로퍼티영역 데이터(값) : a/@5001 →변경됨→ a/@5003
프로퍼티영역 주소 : @7104, 프로퍼티영역 데이터(값) : b/@5002
데이터영역에 저장된 값은 여전히 계속 불변값이지만,
obj1을 위한 별도 영역(프로퍼티영역)은 얼마든지 변경이 가능합니다.
이것 때문에 참조형 데이터를 흔히, "불변하지 않다(=가변하다)"라고 합니다.
중첩객체 : 객체 안에 또 다른 객체가 들어가는 것
객체는 배열, 함수 등을 모두 포함하는 상위개념이기 때문에 배열을 포함하는 객체도 중첩객체라고 할 수 있습니다.
var obj = {
x: 3,
arr: [3, 4, 5],
}
변수영역 주소 : @1001, 변수영역 데이터(값) : obj/@7103~
데이터영역 주소 : @5001 ,데이터영역 데이터(값) : 3
데이터영역 주소 : @5002 ,데이터영역 데이터(값) : 4
데이터영역 주소 : @5003 ,데이터영역 데이터(값) : 5
obj영역 주소 : @7103, obj영역 데이터(값) : x/@5001
obj영역 주소 : @7104, obj영역 데이터(값) : arr/@8104~
arr영역 주소 : @8104, arr영역 데이터(값) : 0/@5001
arr영역 주소 : @8105, arr영역 데이터(값) : 1/@5002
arr영역 주소 : @8106, arr영역 데이터(값) : 2/@5003
// STEP1. 선언
var a = 10; //기본형
var obj1 = { c: 10, d: "ddd" }; //참조형
변수영역 주소 : @1001, 변수영역 데이터(값) : a/@5001
변수영역 주소 : @1002, 변수영역 데이터(값) : obj1/@7103~
데이터영역 주소 : @5001 ,데이터영역 데이터(값) : 10
데이터영역 주소 : @5002 ,데이터영역 데이터(값) : 'ddd'
obj1영역 주소 : @7103, obj1영역 데이터(값) : c/@5001
obj1영역 주소 : @7104, obj1영역 데이터(값) : d/@5002
// STEP2. 복사
var b = a; //기본형
var obj2 = obj1; //참조형
변수영역 주소 : @1003, 변수영역 데이터(값) : b/@5001(a가 가진 주소)
변수영역 주소 : @1004, 변수영역 데이터(값) : obj2/@7103~ (obj1이 가진 주소)
// STEP3. 복사 이후 값 변경(객체의 프로퍼티 변경)
b = 15;
obj2.c = 20;
변수영역 주소 : @1003, 변수영역 데이터(값) : b/@5001(a가 가진 주소) →변경됨→ b/@5003
변수영역 주소 : @1004, 변수영역 데이터(값) : obj2/@7103~(obj1이 가진 주소) →주소는 그대로지만 값이 변경됨
데이터영역 주소 : @5003 ,데이터영역 데이터(값) : 15 →새로운 데이터
데이터영역 주소 : @5004 ,데이터영역 데이터(값) : 20 →새로운 데이터
obj1영역 주소 : @7103, obj1영역 데이터(값) : c/@5001 →변경됨→_ c/@5004
객체의 프로퍼티(=속성)에 접근해서 값을 변경하는 것이 아니라 객체 자체를 변경하는 방식
//기본형
var a = 10; // STEP1. 선언
var b = a; // STEP2. 복사
//참조형
var obj1 = { c: 10, d: "ddd" }; // STEP1. 선언
var obj2 = obj1; // STEP2. 복사
// STEP3. 복사 이후 값 변경(객체 자체를 변경)
b = 15;
obj2 = { c: 20, d: "ddd" };
// 참고
// obj2.c = 20; // STEP3. 복사 이후 값 변경(객체의 프로퍼티 변경)
변수영역 주소 : @1001, 변수영역 데이터(값) : a/@5001
변수영역 주소 : @1003, 변수영역 데이터(값) : b/@5001(a가 가진 주소) →변경됨→ b/@5003
변수영역 주소 : @1002, 변수영역 데이터(값) : obj1/@7103~obj2와 다른주소를 가짐
변수영역 주소 : @1004, 변수영역 데이터(값) : obj2/@8103~obj1과 다른주소를 가짐
데이터영역 주소 : @5001 ,데이터영역 데이터(값) : 10
데이터영역 주소 : @5002 ,데이터영역 데이터(값) : 'ddd'
데이터영역 주소 : @5003 ,데이터영역 데이터(값) : 15 →새로운 데이터
데이터영역 주소 : @5004 ,데이터영역 데이터(값) : 20 →새로운 데이터
obj1영역 주소 : @7103, obj1영역 데이터(값) : c/@5001
obj1영역 주소 : @7104, obj1영역 데이터(값) : d/@5002
obj2영역 주소 : @8103, obj1영역 데이터(값) : c/@5004
obj2영역 주소 : @8104, obj1영역 데이터(값) : d/@5002
var a = 10;
var b = a;
b = 15;
a !== b; // 기본형 변수 복사의 결과는 다른 값
변수영역 주소 : @1001, 변수영역 데이터(값) : a/@5001
변수영역 주소 : @1003, 변수영역 데이터(값) : b/@5001(a가 가진 주소) →변경됨→ b/@5003
데이터영역 주소 : @5001 ,데이터영역 데이터(값) : 10
데이터영역 주소 : @5003 ,데이터영역 데이터(값) : 15 →새로운 데이터
var obj1 = { c: 10, d: "ddd" };
var obj2 = obj1;
obj2.c = 20;
obj1 === obj2; // 참조형 변수 복사의 결과는 같은 값(원하지 않는 결과)
변수영역 주소 : @1002, 변수영역 데이터(값) : obj1/@7103~
변수영역 주소 : @1004, 변수영역 데이터(값) : obj2/@7103~(obj1이 가진 주소) →주소는 그대로지만 값이 변경됨
데이터영역 주소 : @5001 ,데이터영역 데이터(값) : 10
데이터영역 주소 : @5002 ,데이터영역 데이터(값) : 'ddd'
데이터영역 주소 : @5004 ,데이터영역 데이터(값) : 20 →새로운 데이터
obj1영역 주소 : @7103, obj1영역 데이터(값) : c/@5001 →변경됨→ c/@5004
obj1영역 주소 : @7104, obj1영역 데이터(값) : d/@5002
객체의 속성에 접근해서 값을 변경하면 가변이 성립됩니다.
반면, 객체 데이터 자체를 변경(새로운 데이터를 할당)하면 기존 데이터는 변경되지 않습니다. 즉, 불변합니다.
// 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
개선한 방법이지만 이 방법에도 문제점이 있습니다.
changeName 함수는 새로운 객체를 만들기 위해 변경할 필요가 없는 gender 프로퍼티를 하드코딩으로 입력했습니다. 만일 이러한 속성이 10개라면 10개를 다 하드코딩으로 입력해야 합니다.
더 나은 개선 방법으로 얕은 복사가 있습니다.
var shallowCopy = function (target) {
var result = {};
// for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근
// 이 shallowCopy로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경
for (var prop in target) {
result[prop] = target[prop];
}
return result;
};
var user = {
name: "wonjang",
gender: "male",
};
var user2 = shallowCopy(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 = shallowCopy(user);
user2.name = "twojang"; // 1depth인 name은 복사 가능
// 1depth인 얕은 단계에서는 불변성을 유지하기 때문에 값이 다름
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
내부의 모든 값들을 하나하나 다 찾아서 모두 복사하는 방법
객체의 프로퍼티 중, 기본형 데이터는 그대로 복사 + 참조형 데이터는 다시 그 내부의 프로퍼티를 복사 → 재귀적 수행(함수나 알고리즘이 자기 자신을 호출하여 반복적으로 실행되는 것)을 해야 합니다.
var deepCopy = function (target) {
var result = {};
// 복사한 것이 object이면서 null이 아닌 경우(참조형인 경우)
if (typeof target === "object" && target !== null) {
// for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근
for (var prop in target) {
// 자기 자신을 수행(재귀적 수행)
result[prop] = deepCopy(target[prop]);
}
} else {
result = target;
}
return result;
};
var obj = {
a: 1,
b: {
c: null,
d: [1, 2],
},
};
var obj2 = deepCopy(obj);
obj2.a = 3;
obj2.b.c = 4;
obj2.b.d[1] = 3;
console.log(obj.a === obj2.a); // false
console.log(obj.b.c === obj2.b.c); // false
console.log(obj.b.d[1] === obj2.b.d[1]); // false
불변성을 유지하는 또 다른 방법은 JSON을 이용하는 방법이 있습니다.
따라서 JSON을 이용한 깊은 복사는 객체의 구조가 간단하고, 함수나 undefined와 같은 속성 값이 없는 경우에 적합한 방법입니다. 만약 객체의 구조가 복잡하거나 순환 참조가 있는 경우에는 다른 깊은 복사 방법을 고려해야 합니다.
개발자가 직접 지정할 수도 있지만 일반적으로는 자바스크립트 엔진에서 값이 있어야 할 것 같은데 없는 경우, 자동으로 부여합니다.
.이나 []로 접근하려 할 때, 해당 데이터가 존재하지 않는 경우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
"없다"를 명시적으로 표현할 때 사용합니다.
주의사항
console.log(typeof null)
typeof null이 object인 것은 유명한 javascript 자체 버그입니다. 주의해야합니다.
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); // false
console.log(n === null); // true