Using Iterators

김병관
11 min readNov 8, 2015

--

이 Post에서는..

  • Symbol
  • Iteration Protocol
  • Generator Object
  • for…of (Iterator의 loop 방식)
  • tail call optimization

에 대해 소개 합니다.

Symbol

symbol 이라는 새로운 primitive data type 이 추가되었습니다.

(이로서 javascript 다음과 같은 총 6개의 primitive data type을 가짐. -> string, number, boolean, null, undefined, symbol)

특징

  • 유일한 값
  • 수정 불가
  • Literal form이 없어서 생성시에 항상 Symbol() 함수를 사용해야 함. 예) String: var s = ‘hello’; Number: var n = 7;
  • Symbol() 함수는 (option으로) 문자열을 인자로 받는데, 이 문자열은 디버깅 용도로 사용가능 하지만 해당 Symbol에 대한 접근 용으로는 사용 불가
  • primitive type.
  • 'new' 를 사용한 생성 불가

Symbol as Property keys

Object property key의 data type

  • ~ ES5: string
  • ES6: string, symbol

object property key는 ~ES5 까지는 string type으로만 가능 했지만 ES6에서는 symbol을 통해서도 가능하게 하였습니다. (사용법은 아래 예제 참조)

ES6에서 symbol을 도입한 가장 중요한 이유가, 기존에 object property key를 string key로 사용할 때 발생 가능한 key의 충돌을 예방하기 위함 이라네요.

Object.getOwnPropertySymbols()

object property key를 symbol을 사용 하였을 때는, Object.getOwnPropertyNames, Object.keys 를 사용해 symbol인 key를 가져오지 못합니다.(for .. in loop도 symbol을 가져오지 못함.)

따라서 symbol을 가져오려면 새로운 메소드가 필요하고 Object.getOwnPropertySymbols을 통해 접근 가능합니다.

위의 예와 같이 symbol을 key로 사용 할 때는 [symbol] 의 문법을 사용합니다.

Symbol.for(String)

Symbol(string)은 새로운 symbol을 만들 뿐 같은 string을 사용 하더라도 이미 만들어진 symbol을 가져 올 수는 없습니다. string을 key로 사용해 기존에 생성한 symbol을 가져오기 위해 Symbol.for()을 사용 할 수 있습니다.

Symbol.for(string)으로 만든 symbol은 string을 통해 global로 접근 가능합니다. 즉, Symbol.for(string) 을 사용해서, symbol을 global로 선언 하고 사용 할 수 있습니다.

The well-known symbols

well-known symbol 이라는 몇개의 built-in symbol이 있는데,

  • Symbol.iterator
  • Symbol.match
  • Symbol.species
  • 등등..

여기서는 그냥 이런게 있다고만 알고 있으면 됩니다. Symbol.iterator의 경우 다음 주제에서 나오니 그 사용 예를 보면, 이렇게 사용하는 구나... 하고 알 수 있습니다.

앞으로 well-knownsymbol을 언급할 때에 간편하게 부르기 위해서 '@@' 를 symbol 이름 앞에 붙입니다. (예: Symbol.iterator -> @@iterator)

The iteration protocols

The iterator protocol

iterator protocol을 구현 한 object를 iterator라 합니다.

iterator에는 next() 메소드가 있어야 하고, next()를 호출 하면 done, value property를 가진 object를 반환 합니다.

  • done: loop의 끝에 오면, 즉 더이상 next를 부를 수 없을 때 true를 반환 합니다. (그 이외에는 false를 반환)
  • value: 현재 item의 값을 나타내고, done이 true일 때는 값이 없습니다.

The iterable protocol

iterable protocol을 구현한 object를 iterable이라 합니다.

iterable은 property 로 iterator를 가지고, 이 property는 @@iterator를 key로 가집니다. (즉, let iterable[Symbol.iterator] = iterator )

위에서 well-known symbol인 Symbol.iterator는 @@iterator로 적는다고 했죠? @@iterator의 예와 같이 object내에서 특정 의미를 가진 property의 key로 well-known symbol을 사용 할 수가 있습니다.

Generators

Generator?

일반 함수와 같은 함수이지만 다음과 같은 특징을 가집니다.

  1. 함수 호출 시 함수 body를 바로 실행하지 않고, 새로운 instance 반환
  2. 이 instance는 iterator
  3. 반환된 instance(iterator) 는 next 메소가 있으며(iterator의 특징이죠?) next를 호출 할 때마다 특정 구간을 나누어서 실행
  4. 이 구간을 yield라는 keyword가 구분하며 각각 yield 뒤의 값이 반환 됨
  5. 각 구간 마다 값을 반환 하기 때문에 반환 값이 여러 개 일 수 있음

next(param)

next 메소드에는 인자를 줄 수 있는데, 이 인자는 이전에 수행된 yield의 반환 값이 됩니다.

아래 예에서 두 번째 next로 주어진 5는 첫번째 수행된 yield (line: 4)의 반환값이 되어 변수 a에 대입 됩니다.

return(value)

iterator loop을 모두 돌지 않고 끝낼 때 return 메소드를 사용 할 수 있습니다.

(chrome에는 현재 return 메소드가 없고 firefox는 지원합니다.)

throw(exception)

throw 메소드를 이용해서 함수 내에 exception을 발생 시킬 수 있습니다.

throw 를 사용하면 이전에 수행한 yield 다음 위치에서 exception이 발생 하고 catch구문에서 exception이 해소 되며, 그 다음 yield keyword까지 구문을 수행 하고 yield 값을 반환 합니다.

위의 예에서 throw는 line: 12의 yield 3; 구문 까지 수행 합니다.

yield*

yield* keyword는 뒤의 값이 generator 함수나 배열 일 때 각각의 항목 별로 yield를 수행 합니다.

대략 yield* [1, 2] => yield 1; yield 2; 처럼 된다고 생각하시면 됩니다.

참고) yield* 1; 과 같이 iterable object가 아닌 값을 사용하면 exception이 발생 합니다.

for…of

이제 까지 iterable object를 조회 할 때 next()를 사용하였는데, ES6은 기존의 loop방식 (for…) 과 비슷한 방식도 제공합니다.

for (let value of ‘iterable object’) { … }

와 같은 방식으로 iterable object의 각각의 값을 조회 할 수 있습니다.

The tail call optimization

tail call optimization에 대해 설명하기 전에 함수를 수행되는 과정을 virtual memory의 stack 관점에서 설명하고 넘어 가겠습니다.

function second(param) {
var b = 1;
return param + b; // C
}
function first(param) {
var a = 1;
return second(param + a); // B
}
console.log (first(3)); // A/* function call에 따른 stack의 사용 */
/*
stack ceil
---------------------------- // C 시점
[second context]
param =4;
b = 1;
---------------------------- // B 시점
[first context]
param = 3;
a = 1;
---------------------------- // A 시점
[root context]
first = function() ....
second = function() ....
----------------------------
stack floor */

first을 호출 하기 전에는(A 시점) first, second 함수에 대한 공간이 필요 합니다. first(3)을 호출한 후에는(B 시점) first내 인자, 지역변수 정보에 대한 공간을 stack에 추가 하고, first가 second를 호출 하면(C 시점) second내 인자, 지역변수 정보에 대한 공간을 추가 합니다.

이와 같은 함수 호출 과정을 수십, 수 백 번 수행 했을 때는 큰 문제가 없지만 수천, 수 만 번 수행하면 stack이 크게 늘어나 많은 memory, cpu resource를 사용합니다.

tail call optimization은 이와 같은 stack의 증가를 개선한 방식으로 아까의 예제가 어떻게 개선 되는지 설명하겠습니다.

위의 예제에서 first함수가 second함수를 호출 하기 전 까지는 stack증가 방식이 같습니다.

stack ceil
---------------------------- // B 시점
[first context]
param = 3;
a = 1;
---------------------------- // A 시점
[root context]
first = function() ....
second = function() ....
----------------------------
stack floor

하지만 생각해 보면 아래의 first함수는 second함수를 호출 후 더 이상 필요가 없습니다. 단지 second함수가 반환 된 값을 first함수를 호출 한 쪽으로 반환만 하면 됩니다. 즉, first 함수의 param과 지역변수 a를 위한 공간을 구지 유지 할 필요가 없습니다.

function first(param) {
var a = 1;
return second(param + a); // B
}

즉 아래와 같은 stack구조가 가능 합니다. second 함수를 수행 할 때 first함수를 위해 할당된 stack공간을 비우고 second함수를 위한 공간을 할당할 수 있습니다. 그리고 second함수의 반환 값은 원래 first함수가 반환 하려 했던 위치에 반환 하면 됩니다.

stack ceil
---------------------------- // C 시점
[second context]
param = 4;
a = 1;
---------------------------- // A 시점
[root context]
first = function() ....
second = function() ....
----------------------------
stack floor

이런 방식으로 stack의 크기 증가 없이 function call을 수행 할 수 있습니다.

하지만(however), tail call optimization을 항상 사용 하지는 못합니다. 아까 제가 stack에서 first 함수가 차지 하는 공간을 제거해도 된다고 할 때의 이유는 '더 이상 first 함수가 필요가 없어서' 였습니다.

만약 아직 first 함수가 할 일이 있다면 그 공간을 제거 할 수 없고 최적화도 할 수 없습니다. 아래의 예가 first함수가 할 일이 있을 때의 예입니다.

function first(param) {
var a = 1;
return a + second(param); // B
}
/* second함수 수행 후 a를 더하는 작업을 수행 해야 합니다.*/
function first(param) {
var a = 1;
var result = second(param + a); // B
return result;
}
/* second함수 수행 후 그 결과를 result변수에 할당 해야 합니다.*/

결국(in conclusion), 함수의 return 문 에 함수의 호출만 있을 때 tail call optimization이 가능합니다.

정리

이 포스트에서는 symbol을 사용해 object property key를 만드는 새로운 방법과, iterator, iterable protocol과 구현하는 방법에 대해 다루었습니다. 그리고 for..of를 이용해 iterable object을 loop하는 방법에 대해 다루었고, 마지막으로 tail call optimization이 무엇이고 최적화가 되기 위한 조건에 대해 다루었습니다.

참조

--

--