Skip to content

Latest commit

 

History

History
717 lines (537 loc) · 37.3 KB

2주차.md

File metadata and controls

717 lines (537 loc) · 37.3 KB

자료구조와 자료형

원시값의 메서드

자바스크립트에서는 메서드를 마치 객체처럼 다룰 수 있게 합니다. 하지만 여전히 원시값은 객체가 아님을 확실히 알고 있어야합니다.

원시값과 객체의 차이점에 대해 자세히 설명하면 아래와 같습니다.

원시값

  • 원시형의 종류는 문자(string), 숫자(number), bigint, 불린(boolean), 심볼(symbol), null, undefined형으로 총 일곱 가지 입니다.

객체

  • 프로퍼티에 다양한 종류의 값을 저장할 수 있습니다.
  • {name : "John", age : 30}와 같이 중괄호 {}를 사용해 만들 수 있습니다. 자바스크립트에는 여러 종류의 객체가 있는데, 함수도 객체의 일종입니다.
  • 또한 객체는 함수를 프로퍼티로 저장할 수 있는데, 이런 기능은 시스템의 자원을 원시값 보다 더 많이 사용하며, 내부 구조 유지를 위해 추가 자원을 사용합니다.

원시값을 객체로 사용하기

하지만 원시값은 객체로 사용할 수 있습니다. 이러한 목적을 위해 해결책은 다음과 같습니다.

  • 원시값은 원시값 그대로 남겨둬 단일 값 형태를 유지합니다.
  • 문자열, 숫자, 불린, 심볼의 메서드와 프로퍼티에 접근할 수 있도록 언어 차원에서 허용합니다.
  • 이를 가능하게 하기 위해, 원시값이 메서드나 프로퍼티에 접근하려 하면 추가 기능을 제공해주는 특수한 객체, "원시 래퍼 객체(object wrapper)"를 만들어 줍니다. 이 객체는 곧 삭제됩니다.

즉, 만약 str.toUpperCase()를 사용하게 될 때 순간적으로 원시 래퍼 객체를 생성합니다. 그리고 원시 래퍼 객체에 있는 .toUpperCase()에 접근하고 이를 실행합니다. 그리고 해당 기능이 끝날 경우, 원시 래퍼 객체를 파괴합니다.

그러면 객체 생성비용은요!! 라고 할 수 있지만, 자바스크립트 엔진 자체가 이를 최적화를 하였기에 성능에서 큰 문제를 발생시키지는 않습니다.

참고로, null과 undefined에는 메서드가 없습니다.

숫자형

모던 자바스크립트는 숫자를 나타내는 두 가지 자료형을 지원합니다.

  • 일반적인 숫자는 '배정밀도 부동소수점 숫자(double precision floating point number)'로 알려진 64비트 형식의 IEEE-754에 저장됩니다

  • 임의의 길이를 가진 정수는 BigInt 숫자로 나타낼 수 있습니다. 일반적인 숫자는 253이상이거나 -253이하일 수 없다는 제약 때문에 BigInt라는 새로운 자료형이 만들어졌습니다. BigInt는 아주 특별한 경우에만 사용합니다.

10억을 입력해야 하는 경우, 0을 많이 사용해 숫자를 표현하다 보면 잘못 입력하기 쉽기 때문에 실제로는 자바스크립트에서도 숫자 옆에 'e'를 붙이고 0의 개수를 그 옆에 붙여주면 숫자를 줄일 수 있습니다.

let billion = 1e9;  // 10억, 1과 9개의 0
alert( 7.3e9 );  // 73억 (7,300,000,000)

let ms = 1e-6; // 1에서 왼쪽으로 6번 소수점 이동

16진수, 2진수, 8진수

16진수는 색을 나타내거나 문자를 인코딩할 때 등 다양한 곳에서 두루 쓰입니다. 다양한 곳에서 쓰이는 만큼 당연히 16진수를 짧게 표현하는 방법도 존재하겠죠. 16진수는 0x를 사용해 표현할 수 있습니다.

alert( 0xff ); // 255
alert( 0xFF ); // 255 (대·소문자를 가리지 않으므로 둘 다 같은 값을 나타냅니다.)
// 2진수와 8진수는 아주 드물게 쓰이긴 하지만, 접두사 0b와 0o를 사용해 간단히 나타낼 수 있습니다.
let a = 0b11111111; // 255의 2진수
let b = 0o377; // 255의 8진수

alert( a == b ); // true, 진법은 다르지만, a와 b는 같은 수임

num.toString(base) 메서드를 통해 base진법으로 num을 표현한 후, 이를 문자형으로 변환해 반환합니다. 이는 쉽게 진수변환을 하게 해줍니다.

let num = 255;

alert( num.toString(16) );  // ff
alert( num.toString(2) );   // 11111111
  • 123456..toString(36)에 있는 점 두 개는 오타가 아닙니다. 위 예시처럼 숫자를 대상으로 메서드 toString을 직접 호출하고 싶다면 숫자 다음에 점 두 개 ..를 붙여야 합니다.

  • 123456.toString(36)처럼 점을 한 개만 사용하면, 첫 번째 점 이후는 소수부로 인식되어 에러가 발생할 수 있습니다. 점을 하나 더 추가하면 자바스크립트는 소수부가 없다고 판단하고 함수를 호출합니다.

    • (123456).toString(36)도 가능합니다.

