1. 순서가 지정된 컬렉션을 나타내려면 객체 유형 대신 배열을 사용하세요.
ECMAScript 표준은 JavaScript의 객체 유형에서 속성의 저장 순서를 지정하지 않습니다.
그러나 for..in 루프를 사용하여 Object의 속성을 순회하는 경우 특정 순서에 의존해야 합니다. 정확하게는 ECMAScript가 이 시퀀스를 명시적으로 표준화하지 않기 때문에 각 JavaScript 실행 엔진은 고유한 특성에 따라 구현될 수 있으므로 for..in 루프의 동작 일관성은 다양한 실행 환경에서 보장될 수 없습니다.
예를 들어, 보고 메소드를 호출할 때 다음 코드의 결과는 불확실합니다.
function report(highScores) { var result = ""; var i = 1; for (var name in highScores) { // unpredictable order result += i + ". " + name + ": " + highScores[name] + "\n"; i++; } return result; } report([{ name: "Hank", points: 1110100 }, { name: "Steve", points: 1064500 }, { name: "Billy", points: 1050200 }]); // ?
실행 결과가 데이터 순서를 기반으로 하는지 확인해야 하는 경우에는 Object 유형을 직접 사용하는 대신 배열 유형을 사용하여 데이터를 표현하는 데 우선순위를 두세요. 동시에 for..in 루프를 사용하지 말고 명시적인 for 루프를 사용하십시오.
function report(highScores) { var result = ""; for (var i = 0, n = highScores.length; i < n; i++) { var score = highScores[i]; result += (i + 1) + ". " + score.name + ": " + score.points + "\n"; } return result; } report([{ name: "Hank", points: 1110100 }, { name: "Steve", points: 1064500 }, { name: "Billy", points: 1050200 }]); // "1. Hank: 1110100 2. Steve: 1064500 3. Billy: 1050200\n"
특히 순서에 따라 달라지는 또 다른 동작은 부동 소수점 숫자 계산입니다.
var ratings = { "Good Will Hunting": 0.8, "Mystic River": 0.7, "21": 0.6, "Doubt": 0.9 };
항목 2에서는 부동 소수점 수의 덧셈 연산이 교환법칙도 만족할 수 없다고 언급했습니다.
(0.1 0.2) 0.3과 0.1 (0.2 0.3)의 결과는 각각
0.600000000000001 및 0.6
따라서 부동 소수점 숫자에 대한 산술 연산의 경우 임의 순서를 사용할 수 없습니다.
var total = 0, count = 0; for (var key in ratings) { // unpredictable order total += ratings[key]; count++; } total /= count; total; // ?
for..in의 순회 순서가 다르면 얻어지는 최종 총 결과도 다릅니다. 다음은 두 가지 계산 순서와 해당 결과입니다.
(0.8 + 0.7 + 0.6 +0.9) / 4 // 0.75 (0.6 + 0.8 + 0.7 +0.9) / 4 // 0.7499999999999999
물론, 부동 소수점 수 계산과 같은 문제의 경우 한 가지 해결책은 정수를 사용하여 이를 표현하는 것입니다. 예를 들어 먼저 위의 부동 소수점 수를 정수 데이터로 10배 확대한 다음 축소합니다. 10번 계산이 완료되었습니다:
(8+ 7 + 6 + 9) / 4 / 10 // 0.75 (6+ 8 + 7 + 9) / 4 / 10 // 0.75
2. Object.prototype에 열거 가능한 속성을 추가하지 마세요
코드가 for..in 루프를 사용하여 Object 유형의 속성을 반복하는 경우 Object.prototype에 열거 가능한 속성을 추가하지 마세요.
그러나 JavaScript 실행 환경을 강화할 때 Object.prototype 객체에 새로운 속성이나 메소드를 추가해야 하는 경우가 많습니다. 예를 들어, 객체의 모든 속성 이름을 가져오는 메소드를 추가할 수 있습니다.
Object.prototype.allKeys = function() { var result = []; for (var key in this) { result.push(key); } return result; };
그런데 결과는 이렇습니다.
({ a: 1, b: 2, c: 3}).allKeys(); // ["allKeys", "a", "b","c"]
가능한 해결책은 Object.prototype에 새 메서드를 정의하는 대신 함수를 사용하는 것입니다.
function allKeys(obj) { var result = []; for (var key in obj) { result.push(key); } return result; }
그러나 Object.prototype에 새 속성을 추가해야 하고 해당 속성이 for..in 루프에서 탐색되는 것을 원하지 않는 경우 ES5 환경에서 제공하는 Object.defineProject 메서드를 사용할 수 있습니다.
Object.defineProperty(Object.prototype, "allKeys", { value: function() { var result = []; for (var key in this) { result.push(key); } return result; }, writable: true, enumerable: false, configurable: true });
위 코드의 핵심 부분은 열거 가능 속성을 false로 설정하는 것입니다. 이 경우 for..in 루프에서 속성을 순회할 수 없습니다.
3. 배열 순회에는 for..in 루프 대신 for 루프를 사용하세요
이 문제는 이전 항목에서도 언급되었지만, 다음 코드의 경우 최종 평균이 얼마인지 알 수 있나요?
var scores = [98, 74, 85, 77, 93, 100, 89]; var total = 0; for (var score in scores) { total += score; } var mean = total / scores.length; mean; // ?
계산하면 최종 결과는 88이 되어야 합니다.
그러나 for..in 루프에서 탐색되는 것은 항상 값이 아니라 키라는 점을 잊지 마십시오. 배열의 경우에도 마찬가지입니다. 따라서 위 for..in 루프의 점수는 98, 74 등과 같이 예상되는 일련의 값이 아니라 0, 1 등과 같은 일련의 인덱스입니다.
최종 결과는 다음과 같습니다.
(0 1 … 6) / 7 = 21
하지만 이 대답도 틀렸습니다. 또 다른 중요한 점은 for..in 루프의 키 유형이 항상 문자열 유형이므로 여기서 연산자는 실제로 문자열의 접합 작업을 수행한다는 것입니다.
최종적으로 얻은 합계는 실제로 문자열 00123456입니다. 숫자 유형으로 변환된 이 문자열의 값은 123456이고, 그런 다음 요소 수 7로 나누어 최종 결과를 얻습니다: 17636.571428571428
따라서 배열 순회에는 표준 for 루프를 사용하는 것이 가장 좋습니다
4. 루프보다는 순회 방법을 우선적으로 사용합니다
루프를 사용하면 DRY(Don't Repeat Yourself) 원칙을 위반하기 쉽습니다. 순환문의 단락을 손으로 쓰는 것을 피하기 위해 일반적으로 복사-붙여넣기 방법을 선택하기 때문입니다. 그러나 그렇게 하면 코드에 중복된 코드가 많이 생기고 개발자는 무의미하게 "바퀴를 재발명"하게 됩니다. 더 중요한 것은 복사하여 붙여넣을 때 시작 인덱스 값, 종료 조건 등과 같은 루프의 세부 사항을 간과하기 쉽다는 것입니다.
예를 들어, n이 컬렉션 개체의 길이라고 가정할 때 다음 for 루프에는 이 문제가 있습니다.
for (var i = 0; i <= n; i++) { ... } // 终止条件错误,应该是i < n for (var i = 1; i < n; i++) { ... } // 起始变量错误,应该是i = 0 for (var i = n; i >= 0; i--) { ... } // 起始变量错误,应该是i = n - 1 for (var i = n - 1; i > 0; i--) { ... } // 终止条件错误,应该是i >= 0
可见在循环的一些细节处理上很容易出错。而利用JavaScript提供的闭包(参见Item 11),可以将循环的细节给封装起来供重用。实际上,ES5就提供了一些方法来处理这一问题。其中的Array.prototype.forEach是最简单的一个。利用它,我们可以将循环这样写:
// 使用for循环 for (var i = 0, n = players.length; i < n; i++) { players[i].score++; } // 使用forEach players.forEach(function(p) { p.score++; });
除了对集合对象进行遍历之外,另一种常见的模式是对原集合中的每个元素进行某种操作,然后得到一个新的集合,我们也可以利用forEach方法实现如下:
// 使用for循环 var trimmed = []; for (var i = 0, n = input.length; i < n; i++) { trimmed.push(input[i].trim()); } // 使用forEach var trimmed = []; input.forEach(function(s) { trimmed.push(s.trim()); });
但是由于这种由将一个集合转换为另一个集合的模式十分常见,ES5也提供了Array.prototype.map方法用来让代码更加简单和优雅:
var trimmed = input.map(function(s) { return s.trim(); });
另外,还有一种常见模式是对集合根据某种条件进行过滤,然后得到一个原集合的子集。ES5中提供了Array.prototype.filter来实现这一模式。该方法接受一个Predicate作为参数,它是一个返回true或者false的函数:返回true意味着该元素会被保留在新的集合中;返回false则意味着该元素不会出现在新集合中。比如,我们使用以下代码来对商品的价格进行过滤,仅保留价格在[min, max]区间的商品:
listings.filter(function(listing) { return listing.price >= min && listing.price <= max; });
当然,以上的方法是在支持ES5的环境中可用的。在其它环境中,我们有两种选择: 1. 使用第三方库,如underscore或者lodash,它们都提供了相当多的通用方法来操作对象和集合。 2. 根据需要自行定义。
比如,定义如下的方法来根据某个条件取得集合中前面的若干元素:
function takeWhile(a, pred) { var result = []; for (var i = 0, n = a.length; i < n; i++) { if (!pred(a[i], i)) { break; } result[i] = a[i]; } return result; } var prefix = takeWhile([1, 2, 4, 8, 16, 32], function(n) { return n < 10; }); // [1, 2, 4, 8]
为了更好的重用该方法,我们可以将它定义在Array.prototype对象上,具体的影响可以参考Item 42。
Array.prototype.takeWhile = function(pred) { var result = []; for (var i = 0, n = this.length; i < n; i++) { if (!pred(this[i], i)) { break; } result[i] = this[i]; } return result; }; var prefix = [1, 2, 4, 8, 16, 32].takeWhile(function(n) { return n < 10; }); // [1, 2, 4, 8]
只有一个场合使用循环会比使用遍历函数要好:需要使用break和continue的时候。 比如,当使用forEach来实现上面的takeWhile方法时就会有问题,在不满足predicate的时候应该如何实现呢?
function takeWhile(a, pred) { var result = []; a.forEach(function(x, i) { if (!pred(x)) { // ? } result[i] = x; }); return result; }
我们可以使用一个内部的异常来进行判断,但是它同样有些笨拙和低效:
function takeWhile(a, pred) { var result = []; var earlyExit = {}; // unique value signaling loop break try { a.forEach(function(x, i) { if (!pred(x)) { throw earlyExit; } result[i] = x; }); } catch (e) { if (e !== earlyExit) { // only catch earlyExit throw e; } } return result; }
可是使用forEach之后,代码甚至比使用它之前更加冗长。这显然是存在问题的。 对于这个问题,ES5提供了some和every方法用来处理存在提前终止的循环,它们的用法如下所示:
[1, 10, 100].some(function(x) { return x > 5; }); // true [1, 10, 100].some(function(x) { return x < 0; }); // false [1, 2, 3, 4, 5].every(function(x) { return x > 0; }); // true [1, 2, 3, 4, 5].every(function(x) { return x < 3; }); // false
这两个方法都是短路方法(Short-circuiting):只要有任何一个元素在some方法的predicate中返回true,那么some就会返回;只有有任何一个元素在every方法的predicate中返回false,那么every方法也会返回false。
因此,takeWhile就可以实现如下:
function takeWhile(a, pred) { var result = []; a.every(function(x, i) { if (!pred(x)) { return false; // break } result[i] = x; return true; // continue }); return result; }
实际上,这就是函数式编程的思想。在函数式编程中,你很少能够看见显式的for循环或者while循环。循环的细节都被很好地封装起来了。
5、总结
以上就是本文的全部内容,希望通过这篇文章大家更加了解javascript循环的原理,大家共同进步。