Books
코어 자바스크립트
1. 데이터 타입

01 데이터 타입


1.1 데이터 타입의 종류

  • 기본형 (prmimitive type)
    • 숫자 (number), 문자열 (string), 불리언 (boolean), null, undefined, symbol (ES6에서 추가)
  • 참조형 (reference type)
    • 객체 (object)
      • 배열 (array), 함수 (function), 정규표현식 (RegExp), 날짜 (Date), Map과 WeakMap (ES6에서 추가), Set과 WeakSet (ES6에서 추가)
  • 기본형과 참조형의 차이는 무엇일까?
    1. 기본형은 값이 담긴 주소값을 바로 복제한다. 참조형은 값이 담긴 주소값을 가리키는 주소값을 복제한다.
    2. 기본형은 불변성(Immutability)을 띈다.

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

1.2.1 메모리와 데이터

  • 메모리는 0과 1로 표현되는 조각인 bit(비트)의 집합체이다.
  • 바이트(byte)라는 단위로 8개의 비트를 묶어 검색 시간을 줄이고 한번에 표현할 수 있는 데이터의 양을 늘린다.
  • C/C++, Java 등의 정적 타입 언어는 부족한 메모리 용량을 효율적으로 사용하기 위해 데이터 타입별로 할당할 메모리 영역을 정해두었다.
  • JavaScript는 상대적으로 메모리 용량이 커진 상황에서 등장하여, 넉넉하게 할당했다. (예를 들어 숫자는 64비트)

1.2.2 식별자와 변수

  • 변수(variable)은 변할 수 있는 수(데이터)
  • 식별자(identifier)는 어떤 데이터를 식별하는 데 사용하는 이름 = 변수명

1.3 변수 선언과 데이터 할당

1.3.1 변수 선언

var a;
  • 위를 말로 풀면, "변할 수 있는 데이터를 만든다. 이 데이터의 식별자는 a로 한다"
  • 컴퓨터는 메모리에 비어있는 공간 하나를 확보하고, 이 공간의 이름(식별자)을 a라고 지정한다.

1.3.2 데이터 할당

var a;  // 변수 a 선언
a = 'abc';  // 변수 a에 데이터 할당
 
var a = 'abc';  // 변수 선언과 할당을 한 문장으로 표현
  • 실제 메모리에는 변수 영역과 데이터 영역으로 나뉘어 저장된다.
    1. 변수 영역에서 빈 공간을 확보하고 이 공간의 식별자를 a로 지정한다.
    2. 데이터 영역에 빈 공간을 확보하고 이 공간의 데이터 'abc'를 저장한다.
    3. 2번에서 확보한 데이터 영역의 주소 값을 1번에서 확보한 공간의 값으로 저장한다.
  • 왜 굳이 변수 영역과 데이터 영역을 분리했을까?
    • 데이터의 크기는 (특히 문자열의 경우) 정해진 규격이 없어 가변적이다.
    • 만약 한 공간에 데이터를 저장하면, 크기가 늘어났을 때 뒤에 있는 모든 데이터를 옮기고 다시 주소를 연결해야 하므로 비효율적이다
  • a += 'def';로 뒤에 문자를 추가하면, abcdef라는 문자열을 새로 만들어 새로운 공간에 저장하고 그 주소를 변수 공간에 연결한다.
    • 즉, 기존 데이터를 수정하는게 아니라 새로 만들고 재할당하는 식.

1.4 기본형 데이터와 참조형 데이터

1.4.1 불변값

  • 변수(variable)과 상수(constant)를 구분하는 성질은 '변경 가능성'
  • 불변값과 상수는 같은 개념이 아니다
  • 변수와 상수를 구분 짓는 변경 가능성은 변수 영역 메모리기 때문에 변수 공간에 다른 데이터를 재할당할 수 있는지 여부가 관건
  • 불변성 여부는 데이터 영역
  • 기본형은 모두 불변이라고 했는데, 한번 생성된 데이터 영역은 GC에 의해 해제될 때까지 영원히 변하지 않는다
    • 변수의 값을 추가하거나 수정하면 데이터 영역이 수정되는것이 아니라 새로 만들어지기 때문에

