본문 바로가기

# Lang/ECMAScript

ES6 (ES2015) New Features - 변경점 총정리

ES6 또는 ES2015는 그야말로 대격변이 일어났습니다. 하나만 들어와도 벅찬 거대한 개념들이 무수히 많이 추가됬으며, 새로운 문법들도 다수 추가됬습니다. 이번 변경점에서 특히 눈여겨 봐야 할 기능들은 다음과 같습니다.

  • Block-Scope
  • Arrow Functions
  • Symbol
  • Class
  • Promise
  • Iterator Protocol

Class와 같은 일부 기능은 ES5를 사용하여 구현할 수 있습니다. 이런 경우에는 ES5로 어떻게 구현할 수 있는지, 어떤 차이점이 있는지 기억해두면 좋습니다. 누구라도 알만한 유니콘 스타트업의 기술면접에서 실제로 출제되었습니다. ES6은 단언컨대 ECMAScript 역사상 가장 큰 업데이트입니다 😂


톺아보기 :

  • Definition
    • let
    • const
    • Block-Scoped Variables
    • Block-Scoped Functions
  • Function
    • Arrow Functions
    • Default Parameter Value
    • Rest Parameter
    • Spread Operator
  • Literals
    • String
      • BackQoute Literal
      • Tagged String Literal
      • Unicode Literal
    • Number
      • Binary
      • Octal
  • Regular Expression
    • Keep Macthing Position
  • Object
    • New Features
      • Property Shorthand
      • Computed Property Names
      • Method Property
      • Destructuring Assignment
    • New Methods
      • .assign()
      •  
  • String
    • New Methods
      • .repeat()
      • .startsWith()
      • .endsWith()
      • .includes()
  • Number
    • New Methods
      • .isNaN()
      • .isFinite()
      • .isSafeInteger()
    • New Static Constant
      • .EPSILON
  • Math
    • New Methods
      • .trunc()
      • .sign()
  • Modules
  • Classes
  • Symbol Type
  • Iterator Protocol
    • for of
    • Iterators
    • Generators
  • DataStructure
    • Map
    • Set
    • WeakMap
    • WeakSet
  • TypedArrays
  • Promises
  • Meta-Programming
    • Proxy
    • Reflection


Scope

let

중복으로 선언할 수 없는 변수를 생성합니다.


ES 6:

let a = 1;
let a = 2; // error

ES 5:

ES5로 구현할 수 없습니다.


const

값의 변경이 불가능한 변수를 생성합니다.


ES6 :

const a = 123;
a = 999; // error

ES5 :

Object.defineProperty(this, "a", {
    value: 123,
    enumerable: true,
    writable: false,
    configurable: false,
});
a = 999; // will ignored


Block-Scoped Variables

var은 함수단위 스코프이므로 블럭이 끝나도, 여전히 함수 내부에서 사용할 수 있었습니다.

function f() {
    {
        var a = 1;
    }
    console.log(a); // will print 1
}

또한 var은 중복으로 변수를 선언해도 별다른 안전조치가 없습니다.

var a = 1;
var a = 2; // OK.

반면에 letconst는 블럭이 끝나면 선언의 효력이 사라지며, 중복선언시 에러가 발생합니다.

function f() {
    {
        let a = 1;
        let a = 2; // error: a has already been declared.
    }
    console.log(a); // error: a is not defined.
}

스코프 원리를 이해하기 위해 인터널하게 들여다보면 인터프리터는 var로 선언된 변수의 선언을 최상위로 끌어올려 처리합니다. 이러한 특징을 hoisting이라고 합니다.


우리가 작성한 코드 :

for (var c = 0; c < 10; c++) {
    console.log(c);
}
var a = 1;
var b = 2;

인터프리터가 실행하는 수도 코드 :

[declare] a;
[declare] b;
[declare] c;

for (c = 0; c < 10; c++) {
    //
    // a, b가 위에서 선언되었으므로,
    // a, b를 여기서 사용할 수 있음.
    console.log(c);
}
a = 1;
b = 2;
//
// c가 위에서 선언되었으므로,
// c를 여기서 사용할 수 있음.

그러나 위의 동작방식은 이치에 맞지 않으므로 let, const는 호이스팅되지 않습니다.


let으로 재작성된 코드 :

for (let c = 0; c < 10; c++) {
    console.log(c);
}
let a = 1;
let b = 2;

인터프리터가 실행하는 수도 코드 :

{
    [declare] c
    for(c = 0; c<10; c++){
        //
        // a, b가 선언되지 않았으므로,
        // a, b를 사용할 수 없음.
        console.log(c);
    }
    [undeclare] c
}
[declare] a
a = 1
[declare] b
b = 2
//
// c가 효력을 잃었으므로,
// c를 사용할 수 없음.

즉, Block-Level ScopeFunction-Level Scope의 특성은 다음과 같습니다.

  • 효력범위
    • 함수레벨 : 함수가 끝나야 선언이 사라진다.
    • 블럭레벨 : 블럭이 끝나야 선언이 사라진다.
  • 중복선언 가능여부
    • 함수레벨 : 가능
    • 블럭레벨 : 불가능
  • 호이스팅 여부
    • 함수레벨 : 호이스팅 적용됨
    • 블럭레벨 : 호이스팅이 적용되지 않음.


Block-Scoped Functions

자바스크립트는 함수선언문을 다음과 같이 호이스팅된 함수표현식으로 대체되는 것을 표준으로 하고 있습니다.


우리가 작성한 함수선언문 :

{
    function hello() {
        console.log("Hello, World!");
    }
}
hello(); // 가능? 불가능?

인터프리터가 해석한 코드 :

{
    var hello = function () {
        console.log("Hello, World!");
    };
}
hello(); // 가능!

그렇다면 다음 코드는 어떻게 동작할까요?

{
    function hello() {
        console.log("outer");
    }
    {
        function hello() {
            console.log("inner");
        }
        hello();
    }
    hello();
}
hello();

