배열의 요소를 어떻게 순회하나요? 20년 전 JavaScript가 등장했을 때 여러분은 다음과 같이 했을 것입니다.
for (var index = 0; index < myArray.length; index++) { console.log(myArray[index]); } for (var index = 0; index < myArray.length; index++) { console.log(myArray[index]); }
ES5부터 내장된 forEach 메소드를 사용할 수 있습니다:
JavaScript myArray.forEach(function (value) { console.log(value); }); myArray.forEach(function (value) { console.log(value); });
코드가 더 간소화되었지만 작은 단점이 있습니다. break 문을 사용하여 루프에서 벗어날 수 없고 return 문을 사용하여 클로저 함수에서 반환할 수 없습니다.
배열을 순회하는 for 구문이 있으면 훨씬 더 편리할 것입니다.
그럼 for-in을 활용해 볼까요?
for (var index in myArray) { // 实际代码中不要这么做 console.log(myArray[index]); } for (var index in myArray) { // 实际代码中不要这么做 console.log(myArray[index]); }
다음과 같은 이유로 좋지 않습니다.
위 코드의 인덱스 변수는 숫자형이 아닌 "0", "1", "3" 등과 같은 문자열이 됩니다. 일부 작업("2" 1 == "21")에 참여하기 위해 문자열의 인덱스를 사용하는 경우 결과가 예상과 다를 수 있습니다.
배열 자체의 요소뿐만 아니라 사용자가 추가한 추가(확장) 요소도 탐색됩니다. 예를 들어 배열에 myArray.name 속성이 있으면 index="name이 나타납니다. 특정 루프" 상황. 게다가 배열 프로토타입 체인의 속성도 탐색할 수 있습니다.
가장 놀라운 점은 어떤 경우에는 위의 코드가 어떤 순서로든 배열 요소를 순회한다는 것입니다.
간단히 말하면 for-in은 키-값 쌍이 포함된 객체를 순회하도록 설계되었으며 배열에는 그다지 친숙하지 않습니다.
강력한 for-of 루프
지난번에 언급한 내용을 기억하세요. ES6은 기존 JS 코드의 정상적인 작동에 영향을 미치지 않습니다. 이미 수천 개의 웹 애플리케이션이 for-in 기능을 사용하고 있으며 심지어 배열의 특성도 for-in에 의존하므로 아무도 없습니다. 위의 문제를 해결하기 위해 기존 for-in 구문을 "개선"할 것을 제안한 적이 있습니다. ES6가 이 문제를 해결하는 유일한 방법은 새로운 루프 순회 구문을 도입하는 것입니다.
새로운 구문은 다음과 같습니다.
for (var value of myArray) { console.log(value); } for (var value of myArray) { console.log(value); }
위의 for-in 구문을 도입하면 이 구문이 그다지 인상적으로 보이지는 않습니다. for-of의 경이로움에 대해서는 나중에 자세히 소개하겠습니다. 이제 여러분은 이것만 알면 됩니다:
for-in은 객체의 속성을 반복하는 데 사용됩니다.
for-of는 배열의 요소와 마찬가지로 데이터를 반복하는 데 사용됩니다.
그러나 이것이 for-of의 모든 기능은 아니며 아래에 더 흥미로운 부분이 있습니다.
for-of를 지원하는 기타 컬렉션
for-of는 배열용으로 설계되었을 뿐만 아니라 DOM 개체 모음인 NodeList와 같은 배열과 유사한 개체에도 사용할 수 있습니다.
은 문자열을 유니코드 문자 모음으로 처리하는 문자열을 탐색하는 데에도 사용할 수 있습니다.
Map 및 Set 개체에서도 작동합니다.
아마도 Map 및 Set 객체는 ES6의 새로운 객체이고 나중에 자세히 소개할 별도의 기사가 있기 때문에 들어본 적이 없을 것입니다. 이 두 개체를 다른 언어에서 사용해 본 적이 있다면 훨씬 간단합니다.
예를 들어 Set 개체를 사용하여 배열 요소의 중복을 제거할 수 있습니다.
JavaScript // make a set from an array of words var uniqueWords = new Set(words); // make a set from an array of words var uniqueWords = new Set(words);
Set 객체를 얻은 후에는 객체를 탐색하게 될 가능성이 높습니다. 이는 매우 간단합니다.
for (var word of uniqueWords) { console.log(word); } for (var word of uniqueWords) { console.log(word); }
맵 객체는 키-값 쌍으로 구성되며 탐색 방법이 약간 다릅니다. 키와 값을 각각 받으려면 두 개의 독립 변수를 사용해야 합니다.
for (var [key, value] of phoneBookMap) { console.log(key + "'s phone number is: " + value); } for (var [key, value] of phoneBookMap) { console.log(key + "'s phone number is: " + value); }
지금까지 여러분은 이미 알고 계셨습니다. JS는 이미 일부 컬렉션 객체를 지원하고 있으며 앞으로 더 많은 객체를 지원할 예정입니다. for-of 구문은 이러한 컬렉션 개체를 위해 설계되었습니다.
for-of는 객체의 속성을 순회하는 데 직접 사용할 수 없습니다. 객체의 속성을 순회하려면 for-in 문(이 경우 for-in이 사용됨)을 사용하거나 다음 방법:
// dump an object's own enumerable properties to the console for (var key of Object.keys(someObject)) { console.log(key + ": " + someObject[key]); } // dump an object's own enumerable properties to the console for (var key of Object.keys(someObject)) { console.log(key + ": " + someObject[key]); }
内部原理
“好的艺术家复制,伟大的艺术家偷窃。” — 巴勃罗·毕加索
被添加到 ES6 中的那些新特性并不是无章可循,大多数特性都已经被使用在其他语言中,而且事实也证明这些特性很有用。
就拿 for-of 语句来说,在 C++、JAVA、C# 和 Python 中都存在类似的循环语句,并且用于遍历这门语言和其标准库中的各种数据结构。
与其他语言中的 for 和 foreach 语句一样,for-of 要求被遍历的对象实现特定的方法。所有的 Array、Map 和 Set 对象都有一个共性,那就是他们都实现了一个迭代器(iterator)方法。
那么,只要你愿意,对其他任何对象你都可以实现一个迭代器方法。
这就像你可以为一个对象实现一个 myObject.toString() 方法,来告知 JS 引擎如何将一个对象转换为字符串;你也可以为任何对象实现一个 myObject[Symbol.iterator]() 方法,来告知 JS 引擎如何去遍历该对象。
例如,如果你正在使用 jQuery,并且非常喜欢用它的 each() 方法,现在你想使所有的 jQuery 对象都支持 for-of 语句,你可以这样做:
// Since jQuery objects are array-like, // give them the same iterator method Arrays have jQuery.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; // Since jQuery objects are array-like, // give them the same iterator method Arrays have jQuery.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
你也许在想,为什么 [Symbol.iterator] 语法看起来如此奇怪?这句话到底是什么意思?问题的关键在于方法名,ES 标准委员会完全可以将该方法命名为 iterator(),但是,现有对象中可能已经存在名为“iterator”的方法,这将导致代码混乱,违背了最大兼容性原则。所以,标准委员会引入了 Symbol,而不仅仅是一个字符串,来作为方法名。
Symbol 也是 ES6 的新特性,后面将会有单独的文章来介绍。现在你只需要知道标准委员会引入全新的 Symbol,比如 Symbol.iterator,是为了不与之前的代码冲突。唯一不足就是语法有点奇怪,但对于这个强大的新特性和完美的后向兼容来说,这个就显得微不足道了。
一个拥有 [Symbol.iterator]() 方法的对象被认为是可遍历的(iterable)。在后面的文章中,我们将看到“可遍历对象”的概念贯穿在整个语言中,不仅在 for-of 语句中,而且在 Map和 Set 的构造函数和析构(Destructuring)函数中,以及新的扩展操作符中,都将涉及到。
迭代器对象
通常我们不会完完全全从头开始去实现一个迭代器(Iterator)对象,下一篇文章将告诉你为什么。但为了完整起见,让我们来看看一个迭代器对象具体是什么样的。(如果你跳过了本节,你将会错失某些技术细节。)
就拿 for-of 语句来说,它首先调用被遍历集合对象的 [Symbol.iterator]() 方法,该方法返回一个迭代器对象,迭代器对象可以是拥有 .next 方法的任何对象;然后,在 for-of 的每次循环中,都将调用该迭代器对象上的 .next 方法。下面是一个最简单的迭代器对象:
var zeroesForeverIterator = { [Symbol.iterator]: function () { return this; }, next: function () { return {done: false, value: 0}; } }; var zeroesForeverIterator = { [Symbol.iterator]: function () { return this; }, next: function () { return {done: false, value: 0}; } };
在上面代码中,每次调用 .next() 方法时都返回了同一个结果,该结果一方面告知 for-of语句循环遍历还没有结束,另一方面告知 for-of 语句本次循环的值为 0。这意味着 for (value of zeroesForeverIterator) {} 是一个死循环。当然,一个典型的迭代器不会如此简单。
ES6 的迭代器通过 .done 和 .value 这两个属性来标识每次的遍历结果,这就是迭代器的设计原理,这与其他语言中的迭代器有所不同。在 Java 中,迭代器对象要分别使用 .hasNext()和 .next() 两个方法。在 Python 中,迭代器对象只有一个 .next() 方法,当没有可遍历的元素时将抛出一个 StopIteration 异常。但从根本上说,这三种设计都返回了相同的信息。
迭代器对象可以还可以选择性地实现 .return() 和 .throw(exc) 这两个方法。如果由于异常或使用 break 和 return 操作符导致循环提早退出,那么迭代器的 .return() 方法将被调用,可以通过实现 .return() 方法来释放迭代器对象所占用的资源,但大多数迭代器都不需要实现这个方法。throw(exc) 更是一个特例:在遍历过程中该方法永远都不会被调用,关于这个方法,我会在下一篇文章详细介绍。
现在我们知道了 for-of 的所有细节,那么我们可以简单地重写该语句。
首先是 for-of 循环体:
for (VAR of ITERABLE) { STATEMENTS } for (VAR of ITERABLE) { STATEMENTS }
这只是一个语义化的实现,使用了一些底层方法和几个临时变量:
var $iterator = ITERABLE[Symbol.iterator](); var $result = $iterator.next(); while (!$result.done) { VAR = $result.value; STATEMENTS $result = $iterator.next(); } var $iterator = ITERABLE[Symbol.iterator](); var $result = $iterator.next(); while (!$result.done) { VAR = $result.value; STATEMENTS $result = $iterator.next(); }
上面代码并没有涉及到如何调用 .return() 方法,我们可以添加相应的处理,但我认为这样会影响我们对内部原理的理解。for-of 语句使用起来非常简单,但在其内部有非常多的细节。
兼容性
目前,所有 Firefox 的 Release 版本都已经支持 for-of 语句。Chrome 默认禁用了该语句,你可以在地址栏输入 chrome://flags 进入设置页面,然后勾选其中的 “Experimental JavaScript” 选项。微软的 Spartan 浏览器也支持该语句,但是 IE 不支持。如果你想在 Web 开发中使用该语句,而且需要兼容 IE 和 Safari 浏览器,你可以使用 Babel 或 Google 的 Traceur 这类编译器,来将 ES6 代码转换为 Web 友好的 ES5 代码。
对于服务器端,我们不需要任何编译器 — 可以在 io.js 中直接使用该语句,或者在 NodeJS 启动时使用 --harmony 启动选项。
{done: true}