
자바스크립트의 데이터 타입에는 크게 두가지가 있다. 바로 기본형(원시 타입)과 참조형(참조 타입)이다.
기본형 : 숫자, 문자열, 불리언, null, undefined, 심볼
할당/연산 시 복제된다. -> 값이 담긴 주솟값을 바로 복제
불변성(immutability)를 띈다.
참조형 : 객체, 함수, 배열, 날짜, 정규표현식 등..
할단/연산 시 참조된다. -> 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주솟값을 복제
모든 데이터는 바이트 단위의 식별자, 더 정확하게는 메모리 주솟값을 통해 서로 구분하고 연결한다.
var a;
-> 변할 수 있는 데이터를 만든다. 이 데이터의 식별자는 a이다. 변수란 결국 변경 가능한 데이터가 담길 수 있는 공간
변수를 선언하면 메모리 영역에선 어떤 작업이 수행될까? 간략하게 표로 표현했다.
| 주소 | 1002 | 1003 | 1004 | 1005 |
|---|---|---|---|---|
| 데이터 | 이름: a 값: |
var a;
a = 'abc';
var a = 'abc';
-> 실제로 a 변수가 위치한 곳에 문자열 'abc'를 직접 저장하지는 않는다. 변수 할당 과정은 데이터를 저장 하기 위해 별도의 메모리 공간을 다시 확보해서 문자열 'abc'를 저장하고,그 주소를 변수 영역에 저장하는 방식이다.
변수 영역
| 주소 | 1002 | 1003 | 1004 | 1005 |
|---|---|---|---|---|
| 데이터 | 이름: a 값: @5004 |
데이터 영역
| 주소 | 5002 | 5003 | 5004 | 5005 |
|---|---|---|---|---|
| 데이터 | 'abc' |
번거롭게 왜 변수 영역에 값을 저장하지 않고 데이터 영역에 저장하는 이유는 무엇일까요?
예시
var a = "abc";
a = a + "def";
// 기존 'abc'가 'abcdef'로 바뀌는 것이 아니라 새로운 문자열 'abcdef'를 만들어 그 주소를 변수 a에 할당
// 즉, 'abc'와 'abcdef'는 완전 별개의 데이터
var b = 5; // 변수 b에 5 할당. 데이터 영역에서 5를 찾고, 없으면 데이터 공간을 만들어 저장
var c = 5; // 같은 수 5 할당. 이미 만들어놓은 값이니 주소를 재활용
b = 7; // b의 값을 7로 변경. 기존 5를 변경하는 것이 아닌 데이터 영역에서 7을 찾거나 새로 생성해서 주소 저장
-> 불변값의 성질 : 문자열 값도, 숫자 값도 다른 값으로 변경할 수 없다. 변경은 새로 만드는 동작을 통해서만 이뤄지고, 한 번 만들어진 값은 가비지 컬렉션을 당하지 않는 한 변하지 않는다.
예시 1
var obj1 = {
a: 1,
b: 'bbb'
};
-> 변수 영역의 빈 공간 확보 후 obj1로 이름 지정 -> 데이터 영역(@5001) 생성, 그룹 내부의 프로퍼티들을 저장하기 위해 별도 변수 영역 마련 후 그 주소를 @5001에 저장 -> 객체 변수 영역에 각각 a, b로 이름 지정 ->a, b에 해당하는 값 데이터 저장을 위해 데이터 영역 검색 후 저장
기본형 데이터와의 차이는 '객체의 변수(프로퍼티) 영역'이 별도로 존재한다는 점.
-> 객체가 별도로 할애한 영역은 변수 영역이고 데이터 영역은 기존 메모리 공간을 그대로 활용하고 있다. 따라서 변수에는 언제든지 다른 값을 대입할 수 있으므로 참조형 데이터는 불변하지 않다고 하는 것이다.
예시 2
obj1.a = 2;
-> 데이터 영역에서 숫자 2 검색 후 없으므로 빈 공간에 저장 후 이 주소를 객체 변수 영역에 저장
새로운 객체가 만들어진 것이 아니라 객체 내부의 값만 변경됨!
예시 3
중접객체(Nested Object) : 참조형 데이터의 프로퍼티에 다시 참조형 데이터를 할당하는 경우
var obj = {
x: 3,
arr: [3, 4, 5]
};
-> 위 코드의 arr 값을 str로 재할당하면 @5003을 참조하는 변수가 하나도 없게 된다. 이때 참조카운트가 0이 되므로 이 메모리 주소는 가비지 컬렉터의 수거 대상이 된다.
어떤 데이터에 대해 자신의 주소를 참조하는 변수의 개수를 참조 카운트라고 한다.
- 기본형 데이터 vs. 참조형 데이터
var a = 10;
var b = a;
var obj1 = {c: 10, d: 'ddd'};
var obj2 = obj1;
-> 변수를 복사하는 과정은 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보게 되는 점에서 동일하다. 하지만, 데이터 할당 과정에서 차이가 있기 때문에 변수 복사 이후의 동작에도 큰 차이가 발생!
- 변수 복사 이후 값 변경 결과 비교 - 객체의 프로퍼티 변경 시
var a = 10;
var b = a;
var obj1 = {c: 10, d: 'ddd'};
var obj2 = obj1;
b = 15;
obj2.c = 20;
-> 기본형 데이터 같은 경우 @1002의 값이 달라진 반면, 참조형 데이터의 값은 변경되지 않고 obj1과 obj2는 여전히 같은 객체를 바라보고 있다.
주의) 변수의 값을 직접 변경할 때와 값이 아닌 내부 프로퍼티(obj2.c)를 변경할 때의 결과 비교임.
따라서, 같은 조건인 상태도 비교해보겠다.
- 변수 복사 이후 값 변경 결과 비교 - 객체 자체를 변경했을 때
var a = 10;
var b = a;
var obj1 = {c: 10, d: 'ddd'};
var obj2 = obj1;
b = 15;
obj2.c = {c: 20, d: 'dd'};
-> 참조형 데이터임에도 불구하고 값이 변경된 것을 볼 수 있다.
즉, 참조형 데이터가 '가변값' 이라고 설명할 때의 '가변'은 참조형 데이터 자체를 변경할 겨우가 아니라 그 내부의 프로퍼티를 변경할 때만 성립한다.
참조형 데이터의 가변은 내부 프로퍼티를 변경할 때만 성립한다. 데이터 자체를 변경(새 데이터 할당)하고자 하면 기본형 데이터와 마찬가지로 기존 데이터는 변하지 않는다.
-> 내부 프로퍼티를 변경할 필요가 있을 때마다 객체 역시 불변성을 확보하는 방법이 필요.
객체의 가변성에 따른 문제점
var user = {
name: "Jaenam",
gender: "male"
}
var changeName = function(user, newName) {
var newUser = user;
newUser.name = newName;
return newUser;
}
var user2 = changeName(user, 'Jung');
console.log(user.name, user2.name) // Jung Jung
console.log(user === user2) // true
값으로 전달받은 객체에 변경을 가하더라도 원본 객체는 변하지 않아야 하는 경우 발생 -> 불변객체 필요
기존 정보를 복사해서 새로운 객체를 반환하는 함수(얕은 복사)
// 얕은 복사
var copyObject = function (target) {
var result = {};
for (var prop in target) {
result[prop] = target[prop]
}
return result;
}
copyObject를 이용한 객체 복사
프로퍼티 갯수에 상관없이 모든 프로퍼티를 복사하는 함수를 만들 수 있다.
물론 협업하는 모든 개발자들이 copyObject라는 얕은 복사를 사용하면 문제가 되지 않는다. (=user 객체가 곧 불변 객체)
하지만, 그 규칙을 지키지 않을 수도 있으므로 아예 프로퍼티를 변경할 수 없게 제약을 거는게 더 안전하다.
얕은 복사(shallow copy)는 바로 아래 단계의 값만 복사하는 방법이고, 깊은 복사(deep copy)는 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법이다.
중첩된 객체에 대한 깊은 복사
var user2 = copyObject(user);
user2.urls = copyObject(user.urls);
user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio); // false
user.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog); // false
-> 객체를 복사할 때 객체 내부의 모든 값을 복사해서 완전히 새로운 데이터를 만들고자 할 때, 기본형 데이터일 경우에는 그대로 복사하면 되지만 참조형 데이터는 다시 그 내부의 프로퍼티들을 복사해야 함
// 깊은 복사
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을 활용한 간단한 깊은 복사
(메서드나 숨겨진 프로퍼티인 proto나 getter/setter등과 같이 JSON으로 변경할 수 없는 프로퍼티들은 모두 무시)
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);
obj.a = 3;
obj.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]}
undefined와 null은 자바스크립트에서 모두 '없음'을 나타낸다. 의미는 같지만 차이점이 있고 사용하는 목적 또한 다르다.
사용자가 명시적으로 지정할 수도 있지만, 값이 존재하지 않을 때 자바스크립트 엔진이 자동으로 부여하는 경우도 있다.
자바스크립트 엔진은 사용자가 응당 어떤 값을 지정할 것이라고 예상되는 상황임에도 실제로 그러지 않을 때 undefined를 반환한다.
(1)의 경우에 대해 배열의 경우에는 조금 특이한 동작을 확인할 수 있다.
var arr1 = [];
arr1.length = 3;
console.log(arr1) // [empty * 3]
var arr2 = new Array(3)
console.log(arr2) // [empty * 3]
var arr3 = [undefined, undefined, undefined]
console.log(arr3) // [undefined, undefined, undefined]
arr1,arr2는 배열에 3개의 빈 요소를 확보했지만 확보된 각 요소에 문자 그대로 어떤 값도, undefined도 할당되어 있지않다. 이처럼 '비어있는 요소'와 'undefined'를 할당한 요소는 출력 결과부터 다르다.
비어있음을 명시적으로 나타낼 때 사용한다.
주의할 점은 typeof null이 object라는 점이다. 이는 자바스크립트 자체 버그이며 어떤 변수의 값이 Null인지 판별하기 위해서는 typeof 대신 다른 방식으로 접근해야한다.
undefined와 null의 비교
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
동등연산자(==)로는 실제로 null인지 undefined인지 비교로 알 수 없다. 따라서 일치 연산자(===)로 정확히 판별할 수 있다.
모던 자바스크립트 Deep Dive를 읽고 심화된 자바스크립트 책/강의를 들을까 하는데, 찾아보니까 인프런에 유인동님이나 정재남님이 유명하더라고요. 혹시 Hoeon님이 생각하시기에 코어 자바스크립트를 추천하시는 이유가 있을까요?