나중에 선언된 hello가 먼저 선언된 hello를 덮어쓰기 때문에 "inner"가 출력되는 것을 예상할 수 있으며, 실제로 옛날 자바스크립트 엔진은 이러한 방식으로 동작합니다.


구버전 엔진 :

inner
inner
inner

하지만 ES6 부터는 선행함수가 후행함수에 덮어씌어지지 않도록 약간 개선됩니다.


신버전 엔진 :

inner
outer
outer

하지만 ES6을 사용할 수 없어도 ES5만으로 해당 기능을 구현할 수 있습니다. var이 함수레벨 스코프라는 특성을 이용하는 것 입니다.


ES 5:

(function outerScope() {
    function hello() {
        console.log("outer");
    }
    (function innerScope() {
        function hello() {
            console.log("inner");
        }
        hello();
    })();
    hello();
})();

위의 코드는 아래와 같이 동작하기 때문입니다.

(function outerScope() {
    var hello;
    hello = function () {
        console.log("outer");
    };
    (function innerScope() {
        var hello;
        hello = function () {
            console.log("inner");
        };
        hello();
        //
        // var hello는 여기서 만료됨.
    })();
    hello();
    //
    // var hello는 여기서 만료됨.
})();


Function

Arrow Functions

Delcation

기존보다 더 짧은 함수선언이 가능합니다.


ES 6:

const hello = () => "Hello, World!";
const hello = () => {
    return "Hello, World!";
};

ES 5:

const hello = function () {
    return "Hello, World!";
};


LexicalThis

그러나 기존의 함수식과 완전히 같은것은 아닙니다. 기존의 함수표현식의 this는 호출문에서 봤을 때 dot으로 이어진 1단계 상위객체로 바인딩되지만, 화살표 함수는 자신을 생성한 함수의 this로 바인딩됩니다.


기존 함수표현식 :

기존의 함수표현식의 this는 호출문에서 봤을 때 dot으로 이어진 1단계 상위객체 입니다. 따라서, 기존의 함수표현식의 this는 동적입니다.

function increase() {
    this.count++;
}

const counter = {
    count: 0,
    increase: increase,
    utils: { increase: increase },
};

increase(); // this: global
counter.increase(); // this: counter
counter.utils.increase(); // this: counter.utils

//
// 출력하면 아래와 같다.
{
    count : 1,
    utils : {
        count : NaN
    }
}

화살표 함수 표현식 :

화살표 함수의 this는 자신을 생성한 함수의 this입니다. 따라서 화살표 함수의 this는 정적입니다.

let increase;
const counter = {
    count: 0,

    init: function () {
        //
        // 자신을 생성한 함수는 counter.init() 이고,
        // 그 함수의 this가 counter 이므로,
        // 이것의 this도 counter이다.
        const increaseImpl = () => {
            this.count++;
        };

        outerIncreaseImpl = increaseImpl;
    },
};
//
// increaseImpl을 increase로 빼낸다.
counter.init();

//
// 밖으로 빼내어도 increaseImpl의 this는 변하지 않는다.
increase();

함수화살표의 이러한 this 바인딩 방식을 Lexical Binding이라고 합니다.



Constructor

화살표 함수로 만들어진 함수는 prototype 프로퍼티를 갖지 않습니다. 즉, 생성자 자격이 없으므로 new 키워드와 함께 사용할 수 없습니다.



Default Param

주어지지 않은 파라미터의 초기값을 설정할 수 있습니다.


ES6 :

function f(x, y = 7, z = 42) {
    return x + y + x;
}

ES5 :

function f(x, y, x) {
    if (y === undefined) y = 7;
    if (z === undefined) z = 42;
    return x + y + x;
}


Rest Parameter

이후에 오는 모든 파라미터를 배열로 받을 수 있습니다.


ES6 :

function f(x, y, ...remains) {
    //
}

ES5 :

function f(x, y) {
    var remains = Array.prototype.slice.call(arguments, 2);
}


Spread Operator

ES6부터 추가되는 개념인 Iterator Protocol이 구현된 객체는 ...연산자를 통해 풀어헤쳐서 순서대로 파라미터에 전달할 수 있습니다. 순회 프로토콜에 대해서는 좀 더 아래에서 설명합니다.


ES6부터 배열은 Iterator Protocol가 구현됩니다.


ES6 :

const params = [3, 4];
function f(a, b, c, d) {
    return a + b + c + d;
}
f(1, 2, ...params); // f(1, 2, 3, 4)

ES5 :

var params = [3, 4];
function f(a, b, c, d) {
    return a + b + c + d;
}
f.apply(undefined, [1, 2].concat(params));

마찬가지로 문자열도 Iterator Protocol이 도입됩니다.


ES6 :

const word = "foo";
const chars = [...word]; // ["f", "o", "o"]

ES5 :

var word = "foo";
var chars = word.split(""); // ["f", "o", "o"]


Literals

String

BackQuote Literal

백틱문자를 사용하여 문자열을 선언할 수 있습니다.

const name = "World";
const message = `
        Hi, ${name}!
        Hello, ${name}!
        `;


Tagged String Literal

백틱 리터럴에서 문자열, 변수 목록을 가져올 수 있습니다.

function doubler(strings, ...values) {
    // strs : ["a", "b", "c"];
    // vals : [1, 2, 3];
    return strings[1].repeat(3);
}
const tagged = doubler`a${1}b${2}c${3}`;
console.log(tagged); // "bbb"


Unicode Literal

이제 유니코드를 완벽하게 지원합니다.


ES6 :

"𠮷" === "\u{20BB7}";

ES5 :

"𠮷" === "\uD842\uDFB7";


Number

Binary, Octal

2진 8진 리터럴을 지원합니다.


ES6 :

0b111110111 === 503;
0o767 === 503;

ES5 :

parseInt("111110111", 2) === 503;
parseInt("767", 8) === 503;


Regular Expression

Keep Matching Position