어림수 구하기

  • Math.floor
    • 소수점 첫째 자리에서 내림(버림). 3.1은 3, -1.1은 -2가 됩니다.
  • Math.ceil
    • 소수점 첫째 자리에서 올림. 3.1은 4, -1.1은 -1이 됩니다.
  • Math.round
    • 소수점 첫째 자리에서 반올림. 3.1은 3, 3.6은 4, -1.1은 -1이 됩니다.
  • Math.trunc (Internet Explorer에서는 지원하지 않음)
    • 소수부를 무시. 3.1은 3이 되고 -1.1은 -1이 됩니다.
  • Math.toFixed(N)
    • 소수점 n 번째 수까지의 어림수를 구한 후 이를 문자형으로 반환해주는 메서드인 toFixed(n)를 사용합니다.
    let num = 12.34;
    alert( num.toFixed(1) ); // "12.3"

부정확한 계산

숫자는 내부적으로 64비트 형식 IEEE-754으로 표현되기 때문에 숫자를 저장하려면 정확히 64비트가 필요합니다. 64비트 중 52비트는 숫자를 저장하는 데 사용되고, 11비트는 소수점 위치를(정수는 0), 1비트는 부호를 저장하는 데 사용됩니다.

그런데 숫자가 너무 커지면 64비트 공간이 넘쳐서 Infinity로 처리됩니다.

자주 발생하는 현상인 정밀도 손실(loss of precision)도 있습니다.

정밀도 손실

alert( 0.1 + 0.2 == 0.3 ); // false

왜 이런 일이 발생하는 걸까요?

숫자는 0과 1로 이루어진 이진수로 변환되어 연속된 메모리 공간에 저장됩니다. 그런데 10진법을 사용하면 쉽게 표현할 수 있는 0.1, 0.2 같은 분수는 이진법으로 표현하면 무한 소수가 됩니다.

0.1은 1을 10으로 나눈 수인 1/10입니다. 10진법을 사용하면 이러한 숫자를 쉽게 표현할 수 있죠. 1/10과 1/3을 비교해봅시다. 1/3은 무한 소수 0.33333(3)이 됩니다.

이렇게 10의 거듭제곱으로 나눈 값은 10진법에서 잘 동작하지만 3으로 나누게 되면 10진법에서 제대로 동작하지 않습니다. 같은 이유로 2진법 체계에서 2의 거듭제곱으로 나눈 값은 잘 동작하지만 1/10같이 2의 거듭제곱이 아닌 값으로 나누게 되면 무한 소수가 되어버립니다.

10진법에서 1/3을 정확히 나타낼 수 없듯이, 2진법을 사용해 0.1 또는 0.2를 정확하게 저장하는 방법은 없습니다.

IEEE-754에선 가능한 가장 가까운 숫자로 반올림하는 방법을 사용해 이런 문제를 해결합니다. 그런데 반올림 규칙을 적용하면 발생하는 '작은 정밀도 손실’을 우리가 볼 수는 없지만 실제로 손실은 발생합니다.

해당 문제를 해결하기 위해서는 위에서 소개된 toFixed 메서드를 사용합니다. 이때 toFixed는 항상 문자열을 반환한다는 점에 유의해야 합니다.

isNan과 isFinite

isNaN(value) – 인수를 숫자로 변환한 다음 NaN인지 테스트함

alert( isNaN(NaN) ); // true
alert( isNaN("str") ); // true

그런데 굳이 이 함수가 필요할까요? "=== NaN 비교를 하면 되지 않을까?"라는 생각이 들 수 있습니다. 안타깝게도 대답은 '필요하다’입니다. NaN은 NaN 자기 자신을 포함하여 그 어떤 값과도 같지 않다는 점에서 독특합니다.

alert( NaN === NaN ); // false
isFinite(value) // 인수를 숫자로 변환하고 변환한 숫자가 NaN/Infinity/-Infinity가 아닌 일반 숫자인 경우 true를 반환함

alert( isFinite("15") ); // true
alert( isFinite("str") ); // false, NaN이기 때문입니다.
alert( isFinite(Infinity) ); // false, Infinity이기 때문입니다.

parseInt와 parseFloat

실무에선 CSS 등에서 '100px', '12pt'와 같이 숫자와 단위를 함께 쓰는 경우가 흔합니다. 대다수 국가에서 '19€'처럼 금액 뒤에 통화 기호를 붙여 표시하기에 수치를 뽑아내는 방법으로 내장 함수 parseInt와 parseFloat를 활용합니다.

두 함수는 불가능할 때까지 문자열에서 숫자를 읽어드리고, 숫자를 읽는 도중 오류가 발생하면 이미 수집된 숫자를 반환합니다. parseInt는 정수, parseFloat는 부동 소수점 숫자를 반환합니다.

alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5

alert( parseInt('12.3') ); // 12, 정수 부분만 반환됩니다.
alert( parseFloat('12.3.4') ); // 12.3, 두 번째 점에서 숫자 읽기를 멈춥니다.

