사실 JS를 새로 학습하기 시작하면서 생겼던 의문이 있다. object
의 형태가 class
와 너무 유사해보여서 class
라는 개념이 존재하는지에 대해서도 긴가민가 했다. 이에 대해서 적어보려고 한다.
const Person = {
height: a,
weight: b,
} // object
class Person {
height;
weight;
#className = 'swTrack';
constructor(height, weight) {
this.height = height;
this.weight = weight;
} // 생성자
}
const personOne = new Person(174, 70);
미래의 나를 위한 설명이기 때문에 자세한 설명은 생략하고, 간략하게 설명하자면 기존의 다른 객체 지향 언어들과 상당히 유사한 모습을 지녔다. 생성자도 존재하고, 그 이외의 클래스 내 변수들도 존재한다. 앞에 #
을 붙이는 경우 private
과 동일한 기능을 지니게 되며 스코프 외부에서 참조하는 것이 불가능하게 된다. console.log(personOne.#className);
이 안먹힌다는 소리다.
class Element {
#origin = 'Element';
...
}
class Input extends Element {
constructor() {
super(); // 부모의 속성을 상속받음
this.name = 'Input'; // 덮어쓰기
}
}
상속과 같은 개념도 물론 존재한다. 자식 생성자 부분의 super()
는 부모의 데이터들을 상속받는 역할을 한다. 사실 super()
을 사용하지 않으면 그냥 문법 오류가 떠버려서 필수적으로 써야하고, 그렇기 때문에 구조상 부모 클래스의 모든 값들을 상속 받을 수 밖에 없다. 일반적인 캡슐화를 위한 private
속성을 구현하기 위해서는 변수 앞에 #
을 붙이면 된다. 그 예가 Element
클래스의 #origin
이다. 심볼을 사용하는 방식도 존재하긴 하지만 문법적으로 #
이 공식화 되었기 때문에 굳이 알아야 할 필요는 없을 것 같다.
function iter() {
let num = 1;
return {
next: () => {
const done = num > 10; // num이 10보다 클 시 done = true
const item = { done };
if (!done) item.value = num++; // 10보다 작을시 num 값 1 증가
return item;
},
};
}
const numEx = iter();
console.log(numEx.next());
console.log(numEx.next());
console.log(numEx.next());
가장 기본적인 이터레이터 사용 예시이다. 동적인 특성을 지니고 있는데, 그렇기 때문에 메모리 관리 측면에서 유리하다는 장점을 가지고 있다. 이런 카운팅 예제에서 특히 많이 쓰이는 것 같고, 사용 시 next()
메서드를 호출하면 된다.
next() -> { done: true | false, value : ? }
next()
자체의 사용 방식이다. 조건이 충족되어 done
값이 true
가 된다면 그 이후에 아무리 함수를 호출하여도 계속해서 같은 값이 반복되어 나온다. 위의 예제를 봐도 이미 10보다 커져버렸으니 값을 증가시킬 필요가 없기 때문이다.
const nums = [1, 2, 3, 4, 5];
const elements = document.querySelectorAll('div');
for (const num of nums) {
console.log(num);
}
for (const element of elements) {
console.log(element); // Node 0, 1, 2, 3 ...
}
예제 코드를 보면 elements
는 querySelectorAll()
을 통해 다중 값들을 가져온 모습을 확인할 수 있다. 이 같은 경우 모든 값들이 배열 형태로 들어왔다는 착각을 할 수 있는데, 저런식으로 가져오게 된다면 Node
형태로 값을 가져오는 모습을 콘솔창으로 확인할 수 있다. iterable
은 말 그대로 어떠한 집합이던 간에 for .. of
문법을 적용할 수만 있다면 이터러블(반복 가능)의 예가 될 수 있다는 것이다.
function iter() {
let num = 1;
const iterator = {
next: () => {
const done = num > 5;
const item = { done };
if (!done) item.value = num++;
},
};
return {
[Symbol.iterator]() {
return iterator;
},
};
}
for (const num of iter()) {
console.log(num);
}
[Symbol.iterator]
은 이터레이터 객체를 반환한다. next()
와 저런식으로 나누어 작성할 수 있고, 기능은 사실 iterator
예제와 동일하다고 보면 된다.
class Nums {
#num = 1;
next() {
if (this.#num > 11) {
return { done: true };
}
const value = this.#num;
this.#num += 1;
return {
done: false,
value,
};
}
[Symbol.iterator]() {
return this;
}
}
클래스 내의 iterator
사용도 비슷한 느낌이다. 저 문법을 외운다는 느낌보다는 원리에 대한 이해가 필요할 때마다 찾아서 보면 될 것 같다. 사실 이것보다 훨씬 쉬운 방식이 다음 내용에 나오기 때문이다.
앞의 모든 내용들은 이 제네레이터 개념을 이해하기 위한 빌드업이라고 생각하면 된다. 구현 방법은 간단하지만 원리는 iterator
를 어느정도 이해할 수 있어야 한다. 예제 코드를 보자.
function* iter() {
for (let i = 1; i <= 10; i++) {
yield i;
}
}
for (const num of iter()) {
console.log(num);
}
function*, yield
라는 키워드가 익숙치 않을 수 있지만, 각각 반복 함수, 반환 value 값 정도인 것 같다. 그냥 메모리 관리가 필요하거나 저런 식의 카운팅 구현이 필요할 때마다 다시 이 곳을 찾아 방법을 확인하게 될 것 같다.
function* pick(items) {
for (let i = 0; i < items.length; i++) {
for (let j = i + 1; j < items.length; j++) {
yield [items[i], items[j]];
}
}
}
const coms = [...pick(["hello", "world", "good", "day"])];
console.log(coms);
위의 예제는 목록들 중 몇개를 선정하여 팀을 짜는 Combination
기능 구현인데, 확실히 제네레이터는 코드 길이와 로직 구현에서 훨씬 짧고 간단하다는 특성이 있다.
function* pick(items, count) {
yield* _pick([], count, 0);
function* _pick(state, count, i) {
for (; i < items.length; i++) {
const picked = [...state, items[i]];
if (count === 1) {
yield picked;
} else {
yield* _pick(picked, count - 1, i + 1);
}
}
}
}
const samples = [1, 2, 3, 4, 5, 6, 7];
const coms = [...pick(samples, 5)];
console.log(coms);
이 예제 코드를 이해할 정도면 generator
에 대한 이해가 완벽하게 끝났다고 봐도 될 정도라고 한다. 서칭을 해보니 yield*
부분은 iterator()
가 끝나는 시점 done, true
가 되었을 때의 리턴값을 갖는다고 한다.
먼저 조합을 담을 그릇 state
를 []
로 초기화 시킨 후에 _pick
함수를 실행시켜 조합을 만들게 된다. count
의 값은 조합이 늘어날 때마다 1씩 감소되게 하고 i 값은 1씩 증가하면서 계속해서 _pick
함수를 재귀 호출하게 된다.