이제 정규식 결과에서, 각 토큰의 마지막 포지션 값을 알 수 있습니다.


ES6 :

let parser = (input, match) => {
    for (let pos = 0, lastPos = input.length; pos < lastPos; ) {
        for (let i = 0; i < match.length; i++) {
            match[i].pattern.lastIndex = pos;
            let found;
            if ((found = match[i].pattern.exec(input)) !== null) {
                match[i].action(found);
                pos = match[i].pattern.lastIndex;
                break;
            }
        }
    }
};

ES5 :

var parser = function (input, match) {
    for (var i, found, inputTmp = input; inputTmp !== ""; ) {
        for (i = 0; i < match.length; i++) {
            if ((found = match[i].pattern.exec(inputTmp)) !== null) {
                match[i].action(found);
                inputTmp = inputTmp.substr(found[0].length);
                break;
            }
        }
    }
};


Object

New Features

Property Shorthand

변수 이름을 프로퍼티 이름으로 사용할 경우, 객체 선언문을 축약할 수 있습니다.


ES6 :

const x = 1;
const y = 2;
const obj = { x, y };

ES5 :

var x = 1;
var y = 2;
var obj = { x: x, y: y };


Computed Property Names

표현식의 결과를 프로퍼티 이름으로 사용할 수 있습니다.


ES6 :

const obj = {
    foo: "bar",
    ["a".repeat(3)]: "bar",
};

ES5 :

const obj = {
    foo: "bar",
};
obj["a".repeat(3)] = "bar";


Method Property

객체 선언문에서 함수 선언문을 사용할 수 있습니다.


ES6 :

        const obj = {
            a(x,y){
                //
            }
            b(x,y){
                //
            }
        }

ES5 :

var obj = {
    a: function (x, y) {
        //
    },
    b: function (x, y) {
        //
    },
};


Destructuring Assignment

객체의 열거가능한 프로퍼티중 일부분을 추출하여 변수로 선언합니다.


배열의 각 원소는 열거가능하므로,


ES6 :

const list = [1, 2, 3];
const [a, , b] = list; // a=1, b=3
[b, a] = [a, b]; // b=3, a=1

ES5 :

var list = [1, 2, 3];
var a = list[0],
    b = list[2];
var tmp = a;
a = b;
b = tmp;

객체도 각 프로퍼티도 열거가능하므로,


ES6 :

const obj = {
    x: 1,
    y: 2,
    z: 3,
};
const { x, y, z } = obj;

ES5 :

var x = obj.x;
var y = obj.y;
var z = obj.z;

객체에서 사용 시, 재귀적으로 분해하거나 별칭을 부여할 수 있습니다.


ES6 :

const outer = {
    value: 1,
    inner: {
        value: 2,
    },
};
const {
    value: outer_v,
    inner: { value: inner_v },
} = outer;

ES5 :

var outer_v = outer.value;
var inner_v = outer.inner.value;

존재하지 않는 프로퍼티에 대해서는 기본값을 부여할 수 있습니다.


ES6 :

const obj = { a: 1 };
const { a, b = 2 } = obj;

const list = [1];
const [x, y = 2] = list;

ES5 :

var a = obj.a;
var b = obj.b === undefined ? 2 : obj.b;

var x = list[0];
var y = list[1] === undefined ? 2 : list[1];

함수 시그너쳐에도 적용할 수 있습니다.


ES6 :

function a([name, val]) {
    console.log(name, val);
}
a(["x", 1]);

function b({ name: n, val: v }) {
    console.log(n, v);
}
b({ name: "x", val: 1 });

function c({ name, val }) {
    console.log(name, val);
}
c({ name: "x", val: 1 });

ES5 :

    ```ts
    function a(arg) {
        var name = arg[0];
        var val = arg[1];
    }

    function b(arg) {
        var n = arg.name;
        var v = arg.val;
    }

    function c(arg) {
        var name = arg.name;
        var val = arg.val;
    }

---


일치하는 프로퍼티가 없다면 undefined가 적용됩니다.

---


**ES6 :**

```ts
        const list = [7, 42];
        const [a = 1, b = 2, c = 3, d] = list;
        a; // 7
        b; // 42
        c; // 3
        d; // undefined

ES5 :

var a = typeof list[0] !== "undefined" ? list[0] : 1;
var b = typeof list[1] !== "undefined" ? list[1] : 2;
var c = typeof list[2] !== "undefined" ? list[2] : 3;
var d = typeof list[3] !== "undefined" ? list[3] : undefined;


New Methods

assign()

주어진 src의 모든 프로퍼티를 dest에 할당합니다. 연산의 결과로 dest를 반환합니다. 중복된 프로퍼티는 후행의 것으로 덮어씌어집니다.


ES6 :

const dest = {};
const src1 = { a: 1, b: 2 };
const src2 = { a: 3, c: 4 };
Object.assign(dest, ...[src1, src2]);
console.log(dest.a); // 3
console.log(dest.b); // 2
console.log(dest.c); // 4

ES5 :

var dest = {};
var src1 = { a: 1, b: 2 };
var src2 = { a: 3, c: 4 };
Object.keys(src1).forEach(function (k) {
    dest[k] = src1[k];
});
Object.keys(src2).forEach(function (k) {
    dest[k] = src2[k];
});


String

New Methods

repeat()

해당 문자열을 n번 반복한 것을 반환합니다.

console.log("a".repeat(3)); // "aaa"


startsWith()

해당 문자열이 특정 워드를 접두사로 갖는다면 true를 반환합니다.

"Hello, World!".startsWith("H"); // true
"Hello, World!".startsWith("ld!"); // false


endsWith()

해당 문자열이 특정 워드를 접미사로 갖는다면 true를 반환합니다.

"Hello, World!".startsWith("H"); // false
"Hello, World!".startsWith("ld!"); // true


includes()

해당 문자열이 특정 워드를 포함하면 true를 반환합니다.

"Hello, World!".startsWith("H"); // true
"Hello, World!".startsWith("ld!"); // true