parseInt(str, radix)의 두 번째 인수

parseInt()의 두 번째 매개 변수는 선택적으로 사용할 수 있습니다. radix는 원하는 진수를 지정해 줄 때 사용합니다. 따라서 parseInt를 사용하면 16진수 문자열, 2진수 문자열 등을 파싱할 수 있습니다.

alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255, 0x가 없어도 동작합니다.

alert( parseInt('2n9c', 36) ); // 123456

Math 관련 함수

Math.random() 0과 1 사이의 난수를 반환합니다(1은 제외).

alert( Math.random() ); // 0.1234567894322
alert( Math.random() ); // 0.5435252343232
alert( Math.random() ); // ... (무작위 수)
Math.max(a, b, c...) / Math.min(a, b, c...)

인수 중 최대/최솟값을 반환합니다.

alert( Math.max(3, 5, -10, 0, 1) ); // 5
alert( Math.min(1, 2) ); // 1
Math.pow(n, power)

n을 power번 거듭제곱한 값을 반환합니다.

alert( Math.pow(2, 10) ); // 2의 10제곱 = 1024

이 외에도 삼각법을 포함한 다양한 함수와 상수가 Math에 있습니다. 자세한 내용은 MDN 문서를 참고하도록!

문자열

자바스크립트에서 문자열은 페이지 인코딩 방식과 상관없이 항상 UTF-16 형식을 따릅니다. 작은따옴표와 큰따옴표는 기능상 차이가 없습니다. 그런데 백틱엔 특별한 기능이 있습니다. 표현식을 ${…}로 감싸고 이를 백틱으로 감싼 문자열 중간에 넣어주면 해당 표현식을 문자열 중간에 쉽게 삽입할 수 있죠. 이런 방식을 템플릿 리터럴(template literal)이라고 부릅니다.

문자열 특수 문자

  • \n : 줄 바꿈
  • \r : 캐리지 리턴(carriage return). Windows에선 캐리지 리턴과 줄 바꿈 특수 문자를 조합(\r\n)해 줄을 바꿉니다. 캐리지 리턴을 단독으론 사용하는 경우는 없습니다.
  • ', " : 따옴표
  • \ : 역슬래시
  • \t : 탭
  • \b, \f, \v : 각각 백스페이스(Backspace), 폼 피드(Form Feed), 세로 탭(Vertical Tab)을 나타냅니다. 호환성 유지를 위해 남아있는 기호로 요즘엔 사용하지 않습니다.
  • \xXX : 16진수 유니코드 XX로 표현한 유니코드 글자입니다(예시: 알파벳 'z'는 '\x7A'와 동일함).
  • \uXXXX : UTF-16 인코딩 규칙을 사용하는 16진수 코드 XXXX로 표현한 유니코드 기호입니다. XXXX는 반드시 네 개의 16진수로 구성되어야 합니다(예시: \u00A9는 저작권 기호 ©의 유니코드임).
  • \u{X…XXXXXX}(한 개에서 여섯 개 사이의 16진수 글자) : UTF-32로 표현한 유니코드 기호입니다. 몇몇 특수한 글자는 두 개의 유니코드 기호를 사용해 인코딩되므로 4바이트를 차지합니다. 이 방법을 사용하면 긴 코드를 삽입할 수 있습니다.

부분 문자열

  • slice
    • 문자열의 start부터 end까지(end는 미포함)를 반환합니다.
let str = "stringify";
alert( str.slice(0, 5) ); // 'strin', 0번째부터 5번째 위치까지(5번째 위치의 글자는 포함하지 않음)
alert( str.slice(0, 1) ); // 's', 0번째부터 1번째 위치까지(1번째 위치의 자는 포함하지 않음)

// 끝에서 4번째부터 시작해 끝에서 1번째 위치까지
alert( str.slice(-4, -1) ); // gif
  • substr
    • start에서부터 시작해 length 개의 글자를 반환합니다.
    • substr은 끝 위치 대신에 길이를 기준으로 문자열을 추출한다는 점에서 substring과 slice와 차이가 있습니다.
let str = "stringify";
alert( str.substr(2, 4) ); // ring, 두 번째부터 글자 네 개

alert( str.substr(-4, 2) ); // gi, 끝에서 네 번째 위치부터 글자 두 개

indexOf

  • 이 메서드는 문자열 str의 pos에서부터 시작해, 부분 문자열 substr이 어디에 위치하는지를 찾아줍니다. 원하는 부분 문자열을 찾으면 위치를 반환하고 그렇지 않으면 -1을 반환합니다.
let str = 'Widget with id';

alert( str.indexOf('Widget') ); // 0, str은 'Widget'으로 시작함
alert( str.indexOf('widget') ); // -1, indexOf는 대·소문자를 따지므로 원하는 문자열을 찾지 못함

alert( str.indexOf("id") ); // 1, "id"는 첫 번째 위치에서 발견됨 (Widget에서 id)
alert( str.indexOf('id', 2) ) // 12
  • str.lastIndexOf(substr, position)는 indexOf와 유사한 기능을 하는 메서드입니다.
    • 문자열 끝에서부터 부분 문자열을 찾는다는 점만 다릅니다.