1.4.2 가변값

  • 참조형 데이터는 가변값인 경우가 많지만 설정에 따라(Object.defineProperty, Object.freeze 등) 변경 불가일 수 있고, 불변값일 수 있다.
  • 기본형과의 차이는 객체의 변수(프로퍼티) 영역이 별도로 존재한다는 것.
var obj = {
  x: 3,
  arr: [3,4,5]
};
  • 참조형 데이터의 메모리 할당
    1. 변수 영역의 빈공간을 확보하고 그 주소의 이름을 obj라고 지정한다.
    2. 데이터 영역에 빈공간을 확보하고, 이 데이터는 여러 개의 변수와 값이 있는 객체이기 때문에, 별도의 변수 영역을 마련하고 그 주소를 확보한 데이터 영역에 저장한다.
    3. 별도의 변수 영역의 이름을 각각 x, arr로 지정한다.
    4. 데이터 영역에서 숫자 3을 찾고 없으므로 새로운 데이터 영역을 만들어 3을 저장하고 이 주소를 3번에서 x가 저장된 변수 영역에 저장한다.
    5. arr은 데이터 그룹이기 때문에 별도의 변수 영역을 만들고 이 주소 정보를 데이터 영역에 저장한다
    6. 변수 영역에는 3개의 공간을 확보하고 각각 인덱스를 부여한다.
    7. 데이터 영역에서 숫자 3을 찾고 그 주소를 0번 인덱스의 변수영역에 저장한다.
    8. 데이터 영역에서 숫자 4를 찾고 없으므로 새로운 데이터 영역을 만들어 4를 저장하고 그 주소를 1번 인덱스 변수영역에 저장한다.
    9. 데이터 영역에서 숫자 5를 찾고 없으므로 새로운 데이터 영역을 만들어 5를 저장하고 그 주소를 2번 인덱스 변수영역에 저장한다.

1.4.3 변수 복사 비교

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;
  • 기본형 데이터를 복사한 b의 값을 바꿨더니 @1002값이 달라짐. 하지만 obj2의 프로퍼티 값을 바꿨더니 @1004의 값은 달라지지 않음
  • 대부분의 책에서 '기본형은 값을 복사하고 참조형은 주소값을 복사한다' 고 하지만 실제로는 모두 주소값을 복사하는 것이기 때문에 모든 데이터 타입은 참조형 데이터라고 보는게 맞다.
  • 다만 기본형은 주소값을 복사하는 과정이 한번만 일어나고, 참조형 데이터는 한 단계를 더 거치게 된다
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
 
obj2 = { c: 20, d: 'ddd' };
  • obj2에도 새로운 객체를 할당하여 값을 직접 변경한 경우 데이터 영역의 새로운 공간에 객체가 저장되기 때문에 객체에 대한 변경임에도 값이 달라진다.
  • 참조형 데이터가 가변값이라고 할 때는 내부 프로퍼티를 변경할 때만 성립

1.5 불변 객체

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

  • 참조형 데이터의 가변은 프로퍼티를 변경할 때만 성립.
  • 내부 프로퍼티를 변경할 필요가 있을 때마다 매번 새로운 객체를 만든다면? 객체 역시 불변성을 가질 수 있다.
  • 왜 불변 객체가 필요할까? - 값으로 전달받은 객체에 변경을 가해도 원본 객체는 변하지 않아야 할 때가 있기 때문에
var user = {
  name: 'Jaenam',
  gender: 'male'
};
 
var changeName = function (user, newName) {
  var newUser = use;
  newUser.name = newName;
  return newUser;
};
 
var user2 = changeName(user, 'Jung');
 