Number

New Methods

isNaN()

NaN이라면 true를 반환합니다. 등호로는 NaN을 검사할 수 없습니다.

const target = NaN;
target === NaN; // false
Number.isNaN(target); // true


isFinite()

양의 무한대 또는 음의 무한대라면 true를 반환합니다. 등호로도 검사할 수 있습니다.

const target = -Infinity;
target === -Infinity; // true
Number.isFinite(target); // true


isSafeInteger()

매우 큰 숫자, 매우 작은 숫자, 소수점이 포함된 숫자를 저장할 때 정확도를 희생합니다. 정확도가 포기되지 않았다면 true를 반환합니다.

Number.isSafeInteger(Number.MAX_SAFE_INTEGER * 1); // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER * 2); // false
Number.isSafeInteger(0.1); // false


New Constant

EPSILON

IEEE754 방식으로 표현할 수 있는 가장 작은 숫자를 가르킵니다. 이것은 매우 특수한 분야의 연구자를 위해 존재하는 값입니다. 일반적인 프로그래머들은 사용할 일이 없습니다.



Math

New Methods

trunc()

소수점 버림연산입니다. 기존에는 ceilfloor를 사용하여 직접 구현해야 했습니다.


ES6 :

Math.trunc(+42.7); // +42
Math.trunc(+0.1); // +0;
Math.trunc(-0.1); // -0
Math.trunc(-42.7); // -42

ES5 :

function trunc(n) {
    return n < 0 ? Math.ceil(n) : Math.floor(n);
}


sign()

주어진 수의 부호를 판별합니다.


ES6 :

Math.sign(+7); // +1
Math.sign(+0); // +0
Math.sign(-0); // -0
Math.sign(-7); // -1
Math.sign(NaN); // NaN

ES5 :

function sign(n) {
    if (Number.isNaN(n)) return NaN;
    if (n === 0) return 0;
    return x < 0 ? -1 : +1;
}


Modules

모듈 기능이 추가되었습니다. 모듈에 대해서는 또 다른 포스팅에서 다루겠습니다.



Classes

Definition

클래스 문법이 지원됩니다! 그러나 내부적으로는 여전히 프로토타입으로 작동합니다.


ES6 :

class Shape {
    constructor(id, x, y) {
        this.id = id;
        this.move(x, y);
    }
    move(x, y) {
        this.x = x;
        this.y = y;
    }
}

ES5 :

var Shape = function (id, x, y) {
    this.id = id;
    this.move(x, y);
};
Shape.prototype.move = function (x, y) {
    this.x = x;
    this.y = y;
};


Inheritance

상속도 마찬가지입니다.


ES6 :

class Rectangle extends Shape {
    constructor(id, x, y, width, height) {
        super(id, x, y);
        this.width = width;
        this.height = height;
    }
}

ES5 :

var Rectangle = function (id, x, y, width, height) {
    Shape.call(this, id, x, y);
    this.width = width;
    this.height = height;
};
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;


Expression Based Inheritance

무엇을 상속할지 동적으로 결정할 수 있습니다.


ES6 :

class A {
    name = "A";
}
class B {
    name = "B";
}
function getBaseClass() {
    return Math.random() < 0.5 ? A : B;
}

class C extends getBaseClass() {}
console.log(new C().name); // "A" or "B"

ES5 :

function A() {}
A.prototype.name = "A";

function B() {}
B.prototype.name = "B";

function getBaseClass() {
    return Math.random() < 0.5 ? A : B;
}

function C() {}
C.prototype = Object.create(getBaseClass().prototype);
C.prototype.constructor = C;

console.log(new C().name); // "A" or "B"


Base Class Access

Super 키워드를 사용하여 부모 클래스에 접근할 수 있습니다.


ES6 :

class A {
    val = 5;
    _getVal() {
        return this.val;
    }
}
class B extends A {
    getVal() {
        return super._getVal();
    }
}
console.log(new B().getVal()); // 5

ES5 :

function A() {
    this.val = 5;
}
A.prototype._getVal = function () {
    return this.val;
};

function B() {
    A.call(this); // super()
}
B.prototype.getVal = function () {
    return A.prototype._getVal.call(this);
};

console.log(new B().getVal()); // 5


Static Member

static 키워드로 클래스 수준에서 프로퍼티를 설정할 수 있습니다.


ES6 :

class Box {
    val = 0;
    static getRandomBox() {
        const newBox = new Box();
        newBox.val = Math.random();
        return newBox;
    }
}
Box.getRandomBox();

ES5 :

var Box = function () {
    this.val = 0;
};
Box.getRandomBox = function () {
    const newBox = new Box();
    newBox.val = Math.random();
    return newBox;
};
Box.getRandomBox();


getter/setter

get, set을 바로 클래스 내부에서 사용할 수 있습니다.


ES6 :

class Box {
    _val = 0;
    get val() {
        return this._val;
    }
    set val(newVal) {
        this._val = newVal;
    }
}

ES5 :

var Box = function () {
    this._val = 0;
};
Box.prototype = {
    get val() {
        return this._val;
    },
    set val(newVal) {
        this._val = newVal;
    },
};


Symbol Type

프로퍼티를 고유하게 식별할 수 있는 새로운 원시 자료형입니다. 인자로 프로퍼티 이름을 넣을 수 있지만, 프로퍼티 이름이 같다고 해서 같은 심볼을 가르키지 않습니다. 디버깅을 할 때 보기편하게 이름을 설정하는 것 뿐입니다.

const a = Symbol("x");
const b = Symbol("x");
console.log(a === b); // false

심볼은 프로퍼티를 고유하게 식별할 수 있습니다.

const a = Symbol("x");
const b = Symbol("x");
const obj = {};
obj[a] = 1;
obj[b] = 2;
console.log(obj[a], obj[b]); // 1, 2
console.log(obj); // { symbol(x):1, symbol(x):2 }