반환되는 부분 문자열 위치는 문자열 끝이 기준입니다.

localCompare

  • 언어마다 문자 체계가 다르기 때문에 문자열을 ‘제대로’ 비교하는 알고리즘을 만드는 건 생각보다 간단하지 않습니다.

  • 문자열을 비교하려면 일단 페이지에서 어떤 언어를 사용하고 있는지 브라우저가 알아야 합니다.

  • 다행히도 모던 브라우저 대부분이 국제화 관련 표준인 ECMA-402를 지원합니다(IE10은 아쉽게도 Intl.js 라이브러리를 사용해야 합니다).

  • ECMA-402엔 언어가 다를 때 적용할 수 있는 문자열 비교 규칙과 이를 준수하는 메서드가 정의되어있습니다.

  • str.localeCompare(str2)를 호출하면 ECMA-402에서 정의한 규칙에 따라 str이 str2보다 작은지, 같은지, 큰지를 나타내주는 정수가 반환됩니다.

    • str이 str2보다 작으면 음수를 반환합니다.
    • str이 str2보다 크면 양수를 반환합니다.
    • str과 str2이 같으면 0을 반환합니다.

서로게이트 쌍

  • 자주 사용되는 글자들은 모두 2바이트 코드를 가지고 있습니다. 유럽권 언어에서 사용되는 글자, 숫자, 상형 문자 대다수는 2바이트 표현 체계를 사용합니다.

  • 그런데 2바이트는 65,536(2의 16승 – 옮긴이)개의 조합밖에 만들어내지 못하기 때문에 현존하는 기호를 모두 표현하기에 충분하지 않습니다. 이를 극복하기 위해 사용 빈도가 낮은 기호는 '서로게이트 쌍(surrogate pair)'이라 불리는 2바이트 글자들의 쌍을 사용해 인코딩합니다.

  • 기술적으로 서로게이트 쌍은 서로게이트 쌍에 대응하는 코드를 사용해 감지할 수 있습니다. 글자의 코드가 0xd800..0xdbff 사이에 있으면 이 코드는 서로게이트 쌍을 구성하는 첫 번째 글자를 나타낸다는 것을 알 수 있습니다.

  • 이 경우 서로게이트 쌍을 구성하는 두 번째 글자의 코드는 반드시 0xdc00..0xdfff 사이에 있어야 합니다. 범위 0xd800..0xdbff와 0xdc00..0xdfff는 표준에서 서로게이트 쌍을 위해 일부러 비워둔 코드입니다.

즉, 우리가 사용하는 영어나 단순한 글자가 아닌 대응하지 못하는 글자 코드에 대해서는 위의 나타난 0xdc00..0xdfff 범위내의 2가지를 통해 표현이 가능하다.

// charCodeAt는 서로게이트 쌍을 처리하지 못하기 때문에 서로게이트 쌍을 구성하는 부분에 대한 코드를 반환합니다.

alert( '𝒳'.charCodeAt(0).toString(16) ); // d835, 0xd800과 0xdbff 사이의 코드
alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3, 0xdc00과 0xdfff 사이의 코드

배열

  • 키를 사용해 식별할 수 있는 값을 담은 컬렉션은 객체라는 자료구조를 이용해 저장하는데, 객체만으로도 다양한 작업을 할 수 있습니다.
  • 순서가 있는 컬렉션을 다뤄야 할 때 객체를 사용하면 순서와 관련된 메서드가 없기에 객체는 태생이 순서를 고려하지 않고 만들어진 자료구조이기 때문에 객체를 이용하면 새로운 프로퍼티를 기존 프로퍼티 ‘사이에’ 끼워 넣는 것도 불가능합니다.
    • 이를 위해 index를 통해 접근 가능한 배열을 사용할 수 있다.

배열 내부의 동작원리

  • 배열은 특별한 종류의 객체입니다. 배열 arr의 요소를 arr[0]처럼 대괄호를 사용해 접근하는 방식은 객체 문법에서 왔습니다. 다만 배열은 키가 숫자라는 점만 다릅니다.

  • 숫자형 키를 사용함으로써 배열은 객체 기본 기능 이외에도 순서가 있는 컬렉션을 제어하게 해주는 특별한 메서드를 제공합니다. length라는 프로퍼티도 제공하죠. 그렇지만 어쨌든 배열의 본질은 객체입니다.

  • 이렇게 배열은 자바스크립트의 일곱 가지 원시 자료형에 해당하지 않고, 원시 자료형이 아닌 객체형에 속하기 때문에 객체처럼 동작합니다.

  • 배열은 객체와 마찬가지로 참조를 통해 복사됩니다.

  • 배열을 객체처럼 자유롭게 배열보다 큰 길이의 위치에 프로퍼티를 생성할 수 있고, 임의이름으로 프로퍼티를 추가할 수 있습니다. 하지만 이는 배열 특유의 장점을 지우는 것입니다.

  • 배열은 순서가 있는 자료를 저장하는 용도로 만들어진 특수한 자료구조입니다. 배열 내장 메서드들은 이런 용도에 맞게 만들어졌죠. 자바스크립트 엔진은 이런 특성을 고려하여 배열을 신중하게 조정하고, 처리하므로 배열을 사용할 땐 이런 목적에 맞게 사용해 주시기 바랍니다. 임의의 키를 사용해야 한다면 배열보단 일반 객체 {}가 적합한 자료구조일 확률이 높습니다.

