1. Use arrays instead of Object types to represent ordered collections
The ECMAScript standard does not specify the storage order of properties in JavaScript’s Object type.
But when using a for..in loop to traverse the properties in Object, you do need to rely on a certain order. Precisely because ECMAScript does not explicitly standardize this sequence, each JavaScript execution engine can be implemented according to its own characteristics, so the behavioral consistency of the for..in loop cannot be guaranteed in different execution environments.
For example, the result of the following code when calling the report method is uncertain:
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 }]); // ?
If you really need to ensure that the running results are based on the order of the data, give priority to using the array type to represent the data instead of directly using the Object type. At the same time, try to avoid using for..in loops and use explicit for loops:
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"
Another behavior that is particularly order-dependent is the calculation of floating point numbers:
var ratings = { "Good Will Hunting": 0.8, "Mystic River": 0.7, "21": 0.6, "Doubt": 0.9 };
In Item 2, it was mentioned that the addition operation of floating point numbers cannot even satisfy the commutative law:
The results of (0.1 0.2) 0.3 and 0.1 (0.2 0.3) are respectively
0.600000000000001 and 0.6
So for arithmetic operations on floating point numbers, arbitrary order cannot be used:
var total = 0, count = 0; for (var key in ratings) { // unpredictable order total += ratings[key]; count++; } total /= count; total; // ?
When the traversal order of for..in is different, the final total result obtained is also different. The following are the two calculation orders and their corresponding results:
(0.8 + 0.7 + 0.6 +0.9) / 4 // 0.75 (0.6 + 0.8 + 0.7 +0.9) / 4 // 0.7499999999999999
Of course, for problems such as the calculation of floating point numbers, one solution is to use integers to represent them. For example, we first enlarge the above floating point number 10 times into integer data, and then reduce it after the calculation is completed. 10 times:
(8+ 7 + 6 + 9) / 4 / 10 // 0.75 (6+ 8 + 7 + 9) / 4 / 10 // 0.75
2. Never add enumerable attributes to Object.prototype
If your code relies on for..in loops to iterate over properties in the Object type, do not add any enumerable properties to Object.prototype.
However, when enhancing the JavaScript execution environment, it is often necessary to add new properties or methods to the Object.prototype object. For example, you can add a method to get all the attribute names in an object:
Object.prototype.allKeys = function() { var result = []; for (var key in this) { result.push(key); } return result; };
But the result is like this:
({ a: 1, b: 2, c: 3}).allKeys(); // ["allKeys", "a", "b","c"]
A possible solution is to use functions instead of defining new methods on Object.prototype:
function allKeys(obj) { var result = []; for (var key in obj) { result.push(key); } return result; }
But if you really need to add a new property to Object.prototype and do not want the property to be traversed in the for..in loop, you can use the Object.defineProject method provided by the ES5 environment:
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 });
The key part of the above code is to set the enumerable attribute to false. In this case, the property cannot be traversed in the for..in loop.
3. For array traversal, use for loop instead of for..in loop
Although this issue has been mentioned in the previous Item, for the following code, can you see what the final average is?
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; // ?
By calculation, the final result should be 88.
But don’t forget that in the for..in loop, what is traversed is always the key, not the value. The same is true for arrays. Therefore, the score in the for..in loop above is not the expected series of values such as 98, 74, etc., but a series of indexes such as 0, 1, etc.
So you might think the final result is:
(0 1 … 6) / 7 = 21
But this answer is also wrong. Another key point is that the key type in the for..in loop is always a string type, so the operator here actually performs the splicing operation of strings:
The final total obtained is actually the string 00123456. The value of this string converted to a numeric type is 123456, and then divided by the number of elements, 7, to get the final result: 17636.571428571428
So, for array traversal, it is best to use the standard for loop
4. Prioritize the use of traversal methods rather than loops
When using loops, it is easy to violate the DRY (Don't Repeat Yourself) principle. This is because we usually choose the copy-paste method to avoid hand-writing paragraphs of circular statements. But doing so will result in a lot of duplicate code in the code, and developers will "reinvent the wheel" meaninglessly. More importantly, it is easy to overlook the details in the loop when copying and pasting, such as the starting index value, termination condition, etc.
For example, the following for loop has this problem, assuming n is the length of the collection object:
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循环的原理,大家共同进步。