객체에 넣어진 심볼기반의 프로퍼티는 Object.getOwnPropertySymbols로 찾을 수 있습니다.

Object.getOwnPropertyNames(obj); // []
Object.getOwnPropertySymbols(obj); // [x, x]

Symbol.for을 사용하면 컨테이너에 저장된 이름이 같은 심볼을 공유합니다. 이것을 Global Symbol 이라고 합니다.

const a = Symbol.for("x");
const b = Symbol.for("x");
console.log(a === b); // true

특정 용도로 사용되는 Symbol은 스태틱 프로퍼티에 동봉되어 있습니다. 이것을 Well-Known Symbol 이라고 합니다.


이터레이터 심볼 :

  • Symbol.iterator
  • Symbol.asyncIterator

정규표현식 심볼 :

  • Symbol.match
  • Symbol.replace
  • Symbol.search
  • Symbol.split


Iterator Protocol

for of

Iterator Protocol이 구현된 객체는 for of 구문이나 Spread Operator를 사용할 수 있으며, 해당 구문을 사용시 프로토콜에 요청하여 요소들을 하나씩 가져옵니다. 커스텀 객체에 프로토콜을 정의하려면 [Symbol.iterator] 프로퍼티에 올바른 함수를 구현하면 됩니다. ArrayString은 기본적으로 해당 프로토콜이 구현되어 있습니다.

const list = [1, 2, 3, 4, 5];
const word = "Hello, World!";

//
// for of
for (const val of list) {
    console.log(val);
}
for (const char of word) {
    console.log(char);
}

//
// Spread Operator
const cloned = [...list];
foo(...list);
const chars = [...word];

단, ArrayObject는 기본적으로 유한하므로 별 문제가 없지만, 커스텀 프로토콜은 무한하게 요소를 생성해낼 수 있으므로 주의해주세요. Infinite Seriesfor of 또는 Spread Operator를 사용하면 무한루프에 빠질 수 있습니다.

//
// 1부터 Infinite까지 저장되어 있다고 가정합니다.
const list = [1 ... Infinite];

//
// 반복문이 끝나지 않습니다.
for(const val of list){
    console.log(val);
}

//
// 요것도 끝나지 않습니다!
const cloned = [...list];


Iterators

순회를 목적으로 하는 반복자입니다. 이 방식으로 프로토콜을 구현하고 싶다면, 다음 요소를 반환하는 함수를 반환하면 됩니다.

const list = {
    entries: {
        0: "a",
        1: "b",
    },
    [Symbol.iterator]: function () {
        let counter = 0;
        const entries = this.entries;
        return {
            next: function () {
                return {
                    value: entries[counter],
                    done: entries[counter++] === undefined,
                };
            },
        };
    },
};

for (const v of list) {
    console.log(v); // will print a, b
}

const values = [...list]; // ["a", "b"];


Generators

기본 사용법

yield 키워드로 요소를 프로토콜에 제공할 수 있으며, yield는 독특한 특성이 있기 때문에 iterator보다 훨씬 유연하고 정교한 작업이 가능합니다. 함수의 이름 앞에 *을 붙이면 Generators로 정의됩니다.


선언 :

function* fibonacci() {
    //
}

const fibonacci = function* () {
    //
};

const fibonacci = {
    *[Symbol.iterator]() {
        //
    },
};


프로토콜 읽기/쓰기

먼저 yield v표현식을 통해, 값을 프로토콜에 제공할 수 있습니다.

function* generator() {
    yield "Hello, World!";
    yield 12345;
}

위의 형식으로 프로토콜에 제공된 요소들은 Spread Operator, for-of로 읽을 수 있습니다.

//
// Spread Operator
const recived = [...generator()];

//
// for-of
const recived = [];
for (const e of generator()) {
    recived.push(e);
}

또는 Generator.prototype.next()를 사용하여 읽을 수 있습니다. 해당 메서드는 다음과 같은 형태의 값을 반환합니다.

{
    value : "Hello, World!", // 이번 차례에 제공된 값.
    done : false // 더 이상 제공할 요소가 없다면 true.
}

즉, 더 이상 제공할 요소가 없어질 때 까지 next()를 반복하면 됩니다.

const recived = [];
const g = generator();
let next = g.next();
while (next.done === false) {
    recived.push(next.value);
    next = g.next();
}


라이프사이클

제네레이터 함수는 기본적으로 프로토콜 읽기 요청을 받은 경우에만 동작합니다. 예를 들어, 아래의 코드는 제네레이터를 생성하긴 했지만, 프로토콜을 읽는 요청이 없으므로 console.log()에 도달하지 않습니다.

function* generator() {
    console.log("Hello, World!"); // 여기에 도달하지 못함.
    yield 1;
}
const g = generator();

프로토콜 읽기 요청을 감지하면 제네레이터 함수의 내부가 실행되며, 다음 yield에 제공된 값을 프로토콜에 넘기고, 즉시 일시정지합니다.

function* generator() {
    yield 1; // 프로토콜에 값을 넘기고 즉시 일시정지.
    console.log("Hello, World!"); // 여기에 도달하지 못함.
}
const g = generator();
console.log(g.next().value); // 읽기 요청

다음 읽기 요청이 들어오면, 일시정지했던 지점에서 다시 시작합니다.

function* generator() {
    yield 1; // 실행 후, 첫 번째 일시정지
    yield 2; // 실행 후, 두 번째 일시정지
    console.log("Hello, World!"); // 여기에 도달하지 못함.
}
const g = generator();
console.log(g.next().value); // 첫 번째 읽기 요청
console.log(g.next().value); // 두 번째 읽기 요청

마지막으로 제네레이터 함수의 끝(또는 리턴문)에 도달하면 프로토콜에 return value를 전달하고 프로토콜을 닫습니다. 리턴문이 없다면 undefined가 반환됩니다.

