ホームページ >ウェブフロントエンド >jsチュートリアル >me_javascript スキルで JavaScript ループを学習する
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 }]); // ?
実行結果がデータの順序に基づいていることを本当に確認する必要がある場合は、オブジェクト型を直接使用するのではなく、配列型を使用してデータを表すことを優先してください。同時に、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"
特に順序に依存するもう 1 つの動作は、浮動小数点数の計算です。
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 の走査順序が異なると、得られる最終的な合計結果も異なります。次に、2 つの計算順序とそれに対応する結果を示します。
(0.8 + 0.7 + 0.6 +0.9) / 4 // 0.75 (0.6 + 0.8 + 0.7 +0.9) / 4 // 0.7499999999999999
(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"]
function allKeys(obj) { var result = []; for (var key in obj) { result.push(key); } return result; }
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 });
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; // ?
ただし、for..in ループでは、値ではなく常にキーが扱われることを忘れないでください。配列についても同様です。したがって、上記の for..in ループのスコアは、98、74 などの予期される一連の値ではなく、0、1 などの一連のインデックスです。
最終結果は次のようになると思われるかもしれません:
(0 1 … 6) / 7 = 21
取得された最終的な合計は、実際には文字列 00123456 です。数値型に変換されたこの文字列の値は 123456 で、要素数 7 で割ると、最終結果 17636.571428571428
が得られます。
したがって、配列の走査には、標準の for ループを使用するのが最善です
4. ループではなくトラバーサル メソッドの使用を優先します
ループを使用する場合、DRY (Don'trepeat 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循环的原理,大家共同进步。