성능

  • push와 pop은 빠르지만 shift와 unshift는 느립니다.

배열의 메서드

  • splice()
    • 요소를 자유자재로 하는 메서드
let arr = ["I", "study", "JavaScript"];

arr.splice(1, 1); // 인덱스 1부터 요소 한 개를 제거

alert( arr ); // ["I", "JavaScript"]
let arr = ["I", "study", "JavaScript", "right", "now"];

// 처음(0) 세 개(3)의 요소를 지우고, 이 자리를 다른 요소로 대체합니다.
arr.splice(0, 3, "Let's", "dance");

alert( arr ) // now ["Let's", "dance", "right", "now"]
  • concat()
    • 기존 배열의 요소를 사용해 새로운 배열을 만들거나 기존 배열에 요소를 추가하고자 할 때 사용합니다.
let arr = [1, 2];

// arr의 요소 모두와 [3,4]의 요소 모두를 한데 모은 새로운 배열이 만들어집니다.
alert( arr.concat([3, 4]) ); // 1,2,3,4

// arr의 요소 모두와 [3,4]의 요소 모두, [5,6]의 요소 모두를 모은 새로운 배열이 만들어집니다.
alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6

// arr의 요소 모두와 [3,4]의 요소 모두, 5와 6을 한데 모은 새로운 배열이 만들어집니다.
alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6
  • forEach
    • 주어진 함수를 배열 요소 각각에 대해 실행할 수 있게 해줍니다.
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
  alert(`${item} is at index ${index} in ${array}`);
});
  • find
    • 함수가 참을 반환하면 탐색은 중단되고 해당 요소가 반환됩니다. 원하는 요소를 찾지 못했으면 undefined가 반환합니다.
let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

let user = users.find(item => item.id == 1);

alert(user.name); // John
  • filter
    • 함수의 반환 값을 true로 만드는 단 하나의 요소를 찾습니다.
let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

// 앞쪽 사용자 두 명을 반환합니다.
let someUsers = users.filter(item => item.id < 3);

alert(someUsers.length); // 2

배열을 변형하는 메서드

  • map
    • 배열 요소 전체를 대상으로 함수를 호출하고, 함수 호출 결과를 배열로 반환해줍니다.
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths); // 5,7,6
  • sort
    • 배열의 요소를 정렬해줍니다. 배열 자체가 변경됩니다.
function compareNumeric(a, b) {
  if (a > b) return 1;
  if (a == b) return 0;
  if (a < b) return -1;
}

let arr = [ 1, 2, 15 ];

arr.sort(compareNumeric);

alert(arr);  // 1, 2, 15
  • split
    • 구분자(delimiter) delim을 기준으로 문자열을 쪼개줍니다.
let names = 'Bilbo, Gandalf, Nazgul';

let arr = names.split(', ');

for (let name of arr) {
  alert( `${name}에게 보내는 메시지` ); // Bilbo에게 보내는 메시지
}
  • join
    • 인수 glue를 접착제처럼 사용해 배열 요소를 모두 합친 후 하나의 문자열을 만들어줍니다.
let arr = ['Bilbo', 'Gandalf', 'Nazgul'];

let str = arr.join(';'); // 배열 요소 모두를 ;를 사용해 하나의 문자열로 합칩니다.

alert( str ); // Bilbo;Gandalf;Nazgul
  • reduce
    • 요소를 차례로 돌면서 func을 호출합니다. 반환값은 다음 함수 호출에 전달하고 최종적으로 하나의 값이 도출되어 출력합니다.
let arr = [1, 2, 3, 4, 5];

let result = arr.reduce((sum, current) => sum + current, 0);

alert(result); // 15

iterable 객체

  • 반복 가능한(iterable, 이터러블) 객체는 배열을 일반화한 객체입니다. 이터러블 이라는 개념을 사용하면 어떤 객체에든 for..of 반복문을 적용할 수 있습니다.

    • 이터러블엔 메서드 Symbol.iterator가 반드시 구현되어 있어야 합니다.
    • obj[Symbol.iterator]의 결과는 이터레이터라고 부릅니다. 이터레이터는 이어지는 반복 과정을 처리합니다.
    • 이터레이터엔 객체 {done: Boolean, value: any}을 반환하는 메서드 next()가 반드시 구현되어 있어야 합니다. 여기서 done:true은 반복이 끝났음을 의미하고 그렇지 않은 경우엔 value가 다음 값이 됩니다.
    • 메서드 Symbol.iterator는 for..of에 의해 자동으로 호출되는데, 개발자가 명시적으로 호출하는 것도 가능합니다.
    • 문자열이나 배열 같은 내장 이터러블에도 Symbol.iterator가 구현되어 있습니다.
    • 문자열 이터레이터는 서로게이트 쌍을 지원합니다.
  • Array.from(obj[, mapFn, thisArg])을 사용하면 이터러블이나 유사 배열인 obj를 진짜 Array로 만들 수 있습니다. 이렇게 하면 obj에도 배열 메서드를 사용할 수 있죠. 선택 인수 mapFn와 thisArg는 각 요소에 함수를 적용할 수 있게 해줍니다.

