The JavaScript Standard Library
Sets and Maps
// 이터러블을 인자로 받아요
let s = new Set('banana');
console.log(...s);
// Chaining이 가능합니다
s.add('a').add('b').add(true);
console.log(...s);
// 불 값을 리턴해요
console.log(s.delete(true), s.delete('c'));
// 초기화
s.clear();
원소끼리 비교할 때 ===
와 같은 strict equality check를 합니다:
let s = new Set();
s.add(1).add('1');
console.log(...s);
s.add({ a: 1 }).add({ a: 1 });
console.log(...s);
console.log(s.has({ a: 1 }));
삽입 순서를 기억합니다:
let s = new Set();
for (let i = 10; i; i--) s.add(i);
console.log(...s);
집합 순회하기:
let s = new Set('banana');
for (let e of s) console.log(e);
// 보통 두번째 인자가 인덱스이지만 집합은 마땅한게 없으므로 원소를 한번 더 전달합니다
s.forEach((...arr) => console.log(arr));
Map의 생성자에는 [key, value]
쌍의 배열을 전달합니다:
let m1 = new Map([
[1, 'one'],
[2, 'two'],
]);
// 객체가 이미 있으면 아래와 같은 초기화도 가능합니다.
let o = { 1: 'one', 2: 'two' };
let m2 = new Map(Object.entries(o));
// 객체의 키 값은 문자열만 가능하기에 현재 m2의 모든 키는 문자열입니다.
// 이것이 객체와 Map의 차이점 중 하나입니다.
console.log(m1.has(1), m2.has(1), m2.has('1'));
사용법은 set
메서드를 제외하고 집합 자료구조와 아주 유사합니다:
let m = new Map();
m.set(1, 'one').set(2, 'two');
console.log(m.get('1'), m.get(1));
Map도 set과 마찬가지로 삽입 순서대로 순회합니다:
let m = new Map([...Array(5).keys()].map((x) => [x, 2 ** x]));
console.log(...m.keys());
console.log(...m.values());
console.log(...m.entries());
// ⚠️ value 다음 key입니다
m.forEach((...arr) => console.log(arr));
WeakMap
과 WeakSet
은 객체를 약하게 참조하기에 Map과 Set에서 참조한다는 이유로
객체가 GC(garbage collect)되지 않는 경우를 방지합니다. WeakMap
의 키로는 객체만
사용할 수 있고 WeakSet
에도 객체만 추가될 수 있습니다. 순회를 지원하지 않고
size
프로퍼티를 가지지 않습니다. swift의 weak
키워드가 생각나네요.
WeakMap의 사용 예시:
Typed Arrays and Binary Data
Typed array는 엄밀히 Array는 아니지만 대부분의 배열 메서드들을 구현합니다.
let length = 10;
console.log(Array.isArray(new Int8Array(length)));
Typed array의 모든 요소는 같은 타입의 숫자고, 배열의 길이를 바꿀 수
없으며(push
등의 메서드 없음), 배열 생성시 0으로 초기화됩니다.
Name | Description |
---|---|
Int8Array | signed bytes |
Uint8Array | unsigned bytes |
Uint8ClampedArray | unsigned bytes with rollover |
Int16Array | signed 16-bit short integers |
Uint16Array | unsigned 16-bit short integers |
Int32Array | signed 32-bit integers |
Uint32Array | unsigned 32-bit integers |
BigInt64Array | signed 64-bit BigInt values(ES2020) |
BigUint64Array | unsigned 64-bit BigInt values(ES2020) |
Float32Array | 32-bit floating-point value |
Float64Array | 64-bit floating-point value: a regular JS number |
let printBytes = (x) => console.log(x.BYTES_PER_ELEMENT);
// ✍️ 다른 생성자들로도 해보세요
printBytes(Uint8Array);
이름에 clamp가 붙은 배열은 오버플로우를 예쁘게 처리합니다:
let a = new Uint8Array(1);
let b = new Uint8ClampedArray(1);
// -1 = 0b11111111
a[0] = -1;
b[0] = -1;
// 냅다 비트단위로 자릅니다(wrap around).
console.log(a[0]);
// 최소 혹은 최대 값으로 처리합니다
console.log(b[0]);
일반 배열과 비슷한 생성자를 지원합니다:
let white = Uint8ClampedArray.of(255, 255, 255, 0);
let ints = Uint32Array.from(white);
// 범위를 벗어나면 잘라요
console.log(...Uint8Array.of(1.23, 2.99, 0b1111111100000011));
ArrayBuffer은 메모리 청크에 대한 opaque한(구체적인 구현이 없는) 참조에요. Opaque에 대해서는 아래 글을 참고하세요:
en.wikipedia.orgOpaque data type - Wikipedia
따라서 ArrayBuffer의 메모리를 사용하려면 다른 타입을 통해야해요:
// 1MB의 메모리를 할당합니다.
let buffer = new ArrayBuffer(1024 * 1024);
console.log(buffer.byteLength);
let asBytes = new Uint8Array(buffer);
console.log(asBytes.length);
let asInts = new Int32Array(buffer);
console.log(asInts.length);
// 두번째 인자는 시작 바이트 오프셋입니다.
// 마지막 1KB를 바이트로 읽기
let lastK = new Uint8Array(buffer, 1023 * 1024);
console.log(lastK.length);
// 세번째 인자는 배열 요소의 개수입니다.
// 두번째 1KB를 int로 읽기
let ints2 = new Uint8Array(buffer, 1024, 256);
console.log(ints2.length);
모든 typed array는 기저에 ArrayBuffer
가 있으며 buffer
프로퍼티로 접근할 수
있습니다. ArrayBuffer
를 통해 하나의 버퍼를 여러 typed array로 바라볼 수
있어요.
set 메서드는 요소들을 다른 typed array로 복붙합니다:
let bytes = new Uint8Array(12);
let pattern = new Uint8Array([1, 2, 3, 4]);
bytes.set(pattern);
console.log(...bytes);
bytes.set(pattern, 4);
console.log(...bytes);
bytes.set([0, 1, 2, 3], 8);
console.log(...bytes);
slice는 새롭게 생성된 배열을 반환하지만 subarray는 기존 배열을 참조합니다:
let arr = new Int16Array([0, 1, 2, 3]);
let copy = arr.slice(2, 4);
let ref = arr.subarray(2, 4);
arr[2] = -9;
console.log(copy[0], ref[0]);
console.log(copy.buffer === arr.buffer, ref.buffer === arr.buffer);
console.log(copy.byteOffset, ref.byteOffset);
console.log(copy.byteLength, ref.byteLength);
console.log(copy.buffer.byteLength, ref.buffer.byteLength);
성능을 위해 typed array에서는 하드웨어의 엔디언을 따라갑니다.
ko.wikipedia.org엔디언 - 위키백과, 우리 모두의 백과사전
let littleEndian = new Int8Array(new Int32Array([1]).buffer)[0] === 1;
console.log(littleEndian ? '리틀 엔디안 컴퓨터네요!' : '빅 엔디안 컴퓨터네요!');
네트워크등 외부에서 온 데이터는 엔디언이 다를 수 있습니다. 이때 DataView 객체를 활용합니다:
let bytes = new Uint8Array(16);
bytes[0] = 1;
let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
// big-endian
console.log(view.getUint16(0, false));
// little-endian
console.log(view.getUint16(0, true));
// big-endian 형식으로 1을 write
view.setUint16(0, 1, false);
console.log(view.getUint16(0, false));
console.log(view.getUint16(0, true));
Typed array와 DataView를 활용해 압축 데이터나 이미지 데이터 등등 이진 데이터를 처리할 수 있어요.
Pattern Matching with Regular Expressions
재미없어서 다음에...
Dates and Times
Date 객체 만들기:
// 현재 시각
console.log(new Date());
// 1970년부터 지난 밀리초
console.log(new Date(0));
// 여러 숫자를 건네줄 수 있습니다.
// 인자 순서대로 fullyear, month, date, hours, minutes, seconds, milliseconds입니다.
// 플랫폼의 시간대를 사용합니다.
console.log(new Date(2100, 0, 1, 2, 3, 4, 5));
// UTC 기준으로 하고싶으면 UTC static method를 활용해 우선 ms로 변환합니다.
console.log(new Date(Date.UTC(2100, 0, 1, 2, 3, 4, 5)));
// 생성자에 문자열을 건네줄 수 있습니다.
// 적어도 toString, toUTCString, toISOString의 반환값은 지원합니다.
console.log(new Date('2100-01-01T00:00:00Z'));
여담으로 month가 zero-based인 이유:
JS는 내부적으로 timestamp를 사용해 날짜를 표현합니다:
// date 객체에 대응하는 timestamp
console.log(new Date(10).getTime());
// 현재 시간의 timestamp
console.log(Date.now());
// 문자열에서 timestamp 파싱
console.log(Date.parse('1970-01-01T00:00:00Z'));
// 소숫점 단위까지 나타내며
// 특정 이벤트(아래 링크 참고) 이후 시간이 얼마나 지났는지를 나타냅니다.
console.log(performance.now());
date 객체에 시간을 더하는 방법:
let d = new Date();
// overflow가 있더라도 잘 처리됩니다.
d.setMonth(d.getMonth() + 3, d.getDate() + 14);
console.log(d);
날짜를 문자열로 표현하는 메서드들:
let d = new Date(0);
// locale time zone / not user locale aware
console.log(d.toString());
console.log(d.toDateString());
console.log(d.toTimeString());
// locale time zone / user locale aware
console.log(d.toLocaleString());
console.log(d.toLocaleDateString());
console.log(d.toLocaleTimeString());
// UTC time zone / not user locale aware
console.log(d.toUTCString());
// UTC time zone / ISO-8601 standard
console.log(d.toISOString());
None of these date-to-string methods is ideal when formatting dates and times to be displayed to end users
열심히 봤는데 이렇게 말한다고?
Error Classes
JS에서는 아무 값이나 throw할 수 있습니다.
try {
// ✍️ 문자열도 던져보세요
throw 1;
} catch (e) {
console.log(typeof e);
}
그래도 일반적으로는 Error를 상속받은 객체를 던집니다. Error는 생성될 시점의 스택 상태를 가지고있기에 디버깅에 용이합니다. 던져질 시점이 아닌 생성될 시점임을 조심하세요:
let e = new Error();
let f = () => {
// ✍️ 주석을 해제하고 차이를 확인해보세요.
// throw new Error();
throw e;
};
let g = () => {
f();
};
try {
g();
} catch (e) {
// 비표준 기능이지만 모든 주요 JS 엔진에 구현되어있습니다.
// 스택 문자열의 정확한 내용에 의존할 수는 없지만 디버깅 목적으로는 사용할 수 있습니다.
console.log(e.stack);
}
에러 객체는 message와 name 프로퍼티를 가집니다:
class MyError extends Error {
name = 'MyError';
}
let e = new MyError('my message');
console.log(e.name, e.message);
console.log(e.toString());
Error 클래스를 상속받은 여러 빌트인 에러 클래스들이 있습니다:
JSON Serialization and Parsing
자료 구조를 바이트/문자 스트림으로 바꾸는 과정을 serialization(marshaling, pickling)이라 합니다.
JSON.stringify
의 세번째 인자로 indentation을 설정할 수 있습니다.
let o = { a: 1, b: 2 };
console.log(JSON.stringify(o, null, 2));
console.log(JSON.stringify(o, null, ' '));
stringify
에서 지원하지 않는 값을 만나면 해당 값의 toJSON 메서드를 활용합니다.
let date = new Date();
let o = { date };
console.log(JSON.stringify(o));
date.toJSON = () => 'my implementation';
console.log(JSON.stringify(o));
위 예제처럼 date 객체는 문자열로 변환되기에 다시 JSON.parse
를 하더라도 date
객체로 돌아오지 않습니다:
console.log(typeof JSON.parse(JSON.stringify(new Date())));
이럴 때 reviver라고 불리는 parse의 두 번째 인자를 활용합니다:
let date = new Date();
let reviver = function (key, value) {
if (key[0] === '_') return undefined;
if (key === 'date') return new Date(value);
return value;
};
// ✍️ 언더스코어가 없는 프로퍼티를 추가해보세요
let o = { date, _unused: 1 };
let o2 = JSON.parse(JSON.stringify(o), reviver);
console.log(o2);
console.log(o2.date instanceof Date);
stringify의 두번째 인자에 문자열 배열을 전달하면 결과 문자열에 어떤 키를 어떤 순서로 넣을지를 결정합니다. 이런식으로 포맷을 고정시키는게 테스트 짤 때 유용하다고 하네요:
let o = { 1: 'one', 2: 'two', 3: 'three' };
console.log(JSON.stringify(o, [3, 1]));
배열 대신 함수를 전달하면 replacer 함수라고 부르며 reviver 함수의 정반대 역할을 합니다:
let replacer = function (key, value) {
if (key === '2') return undefined;
if (key === '1') return value + '!';
return value;
};
let o = { 1: 'one', 2: 'two', 3: 'three' };
console.log(JSON.stringify(o, replacer));
The Internalization API
숫자 포맷팅하기:
// ✍️ 옵션들을 바꿔보세요
let format = Intl.NumberFormat('ko', {
style: 'currency', // 'decimal' | 'currency' | 'percent'
// 스타일이 currency일 때 유효
currency: 'KRW',
currencyDisplay: 'name', // 'symbol' | 'code' | 'name'
// 숫자 사이 separator(쉼표 등) 관련
useGrouping: true,
// 최대/최소 숫자 개수 관련
minimumIntegerDigits: 5,
minimumFractionDigits: 1,
maximumFractionDigits: 4,
// scientific notation 관련.
// 위에 있는 세 프로퍼티를 덮어씁니다.
minimumSignificantDigits: undefined,
maximumSignificantDigits: undefined,
}).format;
console.log(format(1234.56789));
locale을 override하는 방법이 있다는데 자세한건 아래 문서를 읽어보세요:
let hindi1 = Intl.NumberFormat('hi-IN').format;
let hindi2 = Intl.NumberFormat('hi-IN-u-nu-deva').format;
console.log(hindi1(1234567890));
console.log(hindi2(1234567890));
Intl.DateTimeFormat
으로 날짜와 시간을 포맷팅할 수 있어요. 연도를 제외하는 등
Date 클래스의 toLocaleDateString()
이나 toLocaleTimeString()
보다 세밀하게
조절할 수 있습니다.
let d = new Date();
// ✍️ en-US로 바꿔보세요
let locale = 'ko-KR';
// ✍️ 옵션을 바꿔보세요
let options = {
// 아래 세 개는 'numeric'과 '2-digit'을 지원합니다
year: 'numeric',
month: 'numeric', // 추가로 'long' | 'short' | 'narrow'
day: 'numeric',
// 'long' | 'short' | 'narrow' 를 지원합니다.
weekday: 'long',
// 아래 세 개는 'numeric'과 '2-digit'을 지원합니다
hour: 'numeric',
minute: '2-digit',
second: 'numeric',
timeZone: 'Asia/Seoul',
timeZoneName: 'short', // 'short' | 'long'
hour12: true,
};
console.log(Intl.DateTimeFormat(locale, options).format(d));
사용할 달력 체계를 명시할 수도 있어요:
let date = new Date();
// era는 아주 옛날 날짜나 일본 달력등을 사용할 떄 활용됩니다.
let options = { year: 'numeric', era: 'short' };
console.log(Intl.DateTimeFormat('ko-KR', options).format(date));
console.log(Intl.DateTimeFormat('ko-KR-u-ca-buddhist', options).format(date));
console.log(Intl.DateTimeFormat('ko-KR-u-ca-japanese', options).format(date));
영어가 아닌 언어의 문자열 정렬을 Intl.Collator
로 할 수 있습니다:
// ✍️ numeric을 바꿔보세요
let compareFileName = new Intl.Collator('ko-KR', {
// 숫자를 포함한다면 이를 고려합니다.
numeric: true,
}).compare;
console.log(['page10', 'page9'].sort(compareFileName));
// ✍️ sensitivity를 바꿔보세요
let matcher = new Intl.Collator('ko-KR', {
// 'base' case 무시 accents 무시
// 'accent' case 무시 accents 고려
// 'case' case 고려 accents 무시
// 'variant' case 고려 accents 고려
sensitivity: 'base',
// 공백과 구두점 무시 여부
ignorePunctuation: true,
}).compare;
console.log(matcher('à', 'a') === 0);
console.log(matcher('e', 'E') === 0);
console.log(matcher('i', ' i ') === 0);
The Console API
블로그 컴포넌트에 Console API가 구현되어있지 않으니(ㅠ) 브라우저 콘솔탭에 붙여넣기해서 실행해보세요.
console.log('log');
console.debug('debug');
console.info('info');
console.warn('warn');
console.error('error');
console.trace('trace');
여러 로그를 묶을 수 있어요:
console.group('group');
console.log('1');
console.log('2');
console.groupEnd();
console.groupCollapsed('groupCollapsed');
console.log('1');
console.log('2');
console.groupEnd();
브라우저에서도 assert가 가능합니다:
console.assert(1 === 1, 'assert');
console.assert(1 === 2, 'assert');
표도 그릴수 있네요!!:
console.table({ x: 1, y: 2 });
console.table({ x: 1, y: { z: 2 } });
console.table(
[
{ x: 1, y: 3 },
{ x: 2, y: 2 },
{ x: 3, y: 2 },
],
// ✍️ 주석을 해제해보세요
// 'x',
);
호출 횟수를 기록할 수도 있어요. 이벤트 핸들러 디버깅할 때 유용하다고 하네요:
console.count('count');
console.count('count');
console.countReset('count');
console.count('count');
실행 시간을 기록할 수 있어요:
console.time('time');
console.timeLog('time');
console.timeEnd('time');
콘솔을 초기화할 수 있습니다:
console.clear();
C언어처럼 포맷을 정할 수도 있어요:
console.log('%s %d %f', 'str', 123.45, 123.45);
console.log(
'%c red %c blue',
'color: red; font-size: 16px',
'color: blue; font-size: 32px',
);
URL APIs
let url = new URL('https://example.com:3000/path/file?query=content#fragment');
console.log(url.href);
console.log(url.origin);
console.log(url.protocol);
console.log(url.host);
console.log(url.hostname);
console.log(url.port);
console.log(url.pathname);
console.log(url.search);
console.log(url.hash);
origin을 제외하고는 쓰기가 가능합니다. 결과는 href
나 toString()
으로 볼 수
있어요:
let url = new URL('http://a.xyz');
url.protocol = 'https';
url.hostname = 'example.com';
url.port = 3000;
url.pathname = '/path/file';
url.search = '?query=content';
url.hash = '#fragment';
console.log(url.href);
console.log(url.toString());
search
프로퍼티는 단순 문자열이라 수정하기 어려운데, 수정을 돕는 읽기 전용
searchParams
프로퍼티가 있습니다:
let url = new URL('https://example.com');
url.searchParams.append('key1', 'val1');
// 같은 키를 여러번 추가할 수 있어요
url.searchParams.append('key2', 'val2');
url.searchParams.append('key2', 'val3');
// 해당 키가 여러개면 모두 교체됩니다.
url.searchParams.set('key2', 'val4');
url.searchParams.append('key2', 'val5');
url.searchParams.append('key3', 'val6');
url.searchParams.delete('key3');
console.log([...url.searchParams]);
console.log(url.toString());
console.log(url.searchParams.has('key1'));
console.log(url.searchParams.get('key2'));
console.log(url.searchParams.getAll('key2'));
정렬도 시켜주네요 이렇게 친절할수가:
let url = new URL('https://example.com');
url.searchParams.append('z', 'zz');
url.searchParams.append('a', 'zz');
url.searchParams.append('aa', 'zz');
console.log(url.href);
url.searchParams.sort();
console.log(url.href);
searchParams
를 통하지 않고 아래처럼 적용할 수도 있어요:
let url = new URL('http://example.com');
let params = new URLSearchParams();
params.append('key1', 'val1');
url.search = params;
console.log(url.toString());
URL 문자열을 인코딩하기 위해 아래 함수들을 사용할 수 있어요.
let str = 'https://example.com:3000/path/file?query=content#fragment';
// /, ?, #와 같은 URL separator 문자를 escape하지 않습니다.
console.log(encodeURI(str));
// URL separator 문자를 escape합니다.
console.log(encodeURIComponent(str));
두 함수에 관련해서 읽어볼만한 아티클:
unixpapa.comJavascript Madness: Query String Parsing
Timers
// id의 타입은 브라우저와 노드에서 다릅니다.
// opaque value로 취급하세요.
let id = setTimeout(() => console.log('boom'), 1000);
Microtask, macrotask 등등 타이머 관련해서 다룰 내용이 많지만 이후 챕터에서 더 살펴볼게요.