if (user !== user2) {
  console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // Jung Jung
console.log(user === user2);  // true

changeName 함수는 의도대로 새로운 유저 객체를 만들기 않는다.

var changeName = function (user, newName) {
  return {
    name: newName,
    gender: user.gender
  };
};

changeName 함수가 새로운 객체를 반환하도록 수정하면 된다. 다만 changeName함수는 변경할 필요가 없는 기존 객체의 프로퍼티 (gender)를 불필요하게 하드코딩해서 입력해야 한다.

var copyObject = function (target) {
  var result = {};
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
}
 
var user2 = copyObject(user);
user2.name = 'Jung';
 
console.log(user.name, user2.name); // Jaenam Jung

이런 간단한 함수를 만들어 객체를 복사하고 수정할 수 있다. 다만 이 코드를 사용하는 모든 개발자가 객체를 복사할 때는 copyObject를 반드시 사용해야하는 규칙을 지켜야한다. 그래서 immutable.js baobab.js 등과 같은 라이브러리를 사용하여 내장 객체가 아닌 라이브러리 내의 불변성을 지닌 별도 데이터 타입을 사용하도록 시스템적으로 제약을 거는 것이 안전하다.

1.5.2 얕은 복사와 깊은 복사

  • 얕은 복사(shallow copy)는 바로 아래 단계의 값만 복사하는 방법이고, 깊은 복사(deep copy)는 내부의 모든 값을 전부 복사하는 방법
  • 이전에 만든 copyObject는 얕은 복사를 수행한다. 즉 중첩된 객체에서 참조형 데이터가 있는 경우 주소만 복사한다는 의미, 이런 경우 사본을 변경하면 원본도 변경된다.
var copyObjectDeep = function (target) {
  var result = {};
  if (typeof target === 'object' && target !== null) {  // target !== null이 있는 이유는, typeof null === 'object'이기 때문이다.
    for (var prop in target) {
      result[prop] = copyObjectDeep(target[prop]);
    }
  } else {
    result = target;
  }
 
  return result;
}
  • target이 객체인 경우 재귀적으로 호출하여 참조형 데이터도 다른 객체를 참조할 수 있게 만든다.
  • 객체를 JSON 문자열로 전환했다가 다시 JSON 객체로 바꾸는 방법이 있다.
    • 다만 메서드(함수)나 __proto__, getter/setter 같이 JSON으로 변경할 수 없는 프로퍼티들은 모두 무시된다.
    • 순수한 정보를 다룰 때는 유용한 방법
var copyObjectViaJson = function (target) {
  return JSON.parse(JSON.stringify(target));
};

1.6 undefined와 null

자바스크립트 엔진이 자동으로 undefined를 부여하는 경우

  1. 값을 대입하지 않은 변수
  2. 객체 내부의 존재하지 않는 프로퍼티에 접근하려고 할 때
  3. return 문이 없거나 호출되지 않는 함수의 실행 결과
var a;
console.log(a);      // 1번 케이스
var obj = { a: 1};
console.log(obj.b);  // 2번 케이스
var func = function() {};
console.log(func());  // 3번 케이스

값을 대입하지 않은 배열의 경우는 조금 특이함

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]

'비어있는 요소', 'undefined를 할당한 요소'는 출력이 다르고 '비어있는 요소'는 순회 대상에서 제외된다. undefined를 명시적으로 할당한 배열은 기대한 대로 배열을 순회하지만, 비어있는 경우 어떤 처리도 없이 건너뛴다.

var arr1 = [undefined, 1];
var 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) => v + i);  // [NaN, 2]
arr2.map((v, i) => v + i);  // [empty, 2]

배열도 객체이기 때문에 자연스러운 현상이다. 존재하지 않는 프로퍼티에 대해서 순회할 수 없으니까 배열은 무조건 length 프로퍼티의 개수만큼 빈 공간을 확보할거 같지만 실제론 특정 인덱스에 값을 지정할 때 빈공간을 확보한다. 값으로서 할당된 undefined는 실존하는 데이터지만 엔진이 반환하는 undefined는 문자 그대로 값이 없음을 나타낸다.

'비어있음'을 명시적으로 나타내고 싶을 때는 null을 쓰면 된다. 이 규칙을 따르면 undefined는 엔진이 반환해주는 값으로서만 존재한다.

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

typeof nullobject이다. 이는 자바스크립트의 자체버그이다. ==을 쓰면 undefinednull을 구분할 수 없기 때문에 ===을 사용하자.