[Core JavaScript] | April 18, 2021
Core JavaScript(코어 자바스크립트) 내용 정리
JavaScript는 메모리 용량이 과거보다 월등히 커진 상황에서 등장하여, 메모리 공간을 좀 더 넉넉하게 할당함
(ex. 숫자의 경우 64bit(=8bytes) 확보)
⇒ 개발자가 형변환을 걱정해야 하는 상황이 훨씬 덜 발생
“변할 수 있는 데이터를 만든다. 이 데이터의 identifier는 a로 한다”
변수 선언 시, 컴퓨터는 메모리에서 비어있는 공간 하나를 확보. 이 공간의 identifier는 a라고 지정
사용자가 a에 접근하고자 하면, 컴퓨터는 메모리에서 a라는 이름을 가진 주소를 검색해 해당 공간에 담긴 데이터를 반환
var a;
>>>
--------------------
|Address | 1003 |
--------------------
|Data | name: a |
| | value: |
--------------------
변수에 데이터를 할당할 때는 해당 위치에 데이터를 직접 저장하지 않음
대신, 데이터를 저장하기 위한 별도의 메모리 공간을 확보해서 데이터를 저장하고, 그 주소를 변수 영역에 저장
var a;
a = 'abc';
var a = 'abc';
>>>
------------------------------------------
|Address | 1003(변수영역) | 5004(데이터영역) |
------------------------------------------
|Data | name: a | 'abc' |
| | value: @5004 | |
------------------------------------------
변수 영역에 값을 직접 대입하지 않는 이유: 자유로운 데이터 변환 + 효율적인 메모리 관리
JS는 숫자형 데이터에 대해 64bits(8bytes)의 공간을 확보하지만, 문자열은 정해진 규격이 없으므로 필요한 메모리 용량이 가변적
따라서 데이터 변환시, 확보된 공간을 변환된 데이터 크기에 맞게 늘리는 작업이 필요 ⇒ 효율적인 데이터 변환을 위해 변수와 데이터를 별도의 공간에 나누어 저장
데이터가 변경되면, 기존 데이터에 추가하든 제거하든 상관 없이 무조건 새로 만들어 별도의 공간에 저장
기존의 데이터(@5004)는 자신의 주소를 저장하는 변수가 하나도 없게 되면 garbage collector의 수거 대상이 됨
----------------------------------------------------------
|Address | 1003(변수영역) | 5004(데이터영역) | 5005(데이터영역) |
----------------------------------------------------------
|Data | name: a | 'abc' | 'abcdef' |
| | value: @5005 | | |
----------------------------------------------------------
중복 데이터에 대한 효율적인 처리
만약 500개의 변수에 숫자 5를 할당한다면, 숫자형은 8bytes가 필요하므로 총 4000(500*8)bytes가 필요
그러나 5를 별도의 공간에 한 번만 저장하고 그 주소를 입력한다면, 주소 공간의 크기가 2bytes일 때 1008(500*2 + 8)bytes만 사용 가능
불변성 : 데이터 영역 메모리에서, 변경 가능성이 있는지.
Primitive type인 Number, String, Boolean, null, undefined, Symbol 모두 불변값
변경은 새로 만드는 동작을 통해서만 이루어짐.
한 번 만들어진 값은 garbage collecting을 당하지 않는 한 영원히 변하지 않음.
var a = 'abc';
a = a + 'def';
>>>
// 'abc' 자체가 'abcdef'로 바뀌는 것이 아니라, 새로운 문자열을 만들어서 새로운 주소를 변수 a에 저장
----------------------------------------------------------
|Address | 1003(변수영역) | 5004(데이터영역) | 5005(데이터영역) |
----------------------------------------------------------
|Data | name: a | 'abc' | 'abcdef' |
| | value: @5005 | | |
----------------------------------------------------------
var b = 5;
var c = 5; // 위의 b와 같은 수인 5를 데이터 영역에서 찾고, 이미 위에서 만들어놓은 값이 있으니 그 주소를 재활용
b = 7; // 기존에 저장된 5를 7로 바꾸는 것이 아니라, 기존에 저장했던 7을 찾아서 있으면 재활용하고 없으면 새로 만들어서 b에 저장
>>>
----------------------------------------------------------------------------------
|Address | 1003(변수영역) | 1004(변수영역) | 5004(데이터영역) | 5005(데이터영역) |
----------------------------------------------------------------------------------
|Data | name: b | name: c | 5 | 7 |
| | value: @5004 | value: @5004 -> @5005 | | |
----------------------------------------------------------------------------------
Reference type data의 할당
Primitive type과의 차이는 ‘객체의 변수(property) 영역’이 별도로 존재한다는 것
객체가 별도로 할애한 영역은 변수 영역일 뿐, 데이터 영역은 기존의 메모리 공간을 그대로 활용.
데이터 영역에 저장된 값은 모두 불변값이지만, 변수에는 다른 값을 얼마든지 대입 가능(mutable)
var obj1 = {
a: 1,
b: 'bbb'
};
>>>
// 1. @1002 확보
// 2. @5001에 데이터를 저장하려고 보니, 여러 개의 property로 이뤄진 데이터 그룹.
// 이 그룹 내부의 property를 저장하기 위해 별도의 '변수 영역'을 마련하고 그 영역의 주소(@7103~?)를 저장
// (객체의 property들을 저장하기 위한 메모리 영역은 크기가 정해져 있지 않고 필요한 시점에 동적으로 확보)
// 3. @7103, @7104에 각각 a와 b라는 property 이름을 지정
// 4. '데이터 영역'에서 숫자 1과 'bbb'를 검색해서, 없으면 새롭게 저장 후 주소를 @7103, @7104에 각각 저장
---------------------------------------------------------------------------
|Address | 1002(변수영역) | 5001(데이터영역) | 5003(데이터영역) | 5004(데이터영역) |
---------------------------------------------------------------------------
|Data | name: obj1 | @7103 ~ ? | 1 | 'bbb' |
| | value: @5001 | | | |
---------------------------------------------------------------------------
----------------------------------------------------
|Address | 7103(변수영역-@5001) | 7104(변수영역-@5001) |
----------------------------------------------------
|Data | name: a | name: b |
| | value: @5003 | value: @5004 |
----------------------------------------------------
Reference type data의 property 재할당
재할당 시, ‘새로운 객체’가 만들어지는 것이 아니라 기존의 객체 내부의 값만 바뀜
아래에서, 변수 obj1이 바라보고 있는 주소는 @5001로 변하지 않음
var obj1 = {
a: 1,
b: 'bbb'
};
obj1.a = 2;
>>>
-------------------------------------------------------------------------------------------
|Address | 1002(변수영역) | 5001(데이터영역) | 5003(데이터영역) | 5004(데이터영역) | 5005(데이터영역) |
-------------------------------------------------------------------------------------------
|Data | name: obj1 | @7103 ~ ? | 1 | 'bbb' | 2 |
| | value: @5001 | | | | |
-------------------------------------------------------------------------------------------
-------------------------------------------------------
|Address | 7103(변수영역-@5001) | 7104(변수영역-@5001) |
-------------------------------------------------------
|Data | name: a | name: b |
| | value: @5003 -> @5005 | value: @5004 |
-------------------------------------------------------
Nested object (중첩 객체)
Reference type data의 property에 다시 reference type data를 할당하는 경우
var obj = {
x: 3,
arr: [ 3, 4, 5 ]
};
>>>
------------------------------------------------------------------------------------------------------------
|Address | 1002(변수영역) | 5001(데이터영역) | 5002(데이터영역) | 5003(데이터영역) | 5004(데이터영역) | 5005(데이터영역) |
------------------------------------------------------------------------------------------------------------
|Data | name: obj | @7103 ~ ? | 3 | @8104 ~ ? | 4 | 5 |
| | value: @5001 | | | | | |
------------------------------------------------------------------------------------------------------------
----------------------------------------------------
|Address | 7103(변수영역-@5001) | 7104(변수영역-@5001) |
----------------------------------------------------
|Data | name: x | name: arr |
| | value: @5002 | value: @5003 |
----------------------------------------------------
// Array의 요소가 총 3개이므로 3개의 '변수 공간'을 확보하고 각각 인덱스를 부여(0,1,2)
-------------------------------------------------------------------------
|Address | 8104(변수영역-@5003) | 8105(변수영역-@5003) | 8106(변수영역-@5003) |
-------------------------------------------------------------------------
|Data | name: 0 | name: 1 | name: 2 |
| | value: @5002 | value: @5004 | value: @5005 |
-------------------------------------------------------------------------
obj.arr[1]
을 검색할 때 메모리의 검색 과정: @1002 → @5001 → (@7103~?) → @7104 → @5003 → (@8104~?) → @8105 → @5004 → 4 반환Nested object의 property 재할당
참조 카운트(자신의 주소를 참조하는 변수의 개수)가 0인 메모리 주소는 garbage collector(GC)의 수거 대상이 됨
var obj = {
x: 3,
arr: [ 3, 4, 5 ]
};
obj.arr = 'str';
>>>
// @5003이 GC 수거 대상이 됨
-----------------------------------------------------------------------------------------------------------------------------
|Address | 1002(변수영역) | 5001(데이터영역) | 5002(데이터영역) | 5003(데이터영역) | 5004(데이터영역) | 5005(데이터영역) | 5006(데이터영역) |
-----------------------------------------------------------------------------------------------------------------------------
|Data | name: obj | @7103 ~ ? | 3 | @8104 ~ ? | 4 | 5 | 'str' |
| | value: @5001 | | | | | | |
-----------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------
|Address | 7103(변수영역-@5001) | 7104(변수영역-@5001) |
-------------------------------------------------------
|Data | name: x | name: arr |
| | value: @5002 | value: @5003 -> @5006 |
-------------------------------------------------------
// @5003에 담긴 데이터 값이 사라지면서, 아래의 @8104~?도 참조 카운트가 0이 되어 GC 수거 대상이 됨
-------------------------------------------------------------------------
|Address | 8104(변수영역-@5003) | 8105(변수영역-@5003) | 8106(변수영역-@5003) |
-------------------------------------------------------------------------
|Data | name: 0 | name: 1 | name: 2 |
| | value: @5002 | value: @5004 | value: @5005 |
-------------------------------------------------------------------------
어떤 데이터 타입이든 변수에 할당하기 위해서는 주솟값을 복사해야 함 (primitive type도 결국 주솟값을 참조)
다만, primitive type은 주솟값을 복사하는 과정이 한 번만 이뤄지고, reference type은 한 단계를 더 거침
Primitive type
var a = 10;
var b = a;
>>>
---------------------------------------------------------
|Address | 1001(변수영역) | 1002(변수영역) | 5001(데이터영역) |
---------------------------------------------------------
|Data | name: a | name: b | 10 |
| | value: @5001 | value: @5001 | |
---------------------------------------------------------
var a = 10;
var b = a;
b = 15;
>>> // @1001과 @1002의 값이 달라짐 (a !== b)
----------------------------------------------------------------------------------
|Address | 1001(변수영역) | 1002(변수영역) | 5001(데이터영역) | 5004(데이터영역) |
----------------------------------------------------------------------------------
|Data | name: a | name: b | 10 | 15 |
| | value: @5001 | value: @5001 -> @5004 | | |
----------------------------------------------------------------------------------
Reference type (내부 property를 변경)
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
>>>
------------------------------------------------------------------------------------------
|Address | 1003(변수영역) | 1004(변수영역) | 5001(데이터영역) | 5002(데이터영역) | 5003(데이터영역) |
------------------------------------------------------------------------------------------
|Data | name: obj1 | name: obj2 | 10 | @7103 ~ ? | 'ddd' |
| | value: @5002 | value: @5002 | | | |
------------------------------------------------------------------------------------------
----------------------------------------------------
|Address | 7103(변수영역-@5002) | 7104(변수영역-@5002) |
----------------------------------------------------
|Data | name: c | name: d |
| | value: @5001 | value: @5003 |
----------------------------------------------------
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
obj2.c = 20;
>>> // @1003과 @1004의 값이 달라지지 않음 (obj1 === obj2)
-----------------------------------------------------------------------------------------------------------
|Address | 1003(변수영역) | 1004(변수영역) | 5001(데이터영역) | 5002(데이터영역) | 5003(데이터영역) | 5005(데이터영역) |
-----------------------------------------------------------------------------------------------------------
|Data | name: obj1 | name: obj2 | 10 | @7103 ~ ? | 'ddd' | 20 |
| | value: @5002 | value: @5002 | | | | |
-----------------------------------------------------------------------------------------------------------
-------------------------------------------------------
|Address | 7103(변수영역-@5002) | 7104(변수영역-@5002) |
-------------------------------------------------------
|Data | name: c | name: d |
| | value: @5001 -> @5005 | value: @5003 |
-------------------------------------------------------
Reference type (객체 자체를 변경)
obj2에도 새로운 객체를 할당함으로써 값을 직접 변경 ⇒ 메모리의 ‘데이터 영역’의 새로운 공간에 새 객체가 저장되고 그 주소를 ‘변수 영역’의 obj2 위치에 저장
객체에 대한 변경임에도 값이 달라질 수 있다는 점!
즉, Reference type data가 ‘가변값’이라고 설명할 때의 ‘가변’은 reference data 자체를 변경할 경우가 아니라, 그 내부의 property를 변경할 때만 성립
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
>>>
------------------------------------------------------------------------------------------
|Address | 1003(변수영역) | 1004(변수영역) | 5001(데이터영역) | 5002(데이터영역) | 5003(데이터영역) |
------------------------------------------------------------------------------------------
|Data | name: obj1 | name: obj2 | 10 | @7103 ~ ? | 'ddd' |
| | value: @5002 | value: @5002 | | | |
------------------------------------------------------------------------------------------
----------------------------------------------------
|Address | 7103(변수영역-@5002) | 7104(변수영역-@5002) |
----------------------------------------------------
|Data | name: c | name: d |
| | value: @5001 | value: @5003 |
----------------------------------------------------
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
obj2 = { c: 20, d: 'ddd' };
>>> // @1003과 @1004의 값이 달라짐 (obj1 !== obj2)
------------------------------------------------------------------------------------------------------------------------------------
|Address | 1003(변수영역) | 1004(변수영역) | 5001(데이터영역) | 5002(데이터영역) | 5003(데이터영역) | 5005(데이터영역) | 5006(데이터영역) |
------------------------------------------------------------------------------------------------------------------------------------
|Data | name: obj1 | name: obj2 | 10 | @7103 ~ ? | 'ddd' | 20 | @8204 ~ ? |
| | value: @5002 | value: @5002 -> @5006 | | | | | |
------------------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------
|Address | 7103(변수영역-@5002) | 7104(변수영역-@5002) |
----------------------------------------------------
|Data | name: c | name: d |
| | value: @5001 | value: @5003 |
----------------------------------------------------
----------------------------------------------------
|Address | 8204(변수영역-@5006) | 8205(변수영역-@5006) |
----------------------------------------------------
|Data | name: c | name: d |
| | value: @5005 | value: @5003 |
----------------------------------------------------
객체의 property를 복사하는 함수를 만들어, 객체 내부의 변경이 필요할 때 무조건 이 함수를 사용하기로 합의한다면 객체가 불변 객체가 될 수 있음
그러나 규칙은 깨지기 쉬우므로, 모두가 그 규칙을 따르지 않고는 property를 변경할 수 없게끔 시스템적으로 제약을 거는 편이 안전함
cf) immutable.js, baobab.js 등의 library는, JS 내장 객체가 아닌 library 자체에서 불변성을 지닌 별도의 데이터 타입과 그에 따른 메서드를 제공
Primitive type일 경우에는 그대로 복사하면 되지만, Reference type일 경우에는 다시 그 내부의 property들을 계속 재귀적으로 복사해야만 깊은 복사가 가능
var copyObjectDepp = 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;
};
hasOwnProperty
method를 활용해 prototype chaining을 통해 상속된 property를 복사하지 않게끔 할 수도 있음Object.getOwnPropertyDescriptor
또는 ES2017의 Object.getOwnPropertyDescriptors
외에는 마땅한 방법이 없음JSON을 활용한 간단한 deep copy
객체를 JSON 문법으로 표현된 문자열로 전환했다가 다시 JSON 객체로 변환
다만, method
(function
)나 숨겨진 property인 __proto__
나 getter/setter
등과 같이 JSON으로 변경할 수 없는 property들은 모두 무시
httpRequest로 받은 데이터를 저장한 객체를 복사할 때 등 순수한 정보만 다룰 때 활용하기 좋은 방법
var copyObjectViaJSON = function (target) {
return JSON.parse(JSON.stringify(target));
};
var obj = {
a: 1,
b: {
c: [1, 2],
func1: function() { console.log(3); }
}
}
var obj2 = copyObjectViaJSON(obj);
obj2.a = 3;
obj.b.c[1] = 3;
console.log(obj); // { a: 1, b: { c: [1, 3], fun1: f() } }
console.log(obj2); // { a: 3, b: { c: [1, 2] } }
사용자가 면시적으로 지정할 수도 있지만, 값이 존재하지 않을 때 JS 엔진이 자동으로 부여하는 경우도 있음
// 값을 대입하지 않은, 데이터 영역의 메모리 주소를 지정하지 않은 식별자 접근 시
var a; // 아무것도 할당하지 않음
console.log(a); // 변수 a에 접근하고자 할 때 비로소 undefined를 반환
// 객체 내부의 존재하지 않는 property에 접근 시
var obj = { a: 1};
console.log(obj.b);
// return 문이 없거나 호출되지 않는 함수 실행 결과
var func = function(){};
var c = func();
console.log(c);
‘비어있는 요소’와 ‘undefined를 할당한 요소’는 다름
배열도 객체이므로 존재하지 않는 property에 대해서는 순회할 수 없음
배열은 무조건 length property의 개수만큼 빈 공간을 확보하고 각 공간에 인덱스를 이름으로 지정할 것이라고 생각하기 쉽지만, 실제로는 객체와 마찬가지로 특정 인덱스에 값을 지정할 때 비로소 빈 공간을 확보하고 인덱스를 이름으로 지정하고 데이터의 주솟값을 저장하는 동작을 함
즉, 값이 지정되지 않은 인덱스는 ‘아직은 존재하지 않는 property’
// undefined를 할당한 요소 (값으로써 어딘가에 할당된 실존하는 데이터)
var arr1 = [undefined, 1];
// 비어있는 요소 (문자 그대로 값이 없음을 의미)
var arr2 = [];
arr2[1] = 1;
typeof null
이 object
라는 점을 주의 (js 자체 버그)동등 연산자(equality operator, ==)로 비교할 경우 null과 undefined가 서로 같다고 판단
일치 연산자(identity operator, ===)를 사용해야만 정확히 null인지 판별 가능