[JavaScript] 데이터 타입 (Data Types)


1. 데이터 타입의 종류

  1. 기본형(Primitive Type) : number, boolean, null, undefined, Symbol(ES6부터 추가) 등이 있음
  2. 참조형(Reference Type) : Object 하위 분류로 array, function, date, regExp, Map(ES6~), WeakMap(ES6~), Set(ES6~), WeakSet(ES6~)
  • 기본형 vs 참조형의 차이: 기본형은 값이 담긴 주솟값을 바로 복제 VS 참조형은 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주솟값을 복제

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

(1) 메모리와 데이터

  • 컴퓨터는 모든 데이터를 0,1로 바꿔 기억 (0 , 1로 표현할 수 있는 하나의 메모리 조각을 비트라고함)
  • JS는 메모리 공간을 좀 더 넉넉하게 할당함
  • 모든 데이터는 바이트 단위의 식별자 , 정확하게 메모리 주솟값을 통해 서로 구분하고 연결함

(2) 식별자와 변수

  • 변수 : 변할 수 있는 수
  • 식별자 : 어떤 데이터를 식별하는 데 사용하는 이름

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

(1) 변수 선언

  • 변수 : 변경 가능한 데이터가 담기는 공간
var a;

// 1. a는 메모리 @1003번 주소의 이름 a에 할당

(2) 데이터 할당

var a; // 변수 a 선언 , 마찬 가지로 @1003번 주소의 이름 a에 할당
a = "abc"; // 변수 a에 대해 데이터 할당

var a = "abc"; // 변수 선언과 할당을 한 문장으로 표현

//데이터 할당의 흐름은

// 1. 메모리 @1003번 주소를 할당
// 2. 식별자의 이름을 a로 지정
// 3. 데이터영역의 @5004번 주소에 'abc' 저장
// 4. 변수 영역에 a라는 식별자 검색 (@1003번)
// 5. (@5004)의 주소를 (@1003) 공간에 대입

그렇다면 왜 변수 영역에 직접 대입해서 할당하지 않고 데이터 영역 단계를 거칠까?

  • 데이터 변환의 자유로움과 메모리를 효율적으로 관리 하기 위한 결과다 ⇒ 만약 한번에 변수 영역에 직접 대입해서 처리하면 데이터 크기에 맞게 늘리는 작업이 필요하다, 이건 컴퓨터의 연산이 많아지는것이고 효율이 떨어진다라고 볼 수 있다 (최적)
// 예시로 500개의 변수를 생성해서 모든 변수에 숫자 5를 할당해야된다고 한다면 각 변수 공간 마다 숫자형(8바이트)를 할당해야한다

var a = 5;
var b = 5;
var c = 5;
var d = 5;
var e = 5;
var f = 5;

...

// 총 500 * 8 = 4000 바이트를 사용해야함
  • 5를 별도의 공간에 한번만 저장하고 해당 주소만 입력하게 된다면 주소 공간의 크기가 2바이트라고하면 (500 *2+8) = 1008바이트만 이용하면 된다 ⇒ 중복된 데이터 처리 효율도 올라간다

4. 기본형 데이터와 참조형 데이터

(1) 불변값

  • 기본형 데이터는 불변값(number, boolean, null, undefined…)
  • 변수(variable)와 상수(constant)를 구분하는 것은 변경 가능성 ⇒ 변수 영역의 메모리
  • 불변성 여부를 구분할 때의 변경 가능성 대상 ⇒ 데이터 영역 메모리
var a = "abc"; // a에 'abc' 할당
a = a + "def"; // 새로운 문자열 'abcdef'를 변수 a에 저장
  • 근데 ‘abc’와 ‘abcdef’는 완전 별개의 데이터

var b = 5; // b에 5를 할당
var c = 5; // c에 같은 5를 할당
  • 여기서 컴퓨터는 데이터 주소에서 b에 할당된 5의 데이터 주소를 재활용함

var b = 5; // b에 5를 할당

b = 7; //여기서 b를 7로 다시 바꿈
  • 기존 저장된 5자체를 7로 바꾸는게 아니라 기존에 저장했던 7를 찾아서 있으면 재활용 , 없으면 새로만듬.

