- 자바스크립트는 동적 타입(dynamic/weak type) 언어이다. 변수를 선언할 때 타입을 선언하지 않는다. 변수의 타입은 할당에 의해 결정되는데 이것을 **타입 추론(type inference)**라고 한다. 기존에 할당된 값과 다른 데이터 타입의 값을 재할당할 수 있고 타입은 동적으로 결정된다. 이처럼 값을 할당하는 시점에 변수의 타입이 동적으로 결정되는 특징을 **동적 타이핑(dynamic typing)**이라고 한다.
var foo = 'Hello'; // foo는 String 타입을 가진다.
var foo = 1; // foo는 Number 타입을 가진다.
- 데이터 타입(data type)은 값의 종류이다.
- ECMAScript의 데이터 타입은 **원시(primitive)**와 **객체(object, reference)**로 나누어볼 수 있다.
- ECMAScript의 데이터 타입은 8개(Undefined, Null, Boolean, String, Symbol, Number, BigInt, Object)이다.
The ECMAScript language types are Undefined, Null, Boolean, String, Symbol, Number, BigInt, and Object. ES13.0 specification
- 원시 값(primitive value) 혹은 원시 데이터 타입(primitive data type)은 **변경 불가능(immutable)**하다.
- 원시 값을 변수에 할당하면 확보한 메모리 공간에는 실제 원시 값이 저장된다.
- ES11 기준 원시 데이터 타입에는 7가지 타입이 있다.
- Undefined Type
- Null Type
- Boolean Type
- String Type
- Number Type
- Symbol Type(ES6)
- BigInt Type(ES11)
- 원시 래퍼 객체(object wrapper): 원시값은 원시 래퍼 객체를 통하여 객체의 메서드나 프로퍼티를 사용할 수 있다.
undefined
와null
를 제외한 모든 원시 값은 원시 래퍼 객체를 가진다.- 원시값이 메서드나 프로퍼티를 사용하면 내부적으로 일어나는 일은 다음과 같다.
- (1) 원시값이 프로퍼티에 접근하는 순간 특별한 객체가 만들어진다.
- (2) 객체의 프로퍼티에 접근한다.
- (3) 객체는 파괴되고 원시값만 남는다.
- 자바스크립트 엔진은 최적화를 통해 실제로 원시 래퍼 객체를 생성한 것처럼 동작하게 한다.
undefined
타입의 값은undefined
가 유일하다.var
키워드로 선언한 변수는 암묵적으로undefined
로 초기화된다. 자바스크립트 엔진이 메모리 공간을 처음 할당할 때 쓰레기 값을 넣지 않기 위해undefined
로 초기화하기 때문이다. 변수 선언을 참고한다.
var x;
console.log(x); // undefined
따라서 변수에 값이 없다는 것을 표현하려면 undefined
보다 null
을 할당하는 것이 적절하다.
자바스크립트에서 "undefined"와 "undeclared"는 다르다. "undefined" 변수는 접근 가능한 스코프에 선언된 변수이나, 접근하는 그 시점에서는 아무런 값을 가지고 있지 않다. "undeclared" 변수는 접근 가능한 스코프에 공식적으로(formally) 선언되지 않은 변수이다.
var a;
a; // undefined
b; // ReferenceError: b is not defined
"is not defined"와 "undefined"는 다르다. 전자는 "is not declared"나 "is not found"라는 의미에 더 적합하다.
undefined
는 전역 객체의 프로퍼티로, 전역 범위를 가진 변수이다.undefined
는 전역 변수로서 식별자이나, 예약어는 아니다. 따라서var
로 재선언하거나 재할당하는 것이 가능하다. strict mode에서는 TypeError를 발생시킨다.
var undefined = 1;
console.log(undefined); // undefined
'use strict';
undefined = 2; // TypeError: Cannot assign to read only property 'undefined' of object '#<Window>'
let이나 const로 선언하면 SyntaxError: Identifier 'undefined' has already been declared
이 발생한다.
undefined
변수의 초기 값은undefined
원시 값이다.- 값을 할당하지 않은 변수는
undefined
로 초기화된다. - 함수는 반환값을 지정하지 않으면
undefined
를 반환한다. - 인수에 값을 전달하지 않으면 값으로
undefined
가 할당된다.
var x;
typeof x === 'undefined'; // true
x === undefined; // true
- non-
strict
mode에서는 전역 변수undefined
에 재할당할 수 있다.
function foo() {
undefined = 2;
}
foo();
strict
mode에서는 전역 변수undefined
에 재할당하면TypeError
을 발생시킨다.
function foo() {
'use strict';
undefined = 2; // TypeError: Cannot assign to read only property 'undefined' of object '#<Window>'
}
foo();
- non-
strict
mode에서는 전역 스코프에서var
로undefined
를 재선언할 수 있다.
var undefined = 1;
console.log(undefined); // undefined
strict
mode에서는 전역 스코프에서var
로undefined
를 재선언하면TypeError
를 발생시킨다.
'use strict';
var undefined = 1; // // TypeError: Cannot assign to read only property 'undefined' of object '#<Window>'
- 두 모드 모두 전역 스코프에서
let
이나const
로undefined
를 재선언하면SyntaxError
를 발생시킨다.
let undefined = 1; // SyntaxError: Identifier 'undefined' has already been declared
'use strict';
let undefined = 1; // SyntaxError: Identifier 'undefined' has already been declared
- 두 모드 모두 지역 변수
undefined
를 선언하는 것은 오류를 발생시키지 않는다. (var
,let
,const
모두 그러하다)
function foo() {
'use strict';
var undefined = 2;
console.log(undefined); // 2
}
foo();
null
타입의 값은null
이 유일하다.
var x = null;
null
은 **변수가 선언되었으나 어떠한 값도 가지지 않았다는 것(어떤 객체도 참조하고 있지 않음)**을 가리킨다.
var foo = '유리컵';
foo = null;
이전에 할당되어있는 값에 대한 참조를 명시적으로 제거하여 foo
는 더이상 '유리컵'
을 참조하지 않게 되며, 자바스크립트 엔진은 누구도 참조하지 않는 메모리 공간에 대해 가비지 콜렉션을 수행한다.
typeof
연산자는null
의 타입을'null'
이 아니라'object'
로 반환되는데 이는 자바스크립트의 첫 번째 버전의 버그이다.
typeof null === 'object' // true
- 따라서 값이
null
타입인지 확인하려면typeof
연산자가 아니라 일치 비교 연산자===
를 사용한다.
let x = null;
typeof x; // 'object'
x === null; // true
boolean
타입은 논리 연산의 결과로서true
와false
만을 그 값으로 가진다.
var x = false;
if (x) {
// 실행되지 않는다.
}
함수 | 반환 |
---|---|
Boolean() |
boolean |
new Boolean() |
object |
boolean
타입의 원시 값은Boolean
래퍼 객체를 가진다.
var x = new Boolean(false);
if (x) {
// 실행된다.
}
- 불리언이 아닌 값을 변환할 때에는
Boolean
객체가 아니라Boolean
함수를 사용한다.
var x = new Boolean(expression); // Boolean 생성자를 이용하여 Boolean 객체 반환
var x = Boolean(expression); // Boolean 원시 값 반환
- 인자가 falsy라면,
Boolean
객체는false
값으로 초기화된다. - 인자가 truthy라면,
Boolean
객체는true
값으로 초기화된다.
var a = new Boolean(); // false 값으로 초기화
var b = new Boolean(false); // false 값으로 초기화
var c = new Boolean(true); // true 값으로 초기화
var d = new Boolean('false'); // true 값으로 초기화
- numeric type에는
number
와bigint
가 있다.
number
타입은 배정밀도 64비트 부동소수점 형식을 따르기 때문에 모든 수를 실수로 처리한다.
typeof 1 === 'number' // true
typeof 1.23 === 'number' // true
let a = 1; // 정수
let b = 1.0; // 실수
1 == 1; // true
1 === 1.0 // true
따라서 정수로 표시된 값 1
은 사실 실수 1.0
이다.
number
타입은 special numeric value를 가진다.+Infinity
혹은Infinity
: 양의 무한대-Infinity
: 음의 무한대NaN
: 산술불가(Not a Number)
1 / +0 // Infinity
1 / -0 // -Infinity
1 * 'HELLO WORLD' // NaN
NaN
은 전역 객체의 프로퍼티로, 전역 범위를 가진 변수이다.NaN
변수의 초기값은Number.NaN
과 같다.
isNaN(NaN); // true
isNaN(Number.NaN); // true
isNaN
과Number.isNaN
은 그 의미가 다르다.isNaN
: 현재 값 혹은 변환했을 때의 값이NaN
이어야지만true
를 반환(동등)Number.isNaN
: 현재 값이NaN
이어야지만true
를 반환(일치)
isNaN('a'); // true
Number.isNaN('a') // false
함수 | 반환 |
---|---|
Number() |
number |
new Number() |
object |
number
타입의 원시 값은Number
래퍼 객체를 가진다.
var a = new Number('123'); // (a === 123)은 false
var b = Number('123'); // (b === 123)은 true
a instanceof Number; // true
b instanceof Number; // false
typeof a === 'object'; // true
typeof b === 'number'; // true
const bigInt = 1234567890123456789012345678901234567890n;
typeof bigInt === 'bigint'; // true
bigint
타입은 아주 큰 숫자나 높은 정밀도를 표현할 때 사용한다.bigint
타입의 값은 정수 리터럴 끝에n
을 붙여 만든다.
함수 | 반환 |
---|---|
BigInt() |
bigint |
new BigInt() |
object |
bigint
타입의 원시 값은BigInt
래퍼 객체를 가진다.
const bigInt = BigInt(1234567890123456789012345678901234567890n);
string
타입은 문자열을 나타내는데 사용하며, 문자열은 0개 이상의 16비트 유니코드 문자(UTF-16)의 집합이다.
const NAME = 'umi';
- 문자열은
''
이나""
혹은` `
(ES6)로 감싸 표현한다. 이는 자바스크립트 엔진이 문자열을 키워드나 식별자와 같은 토큰과 구별하기 위해서이다. - 인덱스나
charAt
메서드로 문자에 접근할 수 있다.
'umi'[0]; // 'u'
'umi'.charAt(0) // 'u'
함수 | 반환 |
---|---|
String() |
string |
new String() |
object |
string
타입의 원시 값은String
래퍼 객체를 가진다.String()
은 원시 스트링(primitive string)을 반환한다.
- 템플릿 리터럴은 ES6에 도입된 새로운 문자열 표기법이다.
- 런타임에 일반 문자열로 변환되어 처리된다.
- 문자열을
`
(백틱)으로 감싸고, 변수나 표현식을${}
으로 감싸 문자열 안에 넣는다.
const NAME = '유미';
console.log(`캐릭터 이름은 ${NAME}!`);
- 이스케이프 문자열을 사용하지 않아도 줄바꿈과 모든 공백이 적용된다.
`hello
world`
// 'hello\nworld'
심볼에 대해서는 Symbol을 참고한다.
var foo = Symbol('foo');
typeof foo === 'symbol'; // true
Symbol('foo') === Symbol('foo'); // false
Symbol('foo') === 'foo'; // false
Symbol('foo').toString(); // Symbol(foo)
- ES6에 도입되었다.
symbol
타입의 값은 유일하고 변경 불가능한 원시 값이다(A Symbol is a unique and immutable primitive value)symbol
타입의 값은 리터럴이 아니라Symbol
함수를 호출하여 생성한다.symbol
타입의 값은 주로 객체 프로퍼티의key
값(식별자)으로 사용된다.
var key = Symbol('key');
var obj = {};
obj[key] = 'value';
console.log(obj[key]); // value
함수 | 반환 |
---|---|
Symbol() |
symbol |
symbol
타입의 원시 값은Symbol
래퍼 객체를 가진다.new Symbol()
을 지원하지 않아TypeError
가 발생한다.Symbol
래퍼 객체를 생성하고 싶다면 다음과 같이 한다.
var foo = Symbol('foo');
var obj = Object(foo);
**객체 타입(object/referenece type)**의 값은 변경 가능한(mutable)한 값이며 **키(key)**와 **값(value)**으로 이루어진 **프로퍼티(property)**를 0개 이상 가지고 있다. 객체를 변수에 할당하면 확보한 메모리 공간에는 객체 자체가 아니라 객체가 저장된 메모리 공간의 주소인 참조 값(reference value)이 저장된다.
11. Objects를 참고한다.
원시 타입의 값 | 객체 |
---|---|
변경 불가능한 값(immutable value)이다. | 변경 가능한 값(mutable value)이다. |
변수에는 실제 값이 저장된다. | 변수에는 참조 값이 저장된다. |
값에 의한 전달(pass by value): 변수를 다른 변수에 할당하면 원본의 원시 값이 복사된다. | 참조에 의한 전달(pass by reference): 변수를 다른 변수에 할당하면 원본의 참조 값이 복사된다. |
객체는 동적으로 프로퍼티를 생성하고 추가할 수 있으므로 그 비용이 크다. 변수에 담긴 값을 수정하고 싶다면 원시 값의 경우 기존의 값을 복사하여 연산을 하겠지만, 매번 객체를 복사하고 새로운 메모리 공간을 할당하는 것은 메모리와 성능면에서 비효율적이다. 따라서 변수는 객체 자체가 아니라 객체가 저장된 메모리의 주소인 참조값를 저장하여 복사나 메모리 비용을 줄인다.
단, 여러 개의 식별자가 동일한 객체 참조값을 저장하여 동일한 객체에 접근할 수 있으므로 예상치 못한 일이 벌어질 수 있다. 데이터는 원시 값과 같은 신뢰를 확보할 수 없다.
원시 값은 변경 불가능(immutable)한 값이고 객체는 변경 가능(mutable)한 값이다. 이것은 변수가 저장하고 있는 값이 변경 불가능하다는 뜻이 아니다. 변수는 const
로 선언되지 않았다면 재할당을 통해 변수가 저장하고 있는 값을 교체할 수 있다.
let a = 1;
a = 2;
let b = { name: 'rey' };
b = { val: 10 };
한편, 변수를 다른 변수에 할당한다고 하자. 자바스크립트에서는 원본 변수의 값이 복사되어 전달된다.
let a = 1;
let aa = a;
console.log(a); // 1
console.log(aa); // 1
let b = { name: 'rey' };
let bb = b;
console.log(b == bb); // true
원시 값이 할당된 변수의 경우, 확보한 메모리 공간에는 원시 값이 저장되어있다. 이와 달리 객체가 할당된 변수의 경우, 확보한 메모리 공간에는 객체 자체가 아니라 (다른 메모리 공간에 저장된) 객체에 대한 참조 값이 저장되어있다.
따라서 aa
에는 a
의 메모리 공간에 있는 원시 값 1
이 복사되어 전달되고 bb
에는 b
의 메모리 공간에 있는 참조 값이 복사되어 전달된다. 즉, 두 경우 모두 원본 변수와 새로이 할당된 변수는 각각의 메모리 공간을 가지고 동일한 값을 저장할 뿐이다. 그렇다면 새롭게 할당된 변수의 값을 수정해도 원본 변수의 값이 수정되지는 않는다.
aa = 2;
console.log(a); // 1
console.log(aa); // 2
bb.name = 'lana';
console.log(b.name); // 'lana'
console.log(bb.name); // 'lana'
그러나 참조된 객체의 프로퍼티를 수정하면 원본 변수의 값도 수정되는 것처럼 보인다. 값에 의한 전달(pass by value)을 참고한다.
문자열은 원시 값이고, 원시 값은 변경 불가능하므로 문자열 역시 변경 불가능하다.
let a = 'hello';
a[0] = 'x';
console.log(a); // hello
따라서 a
의 첫번째 문자를 바꾸려해도 바꿀 수 없다.
let a = 'hello';
a = 'world';
그러나 변수 값은 변경할 수 있다. 즉, 재할당을 통해 변수가 새로운 값을 가지도록 할 수 있다. 재할당하면 기존의 메모리 공간이 아니라 새롭게 메모리 공간을 확보해 가리키게 된다. 변수 a
는 'hello'
가 아니라 새롭게 'world'
라는 값을 가진다.
typeof x;
typeof(x);
tyoeof
연산자는 단항 연산자이다.- 피연산자의 타입을
string
으로 반환한다.
typeof 1 === 'number';
typeof 1.0 === 'number';
typeof true === 'boolean';
typeof false === 'boolean';
typeof BigInt(1) === 'bigint'
typeof '1' === 'string';
typeof undefined === 'undefined';
typeof Symbol(1) === 'symbol';
typeof null === 'object';
typeof new Object() === 'object';
typeof function(){} === 'function';
typeof
연산자는 피연산자가 함수이면 'function'
이라는 값을 반환한다. 하지만 다른 타입들과 달리, top-level이 아니라 다만 object
의 subtype이다. ECMAScript 명세에 따르면 내부 메서드 [[Call]]
를 구현한 객체에 대해서는 'function'
을 반환하고 그렇지 않은 객체는 'object'
를 반환한다.
ECMAScript 사양은 string
과 number
타입 외의 데이터 타입의 크기는 명시하지 않으므로 이 경우에 속하는 경우 크기는 자바스크립트 엔진에 따라 다를 수 있다.
number
타입은 8바이트 숫자를 표현하는 배정밀도 65비트 부동소수점 형식을 사용한다.string
타입에서 1개의 문자는 2바이트이다.
데이터 타입이 필요한 이유는 세 가지로 요약할 수 있다.
- 값을 저장하기 위해 확보해야 할 메모리의 크기를 알 수 있다.
- 값을 참조하기 위해 읽어야 할 메모리의 크기를 알 수 있다.
- 참조한 값을 어떻게 해석해야할 지 알 수 있다.
메모리에 값을 저장하려면 확보해야할 메모리 공간의 크기를 알아야 한다. 너무 크게 잡는다면 공간이 낭비될 것이고 너무 작게 잡는다면 값이 손실될 것이다. 자바스크립트 엔진은 데이터 타입에 따라 정해진 크기의 메모리 공간을 확보한다.
var foo = 100;
위 예시를 보자. 자바스크립트 엔진은 리터럴 100
을 number
타입으로 해석한 후, number
타입의 값 100
을 저장하기 위해 8바이트 메모리 공간을 확보하고 그곳에 100을 2진수로 저장할 것이다.
값을 참조하려면 우선 식별자 foo
를 통해 값이 저장된 메모리 공간의 주소(첫번째 메모리 셀)로 찾아간다. 이때 값 100을 손실없이 읽으려면 8바이트 단위로 읽어 들여야 한다. 자바스크립트 엔진은 foo
변수를 number
타입으로 인식하여 8바이트 단위로 저장된 값을 읽을 수 있도록 한다.
컴파일러 또는 인터프리터는 심벌 테이블이라는 자료구조를 사용한다. 키는 식별자로 바인딩된 값의 메모리 주소, 데이터 타입, 스코프 등을 관리한다.
모든 값은 메모리에 2진수로 저장되므로 데이터 타입에 따라 다르게 해석할 수 있어야 한다. 가령 0100 0001
은 숫자로 54이지만 문자열로는 A
이다. foo
변수에 저장된 값은 number
타입이므로 foo
를 참조하여 읽어들인 이진수는 숫자로 해석해야 한다.
- 모던 자바스크립트 Deep Dive - 6장 데이터 타입
- MDN - JavaScript data types and data structures
- 모던 자바스크립트 Deep Dive - 11장 원시 값과 객체의 비교
ES11 기준으로 8개가 있으며, 이 중에서 Object는 객체이고 다른 타입은 모두 원시값이다. 원시값은 변경 불가능한 값이고, 객체는 변경 가능한 값이다. 원시값을 저장한 변수의 경우 바인딩된 메모리 공간에 실제로 원시값이 들어가나, 객체의 경우 바인딩된 메모리 공간에는 객체가 저장된 메모리 공간의 주소인 참조값이 들어간다.