타입, 값, 변수
자바스크립트 소개
코어 js에는 입출력 기능이 포함되어있지 않습니다. 입출력, 네트워크, 그래픽 등의 기능은 js가 임베딩된 호스트 환경의 책임입니다.
표현식은 어떤 값으로 계산되지만 프로그램의 상태를 바꾸지는 않고, 문은 값은 없지만 상태를 바꿉니다.
Identifiers and Reserved Words
쓸 일은 없지만, let도 변수명으로 경우에 따라 쓸 수는 있습니다.
// ✍️ 아래 주석을 해제해보세요
// 'use strict'
var let = 'hello, world!';
console.log(let);
Unicode
// ✍️ 유니코드 값을 바꿔보세요
const π = 3.14;
console.log(π, '\u{1F600}');
const str = [...Array(9).keys()]
.map((x) => 0x1f311 + x)
.map((x) => String.fromCodePoint(x))
.join(' ');
console.log(str);
문자의 모양이 같더라도 다른 유니코드 값일 수 있습니다. 유니코드 표준에 normalization 알고리즘이 있지만 JS가 이를 처리해주지는 않으므로 에디터에서 잘 해주는지 확인합시다.
Optional Semicolons
세미콜론이 없으면 동작이 직관과 다른 경우가 있습니다.
// prettier-ignore
let y = 1 + 2
(3 + 4).toString()
Types, Values, and Variables
JS는 함수와 클래스가 단순 언어 구문(syntax)의 일부가 아니라 코드에 의해 조작될 수 있는 값이라는 점에서 다른 정적 언어들과 다릅니다.
Overview and Definitions
JS에서 문자열은 불변입니다. 이미 선언된 문자열의 일부를 수정할 수 없습니다.
// 'use strict'
const str = 'javascript';
str[0] = 'd';
console.log(str);
Numbers
64비트 부동소수점을 사용합니다. 오버플로우가 발생해도 에러를 던지지는 않습니다.
let val = Number.MAX_VALUE;
// (val + 1은 왜 Infinity가 아니지?)
console.log(val, val + 1, val ** 2);
console.log(val === val + 1);
하지만 배열 인덱싱이나 비트 연산등은 32비트 정수 연산을 합니다.
console.log(1 << 30, 1 << 31, 1 << 32);
16진수도 아래처럼 바로 입력할 수 있습니다. CSS 색상값 넣을 때 종종 썼던 것 같아요.
console.log(0x333);
이외에도 특이한 경우가 많습니다.
console.log(1 / 0, 0 / 0, Infinity / 0, Infinity / Infinity);
let zero = 0;
let negZero = -0;
console.log(zero === negZero);
console.log(1 / zero === 1 / negZero);
NaN은 모든 값과 비교 결과가 false입니다. Number.isNaN 이나 isNaN을 씁시다.
const val = NaN;
console.log(val === NaN, Number.isNaN(val));
아래와 같은 경우를 이해하려면 부동소수점 표준인 IEEE-754를 공부해봅시다:
let x = 0.3 - 0.2;
let y = 0.2 - 0.1;
console.log(x === 0.1, y === 0.1);
부동소수점이 근사값을 사용하는게 문제라면 scaled integer를 사용해볼 수 있습니다. 이더리움에서는 wei라는 최소 단위(=10^(-18)ether)를 쓰는데 책에서 말하는 scaled integer과 비슷한 것 같습니다.
BigInt와 Number간의 연산은 두 자료형이 포함관계가 아니므로 불가능합니다. 비교 연산은 가능합니다.
BigInt는 암호학에 쓰기는 부적합한데 timing attack에 대응하지 않기 때문입니다.
timing.attacks.cr.yp.toTiming attacks: Programming FAQ
Text
UTF-16 인코딩을 사용합니다. Codepoint가 16비트에 들어가지 않는 유니코드 문자는 surrogate pair로 알려진 UTF-16의 시퀀스를 사용합니다.
대부분의 문자열 관련 메서드는 16비트 단위로 동작하고 surrogate pair를 특별 취급하지 않습니다. 다만 이터러블 관련(for/of...)은 실제 문자열처럼 되게 처리해줍니다.
const euro = '€';
const love = '💙';
console.log(euro.length);
console.log(love.length);
console.log(love === '💙');
console.log(love[0]);
for (const c of love) console.log(c);
문자열 표기법 별로 미묘하게 다른 newline 차이입니다.
console.log(
'one\
long\
line',
);
console.log(`two
line`);
이런저런 문자열 관련 메서드들입니다:
// 0xd83d + 0xdc99
const love = '💙';
console.log(love.charAt(0));
// returns an integer between 0 and 65535
// representing the UTF-16 code unit at the given index.
// may return lone surrogates
console.log(love.charCodeAt(0).toString(16));
// 왜 0x1f499인지는..모르겠음..
console.log(love.codePointAt(0).toString(16));
String.prototype.normalize도 있는데 자세히는 보지 않았습니다.
Tagged tempalte literals는 styled-component에서 활용하는걸로 알고 있습니다:
const str = String.raw`\n`;
console.log(str);
Boolean Values
console.log(!!undefined);
console.log(!!null);
console.log(!!0);
console.log(!!-0);
console.log(!!NaN);
console.log(!!'');
null and undefined
undefined는 언어 키워드인 null과는 다르게 미리 정의된 전역 상수값으로 undefined로 초기화되어있습니다.
undefined is a property of the global object. That is, it is a variable in global scope.
console.log('undefined' in globalThis);
console.log('null' in globalThis);
Symbols
프로퍼티명을 심볼로 정하고 이를 공유하지 않으면 다른 모듈의 코드가 실수로 해당 프로퍼티를 건들지 않음을 보장받을 수 있습니다.
const sym = Symbol();
let obj = {
a: 1,
[sym]: 2,
};
console.log(obj, Object.values(obj));
console.log(obj[sym]);
ES6에서 이터러블을 정의할 때 이터레이터 메서드를 문자열 이름으로 정해 표준화하면 기존 코드가 망가질 염려가 있었기에 symbolic name을 도입했습니다. Symbol.for 메서드는 비슷한 맥락에서 global Symbol registry로 기능합니다:
let s = Symbol.for('shared');
let t = Symbol.for('shared');
console.log(s === t, s.toString(), Symbol.keyFor(t));
The Global Object
전역 객체의 프로퍼티들은 JS 프로그램에서 사용할 수 있는 전역적으로 정의된 식별자들입니다. undefined, isNaN(), String(), Math 등등...
브라우저에서 window 객체는 스스로를 참조하는 window 프로퍼티가 있어서 이 프로퍼티를 사용해 전역 객체에 접근할 수 있습니다. (뭔가 메타적이고 신기한듯,,,) Web worker 스레드에서는 좀 다르다는데 이후 챕터에서 공부해봅시다.
원래는 노드에서는 global, 브라우저에서는 window로 전역 객체에 접근했지만 ES2020부터는 globalThis로 표준화되었습니다.
console.log(window === globalThis);
console.log(window === window.window.window);
Immutable Primitive Values and Mutable Object References
객체는 참조하는 대상이 같아야 같습니다:
let a = [1, 2, 3];
let b = [1, 2, 3];
console.log(a === b);
Type Conversions
이딴게.. 언어...?
원시값->원시값 변환 표입니다:
Value | to String | to Number | to Boolean |
---|---|---|---|
undefined | 'undefined' | NaN | false |
null | 'null' | 0 | false |
true | 'true' | 1 | |
false | 'false' | 0 | |
"" | 0 | false | |
" 1.2" | 1.2 | true | |
"one" | NaN | true | |
0 | '0' | false | |
-0 | '0' | false | |
1 | '1' | true | |
Infinity | 'Infinity' | true | |
-Infinity | '-Infinity' | true | |
NaN | 'NaN' | false |
객체->원시값 변환은 뒤에서 살펴볼게요.
Conversions and Equality
어떤 값이 다른 값으로 바뀐다고 두 값이 같음을 의미하지는 않습니다. 타입 변환 알고리즘과 연산자에서 사용하는 알고리즘이 상이하기 때문입니다. 아래 예제를 볼게요.
console.log(Boolean(undefined)); // false로 변환됐습니다.
console.log(undefined == false); // 그렇다고 false와 == 하지는 않습니다.
Explicit Conversions
wrapper '객체'를 사용한 형변환 방식은 하위호환을 위해 있으니 사용하지 맙시다:
const val = new Boolean('true');
console.log(typeof val);
숫자 관련 메서드들입니다:
let n = 17;
console.log(`0b${n.toString(2)}`);
console.log(`0o${n.toString(8)}`);
console.log(`0x${n.toString(16)}`);
console.log();
n = 123456.789;
console.log(n.toString(2));
console.log(n.toFixed(2));
console.log(n.toFixed(5));
console.log(n.toExponential(1));
console.log(n.toExponential(3));
// precesion값이 정수 부분을 표현하지 못할 정도면 지수 표기법을 사용합니다.
console.log(n.toPrecision(4));
console.log(n.toPrecision(10));
console.log();
console.log(Number('3 eggs'));
console.log(parseInt(' 3 eggs'));
console.log(parseInt('3.14'));
console.log(parseInt('0xF'));
console.log(parseFloat('.1'));
console.log(parseInt('.1'));
console.log(parseInt('ff'));
console.log(parseInt('ff', 16));
The toString() and valueOf() methods
객체 -> 원시값 변환 알고리즘을 이해하려면 두 메서드를 알아야해요. 모든 객체가 이 메서드들을 상속받습니다.
- 각 클래스는 각자의 toString을 구현합니다. 객체의 문자열 표현을 반환합니다.
- valueOf는 (만약 있다면) 객체를 표현하는 원시값을 반환합니다. 대부분은 이런 원시값이 없기에 객체 자신을 반환합니다.
console.log(new Boolean(true).valueOf());
console.log({ a: 1 }.valueOf());
console.log(new Date().valueOf());
object-to-primitive conversion algorithms
세 종류로 나뉘어 상황에 맞게 사용됩니다.
- prefer-string: toString이 원시값을 반환한다면(문자열이 아니더라도) 이를 사용, 아니면 valueOf가 원시값 반환하면 이를 사용, 아니면 TypeError.
- prefer-number: prefer-string에서 valueOf와 toString 순서만 교체.
- no-preference: Date면 prefer-string, 다른 모든 빌트인 객체는 prefer-number
console.log(Number([])); // valueOf 실패, toString에서 "", 0으로 변환
console.log(Number([9])); // valueOf 실패, toString에서 "9", 9로 변환
Object to Primitive Conversions
모든 객체는 불 값으로 변환시 true가 됩니다:
console.log(Boolean(new Boolean(false)));
객체를 문자열로 변환해야하면 prefer-string 알고리즘으로 우선 변환하고 필요시 결과값을 위 표에 따라 문자열로 변환합니다.
숫자도 마찬가지로 prefer-number를 돌리고 숫자로 변환합니다. 다만 몇몇 연산자는 예외가 있습니다:
- '+' 연산자는 객체에 no-preference 알고리즘을 돌리고, 이후 피연산자중 하나라도 문자열이면 문자열 concat을, 그렇지 않다면 숫자로 둘 다 수정합니다.
- '==' 와 '!='는 피연산자 하나만 객체면 해당 객체를 no-preference 알고리즘을 사용해 원시값으로 수정 후 비교합니다.
- 부등호 연산자는 둘 다 객체면 prefer-number 알고리즘으로 변환 후 비교합니다. 다만 prefer-number에서 반환된 원시값을 숫자로 형변환하지 않습니다.
const a = {
valueOf: () => 'not a number',
// valueOf: () => 123,
};
console.log(a + '');
console.log(a + 1);
Variable Declaration and Assignment
확실히 안바뀌는 값에만 const를 쓰는 사람들이 있고, 일단 const로 선언하고 나중에 바뀌어야한다면 let으로 수정하는 사람들이 있습니다. 아래는 참고할만한 아티클입니다.
overreacted.ioOn let vs const — overreactedSo which one should I use?
for문에서도 const를 쓸 수 있습니다:
for (const datum of [1, 2, 3]) console.log(datum);
for (const i = 0; i < 5; i++) console.log(i);
let과 const는 block scope입니다.
Top level에서 선언되면 전역 변수/상수라고 합니다. 노드와 client-side js module에서 전역 값의 스코프는 파일입니다. Traditional client-side js에서는 html document입니다. 한 스크립트에서 선언한 전역 값을 다른 스크립트에서 사용할 수 있습니다.
var는 포함하는 함수 body 내부에 스코핑?됩니다:
for (var i = 0; i < 10; i++);
console.log(i);
함수 바깥에서 선언하면 전역 변수가 되는데, globalThis에 포함된다는 점이 var와 let/const의 차이입니다:
var a = 1;
let b = 1;
console.log(globalThis.a, globalThis.b);
var는 중복 선언을 제한하지 않습니다. 또한 hoisting이 이루어집니다. 즉 변수의 선언이 변수를 포함하는 스코프 최상단으로 이동합니다:
var a = 10;
var a = 20;
console.log(a);
console.log(b);
var b = 10;
console.log(c);
let c;
console.log('이 줄은 실행되지 않습니다');
let a;
let a;
strict mode의 유무에 따라 선언되지 않는 변수에 대한 처리가 달라집니다. strict mode가 아니면 새로운 전역 변수가 생성됩니다. 이러한 변수는 var로 선언된 변수와 특성이 '유사'합니다.
(() => {
a = 10;
})();
console.log(a);
(() => {
'use strict';
b = 10;
})();
Destructuring assignment에서 이런 것도 가능합니다:
let [, x, , y] = [1, 2, 3, 4];
console.log(x, y);
let [first, ...rest] = 'Hello';
console.log(first, rest);