문자열 값도 한 번 만든값을 바꿀수 없고 숫자 값도 다른 값으로 변경할 수 없음. 변경은 새로운 동작에서만 이뤄짐 ⇒ 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않음


(2) 가변값

  • 참조형 데이터는 기본적으로 가변값인 경우가 많지만 설정에 따라 변경 불가능 경우도 있고 아예 불변값으로 활용하는 방법이 있음
var obj1 = {
  a: 1,
  b: "bbb",
};

// 1. 변수 영역엔 @1002번 변수 영역의 주소에 할당
// 2. 데이터 영역의 주소 @5001번에 저장하려고 보니 여러개의 프로퍼티로 이루어진 데이터 그룹.
// 3. 이 그룹 내부의 프로퍼티들을 저장하기위해 별도의 변수 영역을 마련하고 데이터 영역 주소에(@7103~?)를 @5001에 저장함
// 4. 별도 @5001의 변수 영역에 @7103 @7104 영역 생성하고 a,b를 각각 프로퍼티 지정
// 5. 데이터 영역에서 숫자 1을 검색하면  검색결과가 없으므로 임의로 @5003에 저장 , 'bbb'도 마찬가지로 @5004에 저장
  • 1000번대는 변수 영역의 메모리
  • 5000번대는 데이터 영역의 메모리
  • 7000번대는 객체의 변수 영역의 메모리

기본형 데이터와의 차이는 객체의 변수 영역이 따로 존재한다. 근데 객체가 별도로 할애한 영역은 변수 영역이고 데이터 영역의 메모리는 그대로 활용한다.

데이터 영역에 저장된값은 모두 불변값이다


var obj1 = {
  a: 1,
  b: "bbb",
};

obj1.a = 2;
  • 여기서 데이터 영역의 주소 (@5003: 1) , (@5004:’bbb’) , (@5005: 2) 데이터 영역만 추가됐을 뿐이지 @5001를 가리켰던 데이터 영역은 바뀌지 않고 객체의 변수 영역만 바뀌는 것

  • 중첩 객체 ( 참조형 데이터의 프로퍼티에 참조형 데이터 할당)

var obj = {
  x: 3,
  arr: [3, 4, 5],
};

// 1. 마찬가지로 obj를 @1002번 변수 영역에 주소 할당
// 2. 여러개의 프로퍼티로 이루어진 데이터 그룹이므로 데이터 영역의 주소 @5001번에 저장.
// 3. 데이터 영역 주소에(@7103~?)를 @5001에 저장함
// 4. @7103은 x , @7104에 arr를 지정
// 5. 데이터 영역에서 숫자 3을 검색 , 없으므로 임의로 데이터 영역 @5002에 저장
// 5. 데이터 영역에서 arr는 데이터 그룹이므로 별도의 변수 영역 (@8104~?) 생성 데이터 영역의 주소 정보는 @5003에 저장
// 6. 배열요소가 3개이므로 3개의 변수 공간 확보 , Index부여 0,1,2
// 7. @5003 데이터 영역에서 숫자 3을 검색해서 @8104에 저장
// 8. @5004 데이터 영역에서 숫자 4을 검색해서 @8105에 저장
// 9. @5005 데이터 영역에서 숫자 5을 검색해서 @8106에 저장

obj.arr[1]를 찾을때 메모리 검색 원리

  • @1002 ⇒ @5001 ⇒ (@7103~?) ⇒ @7104 ⇒ @5003 ⇒ @8104~? ⇒ @8105 ⇒ @5004

만약 arr를 재할당 할 경우?

var obj = {
  x: 3,
  arr: [3, 4, 5],
};

obj.arr = "str";
  • 어떤 데이터에 대해 자신의 주소를 참조하는 변수의 개수를 참조 카운터라고 함.
  • @5003의 참조카운트는 @7104의 @5003이 저장되있던 시점까진 1이였다
  • 근데 @7104의 데이터주소가 다시 @5006이 되었으니 참조 카운터가 0이 되어 버렸으니 가비지 컬렉터에 수거대상이 되었다
  • 결과는 @5006번 데이터 영역에 ‘str’이 생성되고 이전에 저장했던 @5003번의 데이터들은 가비지 콜렉터 수거 대상이 됌

(3) 변수 복사 비교

변수 복사를 할 때 기본형 데이터와 참조형 데이터의 차이