Map과 Set

  • 맵(Map)은 키가 있는 데이터를 저장한다는 점에서 객체와 유사합니다. 다만, 맵은 키에 다양한 자료형을 허용한다는 점에서 차이가 있습니다.

    • new Map() – 맵을 만듭니다.
    • map.set(key, value) – key를 이용해 value를 저장합니다.
    • map.get(key) – key에 해당하는 값을 반환합니다. key가 존재하지 않으면 undefined를 반환합니다.
    • map.has(key) – key가 존재하면 true, 존재하지 않으면 false를 반환합니다.
    • map.delete(key) – key에 해당하는 값을 삭제합니다.
    • map.clear() – 맵 안의 모든 요소를 제거합니다.
    • map.size – 요소의 개수를 반환합니다.
  • 또한 Map은 키값으로 객체를 허용합니다.

let map = new Map();

map.set('1', 'str1');   // 문자형 키
map.set(1, 'num1');     // 숫자형 키
map.set(true, 'bool1'); // 불린형 키

// 객체는 키를 문자형으로 변환한다는 걸 기억하고 계신가요?
// 맵은 키의 타입을 변환시키지 않고 그대로 유지합니다. 따라서 아래의 코드는 출력되는 값이 다릅니다.
alert( map.get(1)   ); // 'num1'
alert( map.get('1') ); // 'str1'

alert( map.size ); // 3

let john = { name: "John" };
// 고객의 가게 방문 횟수를 세본다고 가정해 봅시다.
let visitsCountMap = new Map();

// john을 맵의 키로 사용하겠습니다.
visitsCountMap.set(john, 123);

alert( visitsCountMap.get(john) ); // 123

맵 반복 작업

  • map.keys() – 각 요소의 키를 모은 반복 가능한(iterable, 이터러블) 객체를 반환합니다.
  • map.values() – 각 요소의 값을 모은 이터러블 객체를 반환합니다.
  • map.entries() – 요소의 [키, 값]을 한 쌍으로 하는 이터러블 객체를 반환합니다. 이 이터러블 객체는 for..of반복문의 기초로 쓰입니다.
let recipeMap = new Map([
  ['cucumber', 500],
  ['tomatoes', 350],
  ['onion',    50]
]);

// 키(vegetable)를 대상으로 순회합니다.
for (let vegetable of recipeMap.keys()) {
  alert(vegetable); // cucumber, tomatoes, onion
}

// 값(amount)을 대상으로 순회합니다.
for (let amount of recipeMap.values()) {
  alert(amount); // 500, 350, 50
}

// [키, 값] 쌍을 대상으로 순회합니다.
for (let entry of recipeMap) { // recipeMap.entries()와 동일합니다.
  alert(entry); // cucumber,500 ...
}

--- 
// 각 요소가 [키, 값] 쌍인 배열
let map = new Map([
  ['1',  'str1'],
  [1,    'num1'],
  [true, 'bool1']
]);

alert( map.get('1') ); // str1

---
let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);

let obj = Object.fromEntries(map.entries()); // 맵을 일반 객체로 변환 (*)

// 맵이 객체가 되었습니다!
// obj = { banana: 1, orange: 2, meat: 4 }

alert(obj.orange); // 2

  • 셋(Set)은 중복을 허용하지 않는 값을 모아놓은 특별한 컬렉션입니다. 셋에 키가 없는 값이 저장됩니다.
    • new Set(iterable) – 셋을 만듭니다. 이터러블 객체를 전달받으면(대개 배열을 전달받음) 그 안의 값을 복사해 셋에 넣어줍니다.
    • set.add(value) – 값을 추가하고 셋 자신을 반환합니다.
    • set.delete(value) – 값을 제거합니다. 호출 시점에 셋 내에 값이 있어서 제거에 성공하면 true, 아니면 false를 반환합니다.
    • set.has(value) – 셋 내에 값이 존재하면 true, 아니면 false를 반환합니다.
    • set.clear() – 셋을 비웁니다.
    • set.size – 셋에 몇 개의 값이 있는지 세줍니다.
let set = new Set();

let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

// 어떤 고객(john, mary)은 여러 번 방문할 수 있습니다.
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);

// 셋에는 유일무이한 값만 저장됩니다.
alert( set.size ); // 3

for (let user of set) {
  alert(user.name); // // John, Pete, Mary 순으로 출력됩니다.
}
  • 셋에도 맵과 마찬가지로 반복 작업을 위한 메서드가 있습니다.

    • set.keys() – 셋 내의 모든 값을 포함하는 이터러블 객체를 반환합니다.
    • set.values() – set.keys와 동일한 작업을 합니다. 맵과의 호환성을 위해 만들어진 메서드입니다.
    • set.entries() – 셋 내의 각 값을 이용해 만든 [value, value] 배열을 포함하는 이터러블 객체를 반환합니다. 맵과의 호환성을 위해 만들어졌습니다.

위크맵과 위크셋

자바스크립트 엔진은 도달 가능한 (그리고 추후 사용될 가능성이 있는) 값을 메모리에 유지합니다. 자료구조를 구성하는 요소도 자신이 속한 자료구조가 메모리에 남아있는 동안 대개 도달 가능한 값으로 취급되어 메모리에서 삭제되지 않습니다. 객체의 프로퍼티나 배열의 요소, 맵이나 셋을 구성하는 요소들이 이에 해당합니다.

예를 들어봅시다. 배열에 객체 하나를 추가해 보겠습니다. 이때 배열이 메모리에 남아있는 한, 배열의 요소인 이 객체도 메모리에 남아있게 됩니다. 이 객체를 참조하는 것이 아무것도 없더라도 말이죠.

아래 코드를 통해 확인해봅시다.

let john = { name: "John" };

let array = [ john ];

john = null; // 참조를 null로 덮어씀

// john을 나타내는 객체는 배열의 요소이기 때문에 가비지 컬렉터의 대상이 되지 않습니다.
// array[0]을 이용하면 해당 객체를 얻는 것도 가능합니다.
alert(JSON.stringify(array[0]));

// map도 마찬가지
let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // 참조를 null로 덮어씀

// john을 나타내는 객체는 맵 안에 저장되어있습니다.
// map.keys()를 이용하면 해당 객체를 얻는 것도 가능합니다.
for(let obj of map.keys()){
  alert(JSON.stringify(obj));
}

alert(map.size);

그렇다면 위크맵은 어떻게 될까요?

위크맵의 키로 사용된 객체를 참조하는 것이 아무것도 없다면 해당 객체는 메모리와 위크맵에서 자동으로 삭제됩니다.

let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // 참조를 덮어씀

// john을 나타내는 객체는 이제 메모리에서 지워집니다!
  • john을 나타내는 객체는 오로지 위크맵의 키로만 사용되고 있으므로, 참조를 덮어쓰게 되면 이 객체는 위크맵과 메모리에서 자동으로 삭제됩니다.

  • 맵과 위크맵의 두 번째 차이는 위크맵은 반복 작업과 keys(), values(), entries() 메서드를 지원하지 않는다는 점입니다. 따라서 위크맵에선 키나 값 전체를 얻는 게 불가능합니다.

    • weakMap.get(key)
    • weakMap.set(key, value)
    • weakMap.delete(key)
    • weakMap.has(key)
  • 왜 이렇게 적은 메서드만 제공할까요? 원인은 가비지 컬렉션의 동작 방식 때문입니다. 위 예시의 john을 나타내는 객체처럼, 객체는 모든 참조를 잃게 되면 자동으로 가비지 컬렉션의 대상이 됩니다. 그런데 가비지 컬렉션의 동작 시점은 정확히 알 수 없습니다.

  • 가비지 컬렉션이 일어나는 시점은 자바스크립트 엔진이 결정합니다. 객체는 모든 참조를 잃었을 때, 그 즉시 메모리에서 삭제될 수도 있고, 다른 삭제 작업이 있을 때까지 대기하다가 함께 삭제될 수도 있습니다. 현재 위크맵에 요소가 몇 개 있는지 정확히 파악하는 것 자체가 불가능한 것이죠. 가비지 컬렉터가 한 번에 메모리를 청소할 수도 있고, 부분 부분 메모리를 청소할 수도 있으므로 위크맵의 요소(키/값) 전체를 대상으로 무언가를 하는 메서드는 동작 자체가 불가능합니다.

위크맵을 사용하는 이유

추가 데이터

위크맵에 원하는 데이터를 저장하고, 이때 키는 객체를 사용하면 됩니다. 이렇게 하면 객체가 가비지 컬렉션의 대상이 될 때, 데이터도 함께 사라지게 됩니다.

// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // 위크맵에 사용자의 방문 횟수를 저장함

// 사용자가 방문하면 방문 횟수를 늘려줍니다.
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

위크맵을 사용해 사용자 방문 횟수를 저장하면 visitsCountMap을 수동으로 청소해줄 필요가 없습니다. john을 나타내는 객체가 도달 가능하지 않은 상태가 되면 자동으로 메모리에서 삭제되기 때문입니다. 위크맵의 키(john)에 대응하는 값(john의 방문 횟수)도 자동으로 가비지 컬렉션의 대상이 됩니다.

캐싱

위크맵은 캐싱(caching)이 필요할 때 유용합니다. 캐싱은 시간이 오래 걸리는 작업의 결과를 저장해서 연산 시간과 비용을 절약해주는 기법입니다. 동일한 함수를 여러 번 호출해야 할 때, 최초 호출 시 반환된 값을 어딘가에 저장해 놓았다가 그다음엔 함수를 호출하는 대신 저장된 값을 사용하는 게 캐싱입니다.

let cache = new WeakMap();

// 연산을 수행하고 그 결과를 위크맵에 저장합니다.
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* 연산 수행 */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

// 📁 main.js
let obj = {/* ... 객체 ... */};

let result1 = process(obj);
let result2 = process(obj);

// 객체가 쓸모없어지면 아래와 같이 null로 덮어씁니다.
obj = null;

// 이 예시에선 맵을 사용한 예시처럼 cache.size를 사용할 수 없습니다.
// 하지만 obj가 가비지 컬렉션의 대상이 되므로, 캐싱된 데이터 역시 메모리에서 삭제될 겁니다.
// 삭제가 진행되면 cache엔 그 어떤 요소도 남아있지 않을겁니다.

위크셋

  • 위크셋은 셋과 유사한데, 객체만 저장할 수 있다는 점이 다릅니다. 원시값은 저장할 수 없습니다.
  • 셋 안의 객체는 도달 가능할 때만 메모리에서 유지됩니다.
  • 셋과 마찬가지로 위크셋이 지원하는 메서드는 단출합니다. add, has, delete를 사용할 수 있고, size, keys()나 반복 작업 관련 메서드는 사용할 수 없습니다.

구조 분해 할당

  • 객체나 배열을 변수로 '분해’할 수 있게 해주는 특별한 문법인 구조 분해 할당(destructuring assignment) 을 사용할 수 있습니다.

객체 분해하기

let {prop : varName = default, ...rest} = object
  • object의 프로퍼티 prop의 값은 변수 varName에 할당되는데, object에 prop이 없으면 default가 varName에 할당됩니다.

  • 연결할 변수가 없는 나머지 프로퍼티들은 객체 rest에 복사됩니다.

배열 분해하기

let [item1 = default, item2, ...rest] = array
  • array의 첫 번째 요소는 item1에, 두 번째 요소는 변수 item2에 할당되고, 이어지는 나머지 요소들은 배열 rest 저장됩니다.

  • 할당 연산자 좌측의 패턴과 우측의 구조가 같으면 중첩 배열이나 객체가 있는 복잡한 구조에서도 원하는 데이터를 뽑아낼 수 있습니다.

Date 객체

  • 날짜를 저장할 수 있고, 날짜와 관련된 메서드도 제공해주는 내장 객체

  • 월은 0부터 시작합니다(0은 1월을 나타냅니다).

  • 요일은 getDay()를 사용하면 얻을 수 있는데, 요일 역시 0부터 시작합니다(0은 일요일을 나타냅니다).

  • 범위를 넘어가는 구성요소를 설정하려 할 때 Date 자동 고침이 활성화됩니다. 이를 이용하면 월/일/시간을 쉽게 날짜에 추가하거나 뺄 수 있습니다. 날짜끼리 빼는 것도 가능한데, 이때 두 날짜의 밀리초 차이가 반환됩니다. 이게 가능한 이유는 Date 가 숫자형으로 바뀔 때 타임스탬프가 반환되기 때문입니다.

  • Date.now()를 사용하면 현재 시각의 타임스탬프를 빠르게 구할 수 있습니다. 자바스크립트의 타임스탬프는 초가 아닌 밀리초 기준이라는 점을 항상 유의하시기 바랍니다.

  • 간혹 밀리초보다 더 정확한 시간 측정이 필요할 때가 있습니다. 자바스크립트는 마이크로초(1/1,000,000초)를 지원하진 않지만 대다수의 호스트 환경은 마이크로초를 지원합니다. 브라우저 환경의 메서드 performance.now()는 페이지 로딩에 걸리는 밀리초를 반환해주는데, 반환되는 숫자는 소수점 아래 세 자리까지 지원합니다.

JSON

  • JSON (JavaScript Object Notation)은 값이나 객체를 나타내주는 범용 포맷으로, RFC 4627 표준에 정의되어 있습니다. JSON은 본래 자바스크립트에서 사용할 목적으로 만들어진 포맷입니다.

  • 그런데 라이브러리를 사용하면 자바스크립트가 아닌 언어에서도 JSON을 충분히 다룰 수 있어서, JSON을 데이터 교환 목적으로 사용하는 경우가 많습니다. 특히 클라이언트 측 언어가 자바스크립트일 때 말이죠. 서버 측 언어는 무엇이든 상관없습니다.

    • JSON.stringify – 객체를 JSON으로 바꿔줍니다.
    • JSON.parse – JSON을 객체로 바꿔줍니다.
  • JSON은 일반 객체, 배열, 문자열, 숫자, 불린값, null을 지원합니다.

  • JSON.stringify를 사용하면 원하는 값을 JSON으로 직렬화 할 수 있고, JSON.parse를 사용하면 JSON을 본래 값으로 역 직렬화 할 수 있습니다.

  • 위 두 메서드에 함수를 인수로 넘겨주면 원하는 값만 읽거나 쓰는 게 가능합니다.

  • JSON.stringify는 객체에 toJSON 메서드가 있으면 이를 자동으로 호출해줍니다.

JSON Replacer

  • JSON으로 변환하길 원하는 프로퍼티가 담긴 배열을 두 번째 인수로 넘겨주면 이 프로퍼티들만 인코딩할 수 있습니다.
let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup은 room을 참조합니다.
};

room.occupiedBy = meetup; // room references meetup

alert( JSON.stringify(meetup, ['title', 'participants']) );
// {"title":"Conference","participants":[{},{}]}