function* generator() {
    yield 1; // 실행 후, 첫 번째 일시정지
    yield 2; // 실행 후, 두 번째 일시정지
    return 3; // 실행 후, 프로토콜 닫음
}
const g = generator();
console.log(g.next().value); // 1, done: false
console.log(g.next().value); // 2, done: false
console.log(g.next().value); // 3, done: true


yield 표현식의 값 설정하기

yield n도 표현식이므로 값으로 평가될 수 있으며, 기본값은 undefined 입니다. (아래의 코드에서 yield)

function* generator() {
    //
    // yield가 호출되면 즉시 일시정지되므로,
    // console.log는 다음번 읽기요청에서 처리됩니다.
    console.log(yield 1);

    //
    // yield2는 호출되지만,
    // console.log에는 도달하지 않습니다.
    console.log(yield 2);
}
const g = generator();
console.log(g.next().value);
console.log(g.next().value);

//
// will prints
// 1
// undefined
// 2

yield n의 표현식의 결과값을 설정하고 싶다면 Generator.prototype.next()에 그것을 넘기면 됩니다.

function* generator() {
    console.log(yield 1); // yield 1이 "a"로 평가됨.
    console.log(yield 2);
}
const g = generator();
console.log(g.next("a").value);
console.log(g.next("b").value);

//
// will prints
// 1
// "a"
// 2


Iterator와 비교

  • 제네레이터의 함수 한 번당 여러개의 요소를 전달할 수 있지만, 이터레이터는 함수 한 번당 1개의 요소만을 전달할 수 있습니다.
  • 제네레이터는 함수가 중간에 일시정지될 수 있지만, 이터레이터는 그렇지 않습니다.
  • 제네레이터는 함수 형태로 작성하면, 직접적으로 인자를 받을 수 있습니다.
function* range(srt, end) {
    while (srt < end) {
        yield srt;
        srt++;
    }
}

for (const n of range(3, 6)) {
    console.log(n); // will print 3, 4, 5
}

const r = [...range(3, 6)]; // [3, 4, 5];
  • 모든 이터레이터는 제네레이터로 바꿔쓸 수 있지만, 어떤 제네레이터는 이터레이터로 바꿔쓸 수 없습니다.


DataStructure

Map

Key-Value 형태의 딕셔너리 자료구조입니다. Object와 유사하지만 다음과 같은 차이점이 있습니다.


의도하지 않은 키 방지

Object를 딕셔너리를 사용하면 프로토타입에 정의된 요소들을 엔트리로 인식할 수 있습니다. 반면에 Map은 프로토타입이 없는 Entry Container에 엔트리를 담고 있으므로 명시적으로 제공한 키 외에는 어떤 키도 가지지 않습니다.


object :

const map = {};
map[1] = 1;
console.log(map[0] !== undefined); // false
console.log(map[1] !== undefined); // true
console.log(map["toString"] !== undefined); // true

map :

const map = new Map();
map.set(1, 1);
console.log(map.has(0)); // false
console.log(map.has(1)); // true
console.log(map.has("toString")); // false


모든 형태의 키 허용

Object의 키는 String 또는 Symbol 타입이어야 합니다. 만약 이외의 키를 저장한 경우 String으로 캐스팅됩니다. 반면에 Map은 모든 형태의 키를 허용하며, 키를 손상시키지 않습니다.


object :

const map = {};
map[1] = 123;
map[true] = 456;
map[{ name: "AeroCode" }] = 789;

console.log(Object.keys(map));
//
// will print
// ["1", "true", "[object Object]"]

map :

const map = new Map();
map.set(1, 123);
map.set(true, 456);
map.set({ name: "AeroCode" }, 789);

console.log([...map.keys()]);
//
// will print
// [ 1, true, { name: 'AeroCode' } ]


삽입순서 유지

Map은 추가적인 메모리를 사용하여 삽입된 순서를 유지합니다, 그러므로 Map을 순회하면 먼저 삽입된 요소가 먼저 출력됩니다. 반면에 Object의 키는 삽입순서를 유지하지 않습니다.


object :

const map = {};
map[+1] = +1;
map[-1] = -1;
map[0] = +0;

console.log(Object.keys(map));
//
// will print
// [ '0', '1', '-1' ]

map :

const map = new Map();
map.set(+1, +1);
map.set(-1, -1);
map.set(0, 0);

console.log([...map.keys()]);
//
// will print
// [1, -1, 0]


엔트리 개수 파악

Map의 엔트리 개수는 size 프로퍼티를 통해 쉽고 빠르게 알아낼 수 있지만, Object의 항목 수는 직접 계산해야 하고, 매우 느립니다.


object :

const map = {};
const keys = Object.keys(map); // 오버헤드 지점
const size = keys.length;
console.log(size);

map :

const map = new Map();
console.log(map.size);


순회 프로토콜 지원

Map은 순회 프로토콜을 지원하므로 for of 구문과 Spread Operator를 사용하여 간편하게 순회할 수 있지만, Object는 그렇지 않으므로, 먼저 모든 키를 알아내는 과정이 필요합니다.


object :

const map = {};
const keys = Object.keys(map); // 키부터 알아내야 한다.
for (const key of keys) {
    const val = map[key];
    console.log(key, val);
}

map :

const map = new Map();
map.set(0, 1);
map.set(2, 3);

//
// for of
for (const [key, val] of map) {
    console.log(key, val);
}

//
// spread operator
const entries = [...map];
console.log(entries); // [ [ 0, 1 ], [ 2, 3 ] ]


퍼포먼스 비교

데이터의 개수와 연산의 종류 상관없이, Key가 중복될수록 Object가 유리하고, Key가 중복되지 않을수록 Map이 유리합니다.


object가 유리한 케이스 :

map[1] = 1;
map[1] = 2;
map[1] = 3;
map[1] = 4;
...
map[1] = 987654321;

map이 유리한 케이스 :

map.set(1, 1);
map.set(2, 2);
map.set(3, 3);
...
map.set(987654321, 987654321);


언제 사용해야 하나?