(1): 기본형 데이터

var a = 10;
var b = a;

// 1. 변수 a를 @1001번 , 변수 b를 @1002번
// 2. 10은 데이터 주소 @5001번에 할당
// 3. @1001번과 @1002번의 데이터 영역을 가리키는 주소는 @5001번으로 변수선언과 할당 종료

(2): 참조형 데이터

var obj1 = { c: 10, d: "ddd" };
var obj2 = obj1;

// 1. obj1를 @1003번 , obj2를 @1004번
// 2. 그대로 10은 데이터 주소 @5001번에 할당 , 데이터 그룹의 영역엔 데이터 주소 @5002번에 @7103~?를 주소를 할당
// 3. 이것도 마찬가지로 @1003번 @1004번의 값은 @5002번을 가리키는 할당으로 만든다
// 4. 'ddd'는 새로운 데이터 영역 @5003번에 할당하여 데이터그룹 영역 @7103은 @5001번 주소를 가리키고 @7104는 @5003번 데이터를 가리킨다

(3): 객체복사 비교 (객체 프로퍼티 변경)

var a = 10;
var b = a;
var obj1 = { c: 10, d: "ddd" };
var obj2 = obj1;

b = 15;
obj2.c = 20;

// 1. 변수 a를 @1001번 , 변수 b를 @1002번 ,obj1를 @1003번 , obj2를 @1004번
// 2. 10은 데이터 주소 @5001번에 할당 , 데이터 그룹의 영역엔 데이터 주소 @5002번에 @7103~?를 주소를 할당
// 3. b에 할당한 15의 데이터 값이 없으므로 데이터 주소 @5004번에 할당한다
// 4. obj2.c에 할당한 20도 @5005에 할당한다
// 5. 여기서 @7103주소는 새로운 데이터 주소의 할당한 @5005를 가리킨다
// 6. obj2 변수의 @1004는 달라진게 없다 기존 @5002번을 그대로 할당했기 떄문이다.
// 7. 따라서 obj1과 obj2는 같은 객체를 바라보고 있는거다
  • 따라서 비교 결과
//결과는?

console.log(a === b);
console.log(obj1 === obj2);

a===b? false
obj1===obj2? true

[Done] exited with code=0 in 0.072 seconds
  • 기본형과 참조형 데이터의 차이이다
  • 엄밀히 따지면 모든 데이터 타입은 참조형이다
  • 기본형은 주솟값을 한 번 거치고 , 참조형은 한단계를 더 거친다
//객체 자체 변경

var a = 10;
var b = a;
var obj1 = { c: 10, d: "ddd" };
var obj2 = obj1;

b = 15;
obj2 = { c: 20, d: "ddd" };

// 1. 변수 a를 @1001번 , 변수 b를 @1002번 ,obj1를 @1003번 , obj2를 @1004번
// 2. 10은 데이터 주소 @5001번에 할당 , 'ddd'는 @5003번 할당 데이터 그룹의 영역엔 데이터 주소 @5002번에 @7103~?를 주소를 할당
// 3. b에 할당한 15의 데이터 값이 없으므로 데이터 주소 @5004번에 할당한다
// 4. obj2.c에 할당한 20도 @5005에 할당한다
// 5. 근데 obj2는 새로운 객체 프로퍼티를 했으므로 @5006에 새롭게 할당된다 @8204~? 새로운 주소를 가리킨다
// 6. 따라서 데이터 @5002의 주소는 @7103~ , 데이터 @5006의 주소는 @8204~ 에 할당
// 7. obj1과 obj은 이제 다른 값이다
  • 결과값
a===b? false
obj1===obj2? false

[Done] exited with code=0 in 0.048 seconds
  • 가변값은 참조형 데이터의 데이터 자체를 변경 할 경우가 아니라 내부 프로퍼티 변경할 때만 성립함

5. 불변 객체

(1) 불변 객체 생성

  • 불변 객체는 React,Vue,Angular 라이브러리나 프레임워크에서뿐만 아니라 함수형 프로그래밍 , 디자인 패턴에서도 중요한 기초가 됌

어떤 상황에서 불변 객체가 필요할까?

var user = {
  name: "ABEL",
  gender: "male",
};

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

var user2 = changeName(user, "KIM");

if (user !== user2) {
  console.log("유저 정보가 변경되었습니다.");
}

console.log(user.name, user2.name);
console.log(user === user2);
KIM KIM
true

[Done] exited with code=0 in 0.046 seconds
  • 여기서 값으로 전달 받은 객체에 변경을 하더라도 원본 객체는 변하지 않아야 한다.
  • 정보 변경시 원래의 정보도 보관해서 가지고 있어야하는데 원래의 정보(ABEL)도 KIM으로 다 바뀌어 버린것

  • 이 코드는 위 코드를 수정한 것 , changeName 함수는 이제 새로운 객체를 반환하도록 했다
var user = {
  name: "ABEL",
  gender: "male",
};

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

var user2 = changeName(user, "KIM");

if (user !== user2) {
  console.log("유저 정보가 변경되었습니다.");
}
유저 정보가 변경되었습니다.
ABEL KIM
false

[Done] exited with code=0 in 0.049 seconds
  • 근데 문제는 gender 는 변경할 필요가 없는 기존 객체를 하드 코딩했다 객체 정보가 많으면 입력하는 수고가 늘어난다 ..

  • copyObj 함수를 통한 새로운 객체 복사 방법 ( 얕은복사)
  • 여기서 이제 개발자끼리 객체 내부의 변경이 필요할때는 copyObj 함수를 통해 사용하도록 합의하면 user는 불변 객체가 된다.
var copyObj = function (target) {
  var result = {};
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
};

var user = {
  name: "ABEL",
  gender: "male",
};

var user2 = copyObj(user);
user2.name = "KIM";

if (user !== user2) {
  console.log("유저 정보가 변경되었습니다.");
}

console.log(user.name, user2.name);
console.log(user === user2);

(2) 얕은 복사 , 깊은 복사

얕은 복사 : 중첩 객체에서 참조형 데이터가 저장된 프로퍼티를 복사할때 해당하는 데이터 주솟값만 복사한다.

var copyObj = function (target) {
  var result = {};
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
};

var user = {
  name: "ABEL",
  urls: {
    portfolio: "https://www.dongnyeong.world/",
    blog: "https://kdn0325.github.io/",
    facebook: "https://facebook.com",
  },
};

var user2 = copyObj(user);
user2.name = "KIM";
console.log(user.name === user2.name);

user2.urls.portfolio = "https://portfolio.com";
console.log(user.urls.portfolio === user2.urls.portfolio);

user2.urls.blog = "";
console.log(user.urls.blog === user2.urls.blog);
  • 내부 object의 프로퍼티는 데이터를 그대로 참조한다 그래서 내부 프로퍼티는 true로 출력된다 .
false
true
true

[Done] exited with code=0 in 0.044 seconds

깊은 복사 : 중첩 객체에서 참조형 데이터가 내부에 저장된 데이터 프로퍼티까지 복사

  • target이 객체면 프로퍼티를 순회하면서 copyObjectDeep 함수를 재귀적으로 호출
  • 원본 , 사본이 완전 다른 객체를 참조하게 된다.

(1): 깊은 복사 방법

var copyObjDeep = function (target) {
  var result = {};
  if (typeof target === "object" && target !== null) {
    for (var prop in target) {
      result[prop] = copyObjDeep(target[prop]);
    }
  } else {
    result = target;
  }
  return result;
};

var user = {
  name: "ABEL",
  urls: {
    portfolio: "https://www.dongnyeong.world/",
    blog: "https://kdn0325.github.io/",
    facebook: "https://facebook.com",
  },
};

var user2 = copyObjDeep(user);
user2.name = "KIM";
console.log(user.name === user2.name);

user2.urls.portfolio = "https://portfolio.com";
console.log(user.urls.portfolio === user2.urls.portfolio);

user2.urls.blog = "";
console.log(user.urls.blog === user2.urls.blog);

깊은 복사 확인

var obj = {
  a: 1,
  b: {
    c: null,
    d: [1, 2],
  },
};

var obj2 = copyObjDeep(obj);

obj2.a = 3;
obj2.b.c = 4;
obj2.b.d[1] = 3;
{ a: 1, b: { c: null, d: [ 1, 2 ] } }
{ a: 3, b: { c: 4, d: { '0': 1, '1': 3 } } }

[Done] exited with code=0 in 0.053 seconds

(2): 깊은 복사 방법 (JSON 문법으로 문자열로 전환 후 다시 JSON 객체로 바꾸는 방법)

  • getter/setter 등 JSON으로 변경할 수 없는 프로퍼티는 모두 무시
  • httpRequest로 받은 데이터를 저장한 객체를 복사할 때 좋다
  • 순수 정보를 다룰때 좋다
var copyObjViaJSON = function (target) {
  return JSON.parse(JSON.stringify(target));
};

var obj = {
  a: 1,
  b: {
    c: null,
    d: [1, 2],
    func1: function () {
      console.log(3);
    },
  },
  fun2: function () {
    console.log(4);
  },
};

var obj2 = copyObjViaJSON(obj);

obj2.a = 3;
obj2.b.c = 4;
obj.b.d[1] = 3;

console.log(obj);
console.log(obj2);
{
  a: 1,
  b: { c: null, d: [ 1, 3 ], func1: [Function: func1] },
  fun2: [Function: fun2]
}
{ a: 3, b: { c: 4, d: [ 1, 2 ] } }

[Done] exited with code=0 in 0.046 seconds

6. undefined와 null

  • JavaScript에서 없음을 나타내는 값

undefined 반환 조건

  • 값을 대입하지 않은 변수 접근 (데이터 영역에 메모리 주소를 주소를 지정하지 않은 식별자 접근)
  • 객체 내부에 존재하지 않은 프로퍼티 접근
  • return 문이 없거나 호출되지 않은 함수 실행 결과

예시

var a;
console.log(a); // (1) 값을 대입하지 않은 변수 접근 => undefined

var obj = { a: 1 };
console.log(obj.a); // (2) 객체 내부에 존재하지 않은 프로퍼티 접근 => undefined
console.log(obj.b);
console.log(b); // ReferenceError: b is not defined

var func = function () {}; //
var c = func();
console.log(c); // (3) return 문이 없거나 호출되지 않은 함수 실행 결과 => undefined

(1): undefined와 배열

var arr1 = [];
arr1.length = 3;
console.log(arr1); // [ <3 empty items> ]

var arr2 = new Array(3); // [ <3 empty items> ]
console.log(arr2);

var arr3 = [undefined, undefined, undefined];
console.log(arr3); // [ undefined, undefined, undefined ]
  • 빈 요소와 undefined를 할당한 요소는 다르다
var arr1 = [undefined, 1];
var arr2 = [];
arr2[1] = 1;

arr1.forEach(function (v, i) {
  console.log(v, i);
}); // undefined 0 / 1 1
arr2.forEach(function (v, i) {
  console.log(v, i);
}); // 1 1

arr1.map(function (v, i) {
  return v + i;
}); // [NaN,2]
arr2.map(function (v, i) {
  return v + i;
}); // [empty,2]

arr1.filter(function (v) {
  return !v;
}); // [undefined]
arr2.filter(function (v) {
  return !v;
}); // []

arr1.reduce(function (p, c, i) {
  return p + c + i;
}, ""); //undefined011
arr2.reduce(function (p, c, i) {
  return p + c + i;
}, ""); // 11
  • arr1은 undefined와 1을 직접 할당
  • arr2는 빈 배열의 인덱스 1에 1을 할당

arr2는 메서드들이 비어있는 요소는 어떤 처리도 하지 않고 건너 뛰었음.

값이 지정되지 않은 인덱스는 ‘아직은 존재 하지 않은 프로퍼티’

‘비어 있음’을 명시적으로 나타낵 싶을땐 undefined가 아닌 null 사용

하지만 null은 자바스크립트 자체 버그임 ( null은 object타입이다)

var n = null;
console.log(typeof n); // object
  • 여기서 동등 연산자는 어떤 변수가 null인지 undefined가 서로 같다고 판단함
var n = null;
console.log(typeof n); // object

console.log(n == undefined); // true
console.log(n == null); // true
  • 그래서 일치 연산자(===)로 판단해야함
var n = null;
console.log(typeof n); // object

console.log(n === undefined); // false
console.log(n === null); // true

정리하면 undefined는 어떤 변수에 값이 존재하지 않을 경우 , null은 사용자가 명시적으로 ‘없음’을 표현하기 위한 대입한 값