ES6 또는 ES2015는 그야말로 대격변이 일어났습니다. 하나만 들어와도 벅찬 거대한 개념들이 무수히 많이 추가됬으며, 새로운 문법들도 다수 추가됬습니다. 이번 변경점에서 특히 눈여겨 봐야 할 기능들은 다음과 같습니다.
Block-ScopeArrow FunctionsSymbolClassPromiseIterator Protocol
Class와 같은 일부 기능은 ES5를 사용하여 구현할 수 있습니다. 이런 경우에는 ES5로 어떻게 구현할 수 있는지, 어떤 차이점이 있는지 기억해두면 좋습니다. 누구라도 알만한 유니콘 스타트업의 기술면접에서 실제로 출제되었습니다. ES6은 단언컨대 ECMAScript 역사상 가장 큰 업데이트입니다 😂
톺아보기 :
DefinitionletconstBlock-Scoped VariablesBlock-Scoped Functions
FunctionArrow FunctionsDefault Parameter ValueRest ParameterSpread Operator
LiteralsStringBackQoute LiteralTagged String LiteralUnicode Literal
NumberBinaryOctal
Regular ExpressionKeep Macthing Position
ObjectNew FeaturesProperty ShorthandComputed Property NamesMethod PropertyDestructuring Assignment
New Methods.assign()
StringNew Methods.repeat().startsWith().endsWith().includes()
NumberNew Methods.isNaN().isFinite().isSafeInteger()
New Static Constant.EPSILON
MathNew Methods.trunc().sign()
ModulesClassesSymbol TypeIterator Protocolfor ofIteratorsGenerators
DataStructureMapSetWeakMapWeakSet
TypedArraysPromisesMeta-ProgrammingProxyReflection
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.
반면에 let과 const는 블럭이 끝나면 선언의 효력이 사라지며, 중복선언시 에러가 발생합니다.
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 Scope와 Function-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()
소수점 버림연산입니다. 기존에는 ceil과 floor를 사용하여 직접 구현해야 했습니다.
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] 프로퍼티에 올바른 함수를 구현하면 됩니다. Array와 String은 기본적으로 해당 프로토콜이 구현되어 있습니다.
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];
단, Array와 Object는 기본적으로 유한하므로 별 문제가 없지만, 커스텀 프로토콜은 무한하게 요소를 생성해낼 수 있으므로 주의해주세요. Infinite Series에 for 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
TypedArray는 Array와 다르게 범위를 벗어난 데이터를 저장 시, 손실이 발생할 수 있습니다.
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
각 나라에 맞는 통화, 날짜 형식으로 포맷팅하는 유틸리티를 제공합니다.
'# Lang > ECMAScript' 카테고리의 다른 글
| ES11 (ES2020) New Features - 변경점 총정리 (0) | 2020.11.16 |
|---|---|
| ES9 (ES2018) New Features - 변경점 총정리 (0) | 2020.11.16 |
| ES8 (ES2017) New Features - 변경점 총정리 (0) | 2020.11.16 |
| ES7 (ES2016) New Features - 변경점 총정리 (0) | 2020.11.16 |
| ES5 (ES2009) New Features - 변경점 총정리 (0) | 2020.11.16 |