object를 사용해야 하는 경우 :

  • 저장할 데이터의 키가 중복되는 것이 많을 때. (= 대부분이 갱신일 때)
  • 객체인 경우가 더 자연스러운 경우. (= 쓸데없이 객체를 분리하여 맵 형태로 저장하지 말란 의미)
//
// do
const human = {
    name: "AeroCode",
    age: 25,
    addr: "...",
    hello: function () {
        console.log("Hello, My name is " + this.name + "!");
    },
};

//
// don't
const human = new Map();
human.set("name", "AeroCode");
human.set("age", 25);
human.set("addr", "...");
human.set("hello", function () {
    console.log("Hello, My name is " + this.name + "!");
});

map을 사용해야 하는 경우 :

  • 사용자의 입력에서 키를 받는 경우. (입력으로 toString와 같은 것이 들어올 수 있는 경우 object.prototype.toString과 충돌)
  • 저장된 데이터의 개수의 파악이 중요한 경우.
  • 문자열 외의 키를 허용해야 하는 경우.
  • 메모리를 좀 더 잡아먹더라도, 삽입된 순서가 중요한 경우.


Set

Key만 저장할 수 있는 Map이라고 생각하면 됩니다. 그것을 제외한 대부분의 특성은 Map과 같습니다.

  • 삽입순 정렬
  • 의도치 않은 키 없음
  • 모든 형태의 키 허용
  • 순회 프로토콜 지원
  • 쉬운 엔트리 개수 파악

Object와의 비교결과도 Map과 같습니다.

  • 중복될수록 object가 유리
  • 중복되지 않을수록 set이 유리


WeakMap

메모리 누수

엔트리의 키로 객체가 사용된 경우, 해당 객체가 스코프에서 사라지더라도 Map이 키의 목록을 유지하려고 강하게 참조하고 있기 때문에 GC의 대상이 되지 않습니다.

const map = new Map();
function foo() {
    const obj = {
        hello: function () {
            console.log("Hello, World!");
        },
    };
    map.set(obj, "Hello!");

    //
    // 함수가 끝나는 시점에서 obj는 GC의 수집대상이 되어야 하지만,
    // map이 obj를 강하게 붙잡고 있으므로 GC의 수집대상에서 벗어남.
}
foo();
console.log([...map.keys()]); // [obj];

그러나 위와 같은 동작은 메모리 누수의 원인이 될 수 있습니다.

const map = new Map();
function foo() {
    for (let i = 0; i < 123456789; i++) {
        const key = { idx: i };
        const val = { val: i };
        map.set(idx, val);
    }
}
foo();
console.log(map.size);
//
// will print 123456789
// 메모리가 해제되지 않아...!


느슨한 참조

따라서 Map에서 키 목록 유지 기능을 빼버린 WeakMap이 함께 등장했습니다. 더 이상 키 목록을 유지할 필요가 없기 때문에 키와의 연결이 느슨해지므로 키로 사용된 객체GC에 의해 수집될 수 있습니다.

const weakMap = new WeakMap();
function foo() {
    const obj = {
        hello: function () {
            console.log("Hello, World!");
        },
    };
    weakMap.set(obj, "Hello!");
    //
    // weakMap은 obj를 붙잡고 있지 않으므로,
    // obj는 GC에 의해 수거될 수 있다!
}
foo();


Map과의 비교

  • Map과 달리 키 목록 유지기능이 없음.
    • 즉, 키 목록을 얻을 수 있는 방법이 없음.
    • 즉, .keys() 메서드를 지원하지 않음.
    • 즉, Iterator Protocol을 지원하지 않음.
    • 즉, 메모리 누수를 예방할 수 있음.
  • Map과 달리 Primitive Type을 키로 지정할 수 없음.


WeakSet

WeakMap과 그 특성이 같습니다.



TypedArrays

개요

기본적으로는 Array와 같지만 숫자값만 저장할 수 있다는 것이 특징입니다. 내부적으로는 ArrayBuffer를 사용하여 바이트 단위로 읽기/쓰기를 수행하는 DataView의 일종입니다.

Int8Array;
Uint8Array;
Int16Array;
Uint16Array;
Int32Array;
Uint32Array;
Float32Array;
Float64Array;


Array와의 비교

Array는 동적크기 배열이지만 TypedArray는 고정크기 배열입니다. 내부적으로 참조하는 ArrayBuffer의 크기를 변경할 수 없기 때문입니다.

//
// Array는 배열의 크기가 늘어날 수 있다.
const array = new Array(5);
console.log(array.length); // 5
array.push(5);
console.log(array.length); // 6

TypedArrayArray와 다르게 범위를 벗어난 데이터를 저장 시, 손실이 발생할 수 있습니다.

const array = new Int8Array(1);

//
// 12345 = 0b 00110000 00111001
// Int8Array는 1바이트만 저장할 수 있으므로,
// 12345의 마지막 바이트인 00111001만 저장된다.
array[0] = 12345;

//
// 0b00111001 = 57
console.log(array[0]); // 57


Promise

Callback Hell

프로마이즈는 아래와 같은 콜백지옥 문제를 해결하기 위해 등장한 비동기 작업 실행자입니다. 콜백지옥은 코드의 패딩을 증가시키고, 가독성을 크게 저하시킵니다.

do_1st(
    initVal,
    function (result) {
        do_2nd(
            result,
            function (result) {
                do_3nd(
                    result,
                    function (result) {
                        do_4nd(result, function (result) {
                            // ...
                        });
                    },
                    failureCallback_1
                );
            },
            failureCallback_2
        );
    },
    failureCallback_3
);


Chaining

프로마이즈는 또 다른 프로마이즈와 연결될 수 있는데, 이러한 특성을 사용하여 콜백지옥 문제를 해결합니다. 이것을 Promise Chaining이라고 부릅니다.

promiseObject
    .then(nextCallback_1, failureCallback_1)
    .then(nextCallback_2, failureCallback_2)
    .then(nextCallback_3, failureCallback_3);

failureCallback이 모두 같다면 다음과 같이 축약할 수 있습니다.

promiseObject
    .then(nextCallback_1)
    .then(nextCallback_2)
    .then(nextCallback_3)
    .catch(failureCallback);


프로마이즈의 상태

프로마이즈는 다음 3가지 중 하나를 갖습니다.

  • 대기 (pending) : resolve 또는 reject되지 않은 초기 상태
  • 이행 (fulfilled) : 프로마이즈가 resolve된 상태
  • 거부 (rejected) : 프로마이즈가 reject된 상태

//
// 프로마이즈가 생성될 당시에는 아직 pending 상태.
// 곧, 프로마이즈의 내부로직이 실행됨.
const promiseObject = new Promise((resolve, reject) => {
    console.log("in Promise");
    if (Math.random() < 0.5) {
        //
        // 아래 메서드 실행 후, fulfilled 상태로 변함.
        resolve("Success");
    } else {
        //
        // 아래 메서드 실행 후, rejected 상태로 변함.
        reject("Fail");
    }
});

//
// 이미 프로마이즈가 실행되어 pending 상태에서 벗어났으므로,
// "done" 에 앞서 "in Promise"가 출력됨.
console.log("done");
in Promise
done


콜백과의 차이

CallBack은 동기식이지만 Promise는 비동기로 작동합니다. 즉, CallBack은 후행 코드들을 블럭킹합니다.


CallBack :

function doSomething(onSuccess, onFailure) {
    try {
        console.log("in doSomething");
        if (Math.random() < 0.5) {
            throw new Error();
        }
        const result = "Hello, World!";
        onSuccess(result);
    } catch (reason) {
        onFailure(reason);
    }
}

function main() {
    console.log("start");
    doSomething(
        (result) => console.log("success"),
        (reason) => console.log("failure")
    );
    console.log("end");
}
main();
start
in doSomething
success // or failure
end

Promise :

const doSomething = new Promise((resolve, reject) => {
    console.log("in doSomething");
    if (Math.random() < 0.5) {
        const result = "Hello, World!";
        resolve(result); // = return
    } else {
        reject(); // = throw
    }
});

function main() {
    console.log("start");
    doSomething
        .then((result) => console.log("success"))
        .catch((reason) => console.log("failure"));
    console.log("end");
}
main();
in doSomething
start
end
success // or failure


다수의 프로마이즈 관리

Promise.all()

iteratorable에 저장된 프로마이즈가 전부 이행되어야 fulfilled, 하나라도 거절되면 rejected 상태로 변하는 프로마이즈를 생성합니다.

function makeRandomPromise() {
    return new Promise((resolve, reject) => {
        if (Math.random() < 0.5) {
            resolve();
        } else {
            reject();
        }
    });
}

const promises = [];
for (let i = 0; i < 3; i++) {
    const promise = makeRandomPromise();
    promises.push(promise);
}

Promise.all(promises)
    .then(() => console.log("모든 프로마이즈가 이행됨."))
    .catch(() => console.log("어떤 프로마이즈가 거절됨."));


Promise.race()

iteratorable에 저장된 프로마이즈 중, 가장 빠르게 상태가 변화한 프로마이즈의 상태를 사용합니다. 즉, 가장 빨리 처리된 프로마이즈의 상태가 이행이라면 fulfilled로, 거절이라면 rejected로 변화하는 프로마이즈를 생성합니다.

function makeDelayPromise(delay: number) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() < 0.5) {
                resolve(`resolved with ${delay}`);
            } else {
                reject(`rejected with ${delay}`);
            }
        }, delay);
    });
}

const promises = [];
for (let i = 0; i < 3; i++) {
    const delay = Math.floor(Math.random() * 1000);
    const promise = makeDelayPromise(delay);
    promises.push(promise);
}

Promise.race(promises)
    .then((v) => console.log(v))
    .catch((v) => console.log(v));


Meta Programming

Proxy

특정 객체에 접근하기 전에 훅을 먼저 실행하는 대리자를 생성합니다.

const object = {
    name: "Sample Object",
    desc: "Hello, World!",
};

const objectProxy = new Proxy(object, {
    get: function (target, key) {
        if (key in target) {
            return target[key];
        }
        throw new Error(`No such key.`);
    },
});

const name = objectProxy.name; // OK
const addr = objectProxy.addr; // error

가능한 훅은 다음과 같습니다.

interface ProxyHandler<T extends object> {
    getPrototypeOf?(target: T): object | null;
    setPrototypeOf?(target: T, v: any): boolean;
    isExtensible?(target: T): boolean;
    preventExtensions?(target: T): boolean;
    getOwnPropertyDescriptor?(
        target: T,
        p: PropertyKey
    ): PropertyDescriptor | undefined;
    has?(target: T, p: PropertyKey): boolean;
    get?(target: T, p: PropertyKey, receiver: any): any;
    set?(target: T, p: PropertyKey, value: any, receiver: any): boolean;
    deleteProperty?(target: T, p: PropertyKey): boolean;
    defineProperty?(
        target: T,
        p: PropertyKey,
        attributes: PropertyDescriptor
    ): boolean;
    enumerate?(target: T): PropertyKey[];
    ownKeys?(target: T): PropertyKey[];
    apply?(target: T, thisArg: any, argArray?: any): any;
    construct?(target: T, argArray: any, newTarget?: any): object;
}


Reflect

흩어져있는 객체, 함수, 생성자 관련 함수들을 한데 묶어놓은 유틸리티 객체입니다.

Reflect.apply();
Reflect.construct();
Reflect.defineProperty();
Reflect.deleteProperty();
Reflect.get();
Reflect.getOwnPropertyDescriptor();
Reflect.getPrototypeOf();
Reflect.has();
Reflect.isExtensible();
Reflect.ownKeys();
Reflect.preventExtensions();
Reflect.set();
Reflect.setPrototypeOf();


Localization

각 나라에 맞는 통화, 날짜 형식으로 포맷팅하는 유틸리티를 제공합니다.