Chapter 1: Data Type


Core JavaScript(코어 자바스크립트) 내용 정리


01. 데이터 타입의 종류

1. Primitive type

  • Number, String, Boolean, null, undefined, Symbol
  • 값이 담긴 주솟값을 바로 복제
  • immutability

2. Reference type

  • Object, Array, Function, Date, RegExp, Map, WeakMap, Set, WeakSet
  • 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주솟값을 복제


02. 데이터 타입에 관한 배경지식

1. 메모리와 데이터

  • 1bit = 0 or 1
  • 1byte = 8bit
  • C/C++, 자바 등의 정적 타입 언어는 메모리의 낭비를 최소화하기 위해 데이터 타입별로 할당할 메모리 영역을 정해놓음
    (ex. short는 2bytes, int는 4bytes)
  • JavaScript는 메모리 용량이 과거보다 월등히 커진 상황에서 등장하여, 메모리 공간을 좀 더 넉넉하게 할당함
    (ex. 숫자의 경우 64bit(=8bytes) 확보)

    ⇒ 개발자가 형변환을 걱정해야 하는 상황이 훨씬 덜 발생

  • 모든 데이터는 바이트 단위의 식별자, 즉 memory address를 통해 서로 구분하고 연결할 수 있음

2. variable vs identifier

  • 변수 variable: 변경 가능한 데이터가 담길 수 있는 공간 또는 그릇 (number, string, object, array 등이 담길 수 있음)
  • 식별자 identifier: 어떤 데이터를 식별하는 데 사용하는 이름, 즉 변수명


03. 변수 선언과 데이터 할당

1. 변수 선언

  • “변할 수 있는 데이터를 만든다. 이 데이터의 identifier는 a로 한다”

    변수 선언 시, 컴퓨터는 메모리에서 비어있는 공간 하나를 확보. 이 공간의 identifier는 a라고 지정

    사용자가 a에 접근하고자 하면, 컴퓨터는 메모리에서 a라는 이름을 가진 주소를 검색해 해당 공간에 담긴 데이터를 반환

    var a;
    
    >>>
    --------------------
    |Address |   1003  |
    --------------------
    |Data    | name: a |
    |        | value:  |
    --------------------

2. 데이터 할당

  • 변수에 데이터를 할당할 때는 해당 위치에 데이터를 직접 저장하지 않음

    대신, 데이터를 저장하기 위한 별도의 메모리 공간을 확보해서 데이터를 저장하고, 그 주소를 변수 영역에 저장

    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만 사용 가능



04. 기본형 데이터와 참조형 데이터 (primitive / reference)

1. 불변값

  • 변수 variable vs 상수 constant : 변수 영역 메모리에서, 한 번 데이터 할당이 이뤄진 변수 공간에 다른 데이터를 재할당할 수 있는지.
  • 불변성 : 데이터 영역 메모리에서, 변경 가능성이 있는지.

    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 |                |               |
    ----------------------------------------------------------------------------------

2. 가변값

  • Primitive type data는 모두 불변값이지만, Reference type data는 기본적인 성질은 가변값인 경우가 많지만 설정에 따라 변경 불가능한 경우도 있고(ex. Object.defineProperty, Object.freeze 등), 아예 불변값을 활용하는 방안도 있음

  • 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       |
    -------------------------------------------------------------------------

3. 변수 복사 비교

  • 어떤 데이터 타입이든 변수에 할당하기 위해서는 주솟값을 복사해야 함 (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       |
    ----------------------------------------------------


05. 불변 객체 (immutable object)

1. 불변 객체를 만드는 간단한 방법

  • 값으로 전달받은 객체에 변경을 가하더라도 원본 객체는 변하지 않아야 하는 경우, immutable object 필요
  • Reference type data의 내부 property를 변경할 필요가 있을 때마다 매번 새로운 객체를 만들어 재할당하거나, 자동으로 새로운 객체를 만드는 도구(method나 library)를 활용한다면, 객체 역시 불변성 확보 가능
  • 객체의 property를 복사하는 함수를 만들어, 객체 내부의 변경이 필요할 때 무조건 이 함수를 사용하기로 합의한다면 객체가 불변 객체가 될 수 있음

    그러나 규칙은 깨지기 쉬우므로, 모두가 그 규칙을 따르지 않고는 property를 변경할 수 없게끔 시스템적으로 제약을 거는 편이 안전함

    cf) immutable.js, baobab.js 등의 library는, JS 내장 객체가 아닌 library 자체에서 불변성을 지닌 별도의 데이터 타입과 그에 따른 메서드를 제공

2. Shallow copy / Deep copy

  • 얕은 복사 shallow copy : 바로 아래 단계의 값만 복사
    깊은 복사 deep copy : 내부의 모든 값들을 하나하나 찾아서 전부 복사

  • shallow copy의 경우에는, 해당 객체에 직접 속한 property에 대해서는 복사해서 완전히 새로운 데이터가 만들어진 반면, 한 단계 더 들어간 내부 property들은 기존 데이터를 그대로 참조
  • 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를 복사하지 않게끔 할 수도 있음
  • ES5의 getter/setter를 복사하는 방법은 안타깝게도 ES6의 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] } }


06. undefined / null

1. undefined

  • 사용자가 면시적으로 지정할 수도 있지만, 값이 존재하지 않을 때 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;

2. null

  • JS 엔진에서 자동으로 undefined를 반환하는 경우와 직접 undefined를 할당하는 경우가 헷갈릴 수 있음 ⇒ undefined 대신 같은 의미를 가진 null 사용
  • typeof nullobject라는 점을 주의 (js 자체 버그)
  • 동등 연산자(equality operator, ==)로 비교할 경우 null과 undefined가 서로 같다고 판단

    일치 연산자(identity operator, ===)를 사용해야만 정확히 null인지 판별 가능



Reference

Core JavaScript(코어 자바스크립트)