자바스크립트 데이터 타입은 기본형(원시형, primitive type)과 참조형(reference type)이 있다. 할당 시 기본형은 값이 담긴 주솟값을 바로 복제, 참조형은 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주소값을 복제한다.
기본형
참조형
Symbol
Map과 WeakMap Set과 WeakSet
→ Map에서 객체를 키로 사용한 경우 Map이 메모리에 있는 한 객체도 메모리에 남으며 가비지 컬렉터의 대상이 되지 않는다.
→ WeakMap을 사용하면 키로 쓰인 객체가 가비지 컬렉션의 대상이 된다.(WeakMap의 키는 항상 객체이다)
→ WeakMap은 반복과 keys()
, values()
, entries()
메서드를 지원하지 않으며 키나 값 전체를 얻는 게 불가능하다.
→ WeakSet은 객체만 저장할 수 있다. WeakMap과 같이 Set 안의 객체가 도달 가능할 때만 메모리에서 유지된다.
https://ko.javascript.info/weakmap-weakset
💡 기본형은 불변성의 띈다.바이트 역시 비트와 같이 시작하는 비트의 식별자로 위치를 파악할 수 있다.
모든 데이터는 바이트 단위의 식별자 즉 메모리 주솟값을 통해 서로 구분하고 연결할 수 있다.
변수 : 변할 수 있는 무언가
식별자: 어떤 데이터를 식별하는 데 사용하는 이름, 변수명
선언 : 공간을 확보하고 변수명과 주소를 매칭시키는 과정
할당 : 해당 변수가 가리키는 주소의 공간에 데이터를 저장하는 과정
var a; // 식별자를 a로 하는 변할 수 있는 데이터를 만든다.
변수란 결국 변경 가능한 데이터가 담길 수 있는 공간 또는 그릇을 말한다.
데이터 할당에 따른 메모리 영역의 변화는 아래와 같은 순서로 이뤄진다.
주소 | … | 1002 | 1003 | … |
---|---|---|---|---|
데이터 | 이름: a 값: |
이후 사용자가 a에 접근하고자 하면 컴퓨터는 메모리에서 a라는 이름을 가진 주소를 검색해 해당 공간에 담긴 데이터를 반환한다.
var a; // 변수 a 선언
a = 'abc'; // 변수 a에 데이터 할당
var a = 'abc'; //변수 선언과 할당을 한 문장으로 표현
해당 위치에 문자열 ‘abc’를 직접 저장하지는 않으며 데이터를 저장하기 위한 별도의 메모리 공간을 다시 확보해서 문자열 ‘abc’를 저장하고 그 주소를 변수 영역에 저장한다.
변수영역 | 주소 | … | 1002 | 1003 | … |
---|---|---|---|---|---|
데이터 | 이름: a, 값:@5003 |
데이터 영역 | 주소 | … | 5003 | 5004 | … |
---|---|---|---|---|---|
데이터 | ‘abc’ | ‘abcedf’ |
한 번에 저장하지 않는 이유?
⇒ 데이터 변환을 자유롭게 + 메모리를 효율적으로 관리
불변성 여부를 구분할 때의 변경 가능성의 대상은 데이터 영역 메모리이다.
즉 불변값이란 메모리 영역에서의 변경이 불가능한 값을 의미한다.
기본형 데이터는 모두 불변값이다.
var a = 'abc'
a = a + 'def'
위 코드에서 a를 보면 ‘abc’에서 ‘abcdef’로 변경된 것처럼 보이지만 이는 메모리의 값이 변경된 것이 아니라 식별자와 연결되어 있는 메모리가 변경된 것이다. ‘abc’와 ‘abcdef’는 완전히 별개의 데이터이다.
변경은 새로 만드는 동작으로 통해서만 이뤄지며 이것이 불변값의 성질이다. 한 번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않는다.
참조형 데이터는 가변값이다.
(물론 설정에 따라 변경 불가능한 경우도 있고 아예 불변값으로 활용하는 방안도 있다)
참조형 데이터의 할당 과정은 아래와 같다.
var obj = { a: 1, b: 'bbb' }
변수영역 | 주소 | … | 1002 | 1003 | … |
---|---|---|---|---|---|
데이터 | 이름: obj, 값:@5003 |
데이터 영역 | 주소 | … | 5003 | 5004 | 5005 | 5006 |
---|---|---|---|---|---|---|
데이터 | @7103 ~ ? | 1 | ‘bbb’ | 2 |
객체 @5003의 변수 영역 | 주소 | … | 7103 | 7104 | … |
---|---|---|---|---|---|
데이터 | 이름: a, 값: @5004 | 이름: b, 값: @5005 |
기본형 데이터와의 차이는 객체의 변수(프로퍼티) 영역이 별도로 존재한다는 점이다. 즉 참조형 데이터의 경우 객체는 어딘가에 따로 저장되어 있고 그 객체가 저장된 주소만을 참조한다. 데이터 영역에 저장된 값은 모두 불변 값이다. 하지만 변수에는 다른 값을 얼마든지 대입할 수 있다. 이 때문에 참조형 데이터는 가변값이라고 한다.
obj.a = 2;
객체 내부의 프로퍼티를 변경하게 될 경우 @1002에서 참조하고 있는 값은 변경사항이 없으며 @7103에서 참조하고 있는 주소만이 변하게 된다.
즉 새로운 객체가 만들어진 것이 아니라 기존의 객체 내부의 값만 변경된다.
중첩 객체의 프로퍼티 할당 과정은 아래와 같다.
var obj = { a: 1, arr: [1, 2, 3] }
변수영역 | 주소 | … | 1002 | 1003 | … |
---|---|---|---|---|---|
데이터 | 이름: obj, 값:@5003 |
데이터 영역 | 주소 | 5003 | 5004 | 5005 | 5006 | 5007 |
---|---|---|---|---|---|---|
데이터 | @7103 ~ ? | 1 | @8103 | 2 | 3 |
객체 @5003의 변수 영역 | 주소 | … | 7103 | 7104 | … |
---|---|---|---|---|---|
데이터 | 이름: a, 값: @5004 | 이름: arr, 값: @5005 |
배열 @5005의 변수 영역 | 주소 | … | 8103 | 8104 | 8105 |
---|---|---|---|---|---|
데이터 | 이름: 0, 값: @5004 | 이름: 1, 값: @5006 | 이름: 2, 값: @5007 |
위 상태에서 재할당을 하게 될 경우 새로 공간을 확보하여 값을 저장하고 변수영역에서 가르키는 주소를 변경하게 된다.
이 때 만약 메모리 주소의 참조 카운트가 0이 되면 메모리 주소는 가비지 컬렉터의 수거 대상이 된다.
var a = 10;
var b = a;
var obj1 = { c: 10, d: 'ddd'};
var obj2 = obj1;
변수영역 | 주소 | 1001 | 1002 | 1003 | 1004 |
---|---|---|---|---|---|
데이터 | 이름: a, 값:@5001 | 이름: b, 값:@5001 | 이름: obj1, 값:@5002 | 이름: obj2, 값:@5002 |
데이터 영역 | 주소 | 5001 | 5002 | 5003 | 5004 |
---|---|---|---|---|---|
데이터 | 10 | @7103 ~ ? | ‘ddd’ |
객체 @5001의 변수 영역 | 주소 | 7103 | 7104 | 7105 | 7104 |
---|---|---|---|---|---|
데이터 | 이름: c, 값: @5001 | 이름: d, 값: @5003 |
변수를 복사하는 과정은 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보게 되는 점에서 동일하다.
복사 과정은 동일하지만 데이터 할당 과정에서 차이가 있으므로 변수 복사 이후 동작에 큰 차이가 발생한다.
b = 15;
obj2.c = 20;
변수영역 | 주소 | 1001 | 1002 | 1003 | 1004 |
---|---|---|---|---|---|
데이터 | 이름: a, 값:@5001 | 이름: b, 값:@5004 | 이름: obj1, 값:@5002 | 이름: obj2, 값:@5002 |
데이터 영역 | 주소 | 5001 | 5002 | 5003 | 5004 | 5005 |
---|---|---|---|---|---|---|
데이터 | 10 | @7103 ~ ? | ‘ddd’ | 15 | 20 |
객체 @5001의 변수 영역 | 주소 | 7103 | 7104 | 7105 | 7104 |
---|---|---|---|---|---|
데이터 | 이름: c, 값: @5005 | 이름: d, 값: @5003 |
기본형 데이터를 복사한 변수 b의 값을 변경하면 @1002의 값이 변경된다. 하지만 참조형 데이터를 복사한 변수 obj2의 프로퍼티 값을 변경하면 @1004의 값은 변경되지 않는다. 즉 변수 a, b 는 서로 다른 주소를 바라보게 되었으나 obj1, obj2는 여전히 같은 객체를 바라보고 있다.
단 내부 프로퍼티를 변경하는 것이 아닌 객체 자체를 변경하게 될 경우 새로운 객체를 할당함으로써 객체에 대한 변경임에도 값이 달라지게 된다.
즉 참조형 데이터가 가변값이라고 할 때 가변은 참조형 데이터 자체를 변경할 경우가 아니라 그 내부의 프로퍼티를 변경할 때만 성립한다.
값으로 전달받은 객체에 변경을 가하더라도 원본 객첸느 변하지 않아야 할 때 불변 객체가 필요하다.
앞서 봤듯이 참조형 데이터의 가변은 데이터 자체가 아닌 내부 프로퍼티를 변경할 때만 성립한다. 데이터 자체를 변경하고자 하면 기본형 데이터와 마찬가지로 기본 데이터는 변하지 않는다. 그렇다면 내부 프로퍼티를 변경할 필요가 있을 때마다 매번 새로운 객체를 만들어 재할당하기로 규칙을 정하거나 자동으로 객체를 만드는 도구(immutable.js, immer.js, immutability-helper)를 활용한다면 객체 역시 불변성을 확보할 수 있을 것이다.
기존 정보를 복사해서 새로운 객체를 반환하는 함수(얕은 복사)
var copyObject = function (target) {
var result = {}
for(var prop in target) {
result[prop] = target[prop];
}
return result;
};
얕은 복사
깊은 복사
cloneDeep
Array.prototype.slice()
slice()
는 원본을 대체하지 않습니다. 원본 배열에서 요소의 얕은 복사본을 반환합니다.
즉 slice()
는 중첩 객체의 깊은 복사를 수행할 수 없다.
**Object.assign()**
Object.assign()
은 속성의 값을 복사하기 때문에 깊은 복사를 수행하려면 다른 방법을 사용해야 한다.
Object.assign(새로운 객체, 복사할 객체)
const object = {
a: "a",
number: {
one: 1,
two: 2,
},
};
const copy = Object.assign({}, object);
copy.number.one = 3;
console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // true
재귀함수
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;
}
깊은 복사를 하기 위해서는 target이 객체인 경우 내부 프로퍼티들을 순회하며 copyObjectDeep 함수를 재귀적으로 호출하고 객체가 아닌 경우 target을 그대로 지정하게끔 해야 한다. 해당 함수를 사용해 객체를 복사한 경우 원본과 사본이 서로 완전히 다른 객체를 참조하게 되어 어느 쪽의 프로퍼티를 변경하더라도 다른 쪽에 영향을 주지 않는다.
JSON.parse & JSON.stringify
JSON.stringify()
는 객체를 json 문자열로 변환하는데 이 과정에서 원본 객체와의 참조가 모두 끊어진다. 객체를 json 문자열로 변환 후 JSON.parse()를 이용해 다시 객체로 변환한다. 간단하지만 다른 방법에 비해 느리며 함수나 숨겨진 프로퍼티와 같은 JSON으로 변경할 수 없는 프로퍼티는 모두 무시된다. 따라서 순수한 정보만 다를 때 활용하기 좋다.
const object = {
a: "a",
number: {
one: 1,
two: 2,
},
};
const copy = JSON.parse(JSON.stringify(object));
console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false
Lodash
→ A modern JavaScript utility library delivering modularity, performance & extras.
.cloneDeep
은 .clone
을 재귀적으로 수행해주는 함수
hasOwnProperty() : 해당 객체에 특정 프로퍼티가 존재하면 true, 그렇지 않으면 false 를 반환한다.
단, 프로토타입 체인은 확인하지 않고, 해당 객체가 스스로 정의한 프로퍼티만을 판단한다.
undefined와 null은 자바스크립트에서 없음을 나타내는 두 가지 값이지만 의미와 목적이 다르다.
undefined
undefined는 사용자가 명시적으로 지정할 수도 있지만 값이 존재하지 않을 겨우 자바스크립트 엔진이 자동으로 부여하는 경우도 있다.
자바스크립트 엔진은 사용자가 응당 어떤 값을 지정할 것이라고 예상되는 상황임에도 실제로는 그렇게 하지 않았을 때 undefined를 반환한다.
let arr1 = [undefined, 1]
let arr2 = []
arr2[1] = 1
arr1.forEach((v, i) => console.log(v, i)) // undefined 0 / 1 1
arr2.forEach((v, i) => console.log(v, i)) // 1 1
arr1.map((v, i) => {return v + i}) // [NaN, 2]
arr2.map((v, i) => {return v + i}) // [empty, 2]
arr1.filter((v) => {return !v}) //[undefined]
arr2.filter((v) => {return !v}) //[]
arr1.reduce((p, c, i) => {return p + c + i}, '') // undefined011
arr2.reduce((p, c, i) => {return p + c + i}, '') // 11
비어있는 요소와 undefined를 할당한 요소는 출력 결과부터 다르다.
비어있는 요소는 순회와 관련된 많은 배열 메서드들의 순회 대상에서 제외된다. 이는 배열도 객체이므로 존재하지 않는 프로퍼티에 대해서는 순회할 수 없기 때문이다. 배열은 객체와 마찬가지로 특정 인덱스에 값을 지정할 때 비로소 빈 공간을 확보하고 인덱스를 이름으로 지정하고 데이터의 주소값을 저장한다.
즉 값이 지정되지 않은 인덱스는 아직은 존재하지 않는 프로퍼티에 지나지 않는다.
사용자가 명시적으로 부여한 경우의 undefined는 그 자체로 값이다. 비어있음을 의미하긴 하지만 하나의 값으로 동작하기 때문에 이때의 프로퍼티나 배열의 요소는 고유의 키값이 실존하게 되고 순회의 대상이 될 수 있다.
사용자가 아무것도 하지 않은 채로 접근했을 때 자바스크립트 엔진이 반환하는 undefined는 해당 프로퍼티 내지 배열의 키값 자체가 존재하지 않음을 의미한다.
값으로써 어딘가에 할당된 undefined는 실존하는 데이터인 반면, 자바스크립트 엔진이 반환해주는 undeined는 문자 그대로 값이 없음을 나타내는 것이다.
null
이처럼 복잡한 경우를 피하기 위해 null을 사용한다.
null은 식별되지 않은 것을 표현하며 즉 변수가 아무런 객체를 가리키지 않음을 표현한다.
비어있음을 명시적으로 나타내고 싶을 때는 undefined가 아닌 null을 쓰면 된다.
💡typeof null 은 object이다.
따라서 어떤 변수의 값이 null인지 여부를 판별하기 위해서는 typeof를 사용하면 안된다.
let 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
<코어 자바스크립트> 정재남, 위키북스(2019)