위 문법은 이터러블한 객체(Iterable Interface를 준수한 객체, (Typed )Array, String, (Weak)Map/Set)만 쓸 수 있는 문법이다.
1
console.log(Array.prototype);
반면에 Object는 이터러블한 객체가 아니다.
1
console.dir(Object.prototype);
Symbol.iterator() 메소드를 눈 씻고 찾아볼 수가 없다. 따라서 for of와 같은 문법을 쓸 수 없다. 하지만 현재 object에도 spread operator와 destructuring assignment를 쓸 수 있게 한 제안이 Stage-3까지 올라가 있어서 바벨의 transform-object-rest-spread나 Stage 3 preset을 쓰면 된다. 그럼 이제 이터러블과 이터레이터가 뭔지 알아보자.
Iterator 관련 Interface
타입스크립트의 인터페이스 표기법을 사용하고 있으므로 타입스크립트에 익숙한 사람은 이미지를 보자마자 무슨 의미인지 알 수 있을 것이다.
Iterable Interface
인터페이스이기 때문에 객체가 가져야하는 키와 그 키가 가져야하는 값을 명시하고 있다.
가져야하는 키: Symbol.iterator(well-known symbol 중 하나)
키(Symbol.iterator): 함수인데 반환 값은 Iterator 인터페이스를 준수한 객체가 오면 된다.
// this를 바인딩해야하므로 ES5식 함수 사용 const makeIteratorResultObject = function() { console.log(this.length) return { // IteratorResult 인터페이스를 준수한 객체를 반환 value: this.pop(), // value 값이 반환됨. done: this.length === 0 }; };
// this를 바인딩해야하므로 ES5식 함수 사용 const makeIteratorObject = function() { return { // Iterator 인터페이스를 준수한 객체를 반환 next: () => { // IteratorResult 인터페이스를 준수한 객체를 반환 return makeIteratorResultObject.call(this); } } };
const arr = [1, 2, 3, undefined, 0];
// arr은 Iterable 인터페이스를 준수한 객체가 됨. arr[Symbol.iterator] = function() { // Iterator 인터페이스를 준수한 객체를 반환 return makeIteratorObject.call(this) };
const iter = arr[Symbol.iterator]();
console.dir(iter); // Iterator 오브젝트를 반환함. // pop 하기 전 length: 5, pop 한 후 length 4 console.log(iter.next()); // Object {value: 0, done: false} // pop 하기 전 length: 4, pop 한 후 length 3 console.log(iter.next()); // Object {value: undefined, done: false} // pop 하기 전 length: 3, pop 한 후 length 2 console.log(iter.next()); // Object {value: 3, done: false} // pop 하기 전 length: 2, pop 한 후 length 1 console.log(iter.next()); // Object {value: 2, done: false} // pop 하기 전 length: 1, pop 한 후 length 0 // 즉 value를 실행해서 pop을 먼저 실행하고 그 이후의 length인 0을 가지고 done이 평가되는 거임. // 그리고 그 done이 true이면 value를 반환하지 않게되니 1이 반환되지 않는 거였음. console.log(iter.next()); // Object {value: 1, done: true} console.log(iter.next()); // Object {value: undefined, done: true}
// this를 바인딩해야하므로 ES5식 함수 사용 const makeIteratorResultObject = function(idx) { return { // IteratorResult 인터페이스를 준수한 객체를 반환 value: this.slice(-idx)[0], // value 값이 반환됨. done: --idx === this.length }; };
// this를 바인딩해야하므로 ES5식 함수 사용 const makeIteratorObject = function() { let idx = 0; return { // Iterator 인터페이스를 준수한 객체를 반환 next: () => { // 이 next 함수 안에 있는 내용은 매번 실행됨. // IteratorResult 인터페이스를 준수한 객체를 반환 return makeIteratorResultObject.call(this, ++idx); } } };
const arr = [1, 2, 3, undefined, 0];
// arr은 Iterable 인터페이스를 준수한 객체가 됨. arr[Symbol.iterator] = function() { // 요 함수에 있는 내용은 한 번만 실행됨. // Iterator 인터페이스를 준수한 객체를 반환 return makeIteratorObject.call(this); };
// 배열이 숫자로만 이루어져있는지 파악하는 함수 //ES3 var isArrNum = function(arr) { var isNum = false; // 프로그래머가 실수로 let i = 1;이라고 초기화한다면? // 실수로 i<arr.length+1; 이라고 조건식을 잘못 입력한다면? // i+=2; 라고 증감식을 잘못 입력한다면? // arr[i+1]로 잘못 참조한다면? // 이렇게 일일이 초기화, 조건식 지정, 증감식 지정 등등을 일일이 해줘야하므로 귀찮다. for(var i=0; i<arr.length; i++) { console.log(arr[i]); if(!isNaN(arr[i])) { // 숫자라면 // 아래 있는 코드는 실행할 필요 없이 다음 요소를 검사해야함. // 즉 다음 반복문을 실행. continue; } // 숫자가 아니라면 isNum = false; break; }
return isNum; };
// ES5 var isArrNum2 = function(arr) { // 과연 직관적이라고 말할 수 있는가? return !arr.some(v => { console.log(v); returnisNaN(v); }); };
// ES6 constisArrNum3 = arr => { // 실수할 요소가 확연히 줄어들고 ES5보다 훨씬 직관적으로 변하였다. let isNum = true; for(const v of arr) { console.log(v); if(!Number.isNaN(+v)) continue; isNum = false; break; } return isNum; }