타입, 값, 변수

자바스크립트 소개

코어 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을 씁시다.

stackoverflow.comConfusion between isNaN and Number.isNaN in javascriptI have a confusion in how NaN works. I have executed isNaN(undefined) it returned true. But if I will use Number.isNaN(undefined) it is returning false. So which one I should use? Also why there is...

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도 있는데 자세히는 보지 않았습니다.

developer.mozilla.orgString.prototype.normalize() - JavaScript | MDNnormalize() 메서드는 주어진 문자열을 유니코드 정규화 방식(Unicode Normalization Form)에 따라 정규화된 형태로 반환합니다. 만약 주어진 값이 문자열이 아닐 경우에는 우선 문자열로 변환 후 정규화합니다.

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.

developer.mozilla.orgundefined - JavaScript | MDNThe undefined global property represents the primitive value undefined. It is one of JavaScript's primitive types.

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

이딴게.. 언어...?

원시값->원시값 변환 표입니다:

Valueto Stringto Numberto Boolean
undefined'undefined'NaNfalse
null'null'0false
true'true'1
false'false'0
""0false
" 1.2"1.2true
"one"NaNtrue
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);