1. Gunakan tatasusunan dan bukannya jenis Objek untuk mewakili koleksi tersusun
Piawaian ECMAScript tidak menyatakan susunan storan sifat dalam jenis Objek JavaScript.
Tetapi apabila menggunakan gelung for..in untuk melintasi sifat dalam Object, anda perlu bergantung pada susunan tertentu. Tepat kerana ECMAScript tidak menyeragamkan jujukan ini secara eksplisit, setiap enjin pelaksanaan JavaScript boleh dilaksanakan mengikut ciri-cirinya sendiri, jadi ketekalan tingkah laku gelung for..in tidak boleh dijamin dalam persekitaran pelaksanaan yang berbeza.
Sebagai contoh, hasil kod berikut semasa memanggil kaedah laporan tidak pasti:
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 }]); // ?
Jika anda benar-benar perlu memastikan bahawa hasil yang dijalankan adalah berdasarkan susunan data, beri keutamaan untuk menggunakan jenis tatasusunan untuk mewakili data dan bukannya menggunakan jenis Objek secara langsung. Pada masa yang sama, cuba elakkan menggunakan for..in loops dan gunakan eksplisit 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"
Satu lagi tingkah laku yang sangat bergantung pada susunan ialah pengiraan nombor titik terapung:
var ratings = { "Good Will Hunting": 0.8, "Mystic River": 0.7, "21": 0.6, "Doubt": 0.9 };
Dalam Item 2, disebutkan bahawa operasi penambahan nombor titik terapung tidak dapat memenuhi undang-undang komutatif:
Keputusan (0.1 0.2) 0.3 dan 0.1 (0.2 0.3) masing-masing adalah
0.600000000000001 dan 0.6
Jadi untuk operasi aritmetik pada nombor titik terapung, susunan sewenang-wenangnya tidak boleh digunakan:
var total = 0, count = 0; for (var key in ratings) { // unpredictable order total += ratings[key]; count++; } total /= count; total; // ?
Apabila susunan traversal for..in berbeza, jumlah hasil akhir yang diperoleh juga berbeza Berikut adalah dua susunan pengiraan dan hasil yang sepadan:
(0.8 + 0.7 + 0.6 +0.9) / 4 // 0.75 (0.6 + 0.8 + 0.7 +0.9) / 4 // 0.7499999999999999
Sudah tentu, untuk masalah seperti pengiraan nombor titik terapung, satu penyelesaian adalah dengan menggunakan integer untuk mewakilinya Sebagai contoh, kami mula-mula membesarkan nombor titik terapung di atas 10 kali menjadi data integer, dan kemudian mengurangkannya selepas itu. pengiraan selesai 10 kali:
(8+ 7 + 6 + 9) / 4 / 10 // 0.75 (6+ 8 + 7 + 9) / 4 / 10 // 0.75
2. Jangan sekali-kali menambah atribut yang boleh dikira pada Object.prototype
Jika kod anda bergantung pada untuk..dalam gelung untuk mengulang sifat dalam jenis Objek, jangan tambahkan sebarang sifat yang boleh dikira pada Object.prototype.
Walau bagaimanapun, apabila mempertingkatkan persekitaran pelaksanaan JavaScript, selalunya perlu menambah sifat atau kaedah baharu pada objek Object.prototype. Sebagai contoh, anda boleh menambah kaedah untuk mendapatkan semua nama atribut dalam objek:
Object.prototype.allKeys = function() { var result = []; for (var key in this) { result.push(key); } return result; };
Tetapi hasilnya seperti ini:
({ a: 1, b: 2, c: 3}).allKeys(); // ["allKeys", "a", "b","c"]
Penyelesaian yang mungkin adalah menggunakan fungsi dan bukannya menentukan kaedah baharu pada Object.prototype:
function allKeys(obj) { var result = []; for (var key in obj) { result.push(key); } return result; }
Tetapi jika anda benar-benar perlu menambah harta baharu pada Object.prototype dan tidak mahu harta itu dilalui dalam gelung for..in, anda boleh menggunakan kaedah Object.defineProject yang disediakan oleh persekitaran ES5:
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 });
Bahagian utama kod di atas adalah untuk menetapkan atribut yang boleh dihitung kepada palsu. Dalam kes ini, harta itu tidak boleh dilalui dalam gelung for..in.
3. Untuk traversal tatasusunan, gunakan untuk gelung dan bukannya untuk..dalam gelung
Walaupun isu ini telah disebut dalam Item sebelumnya, untuk kod berikut, bolehkah anda melihat apakah purata akhir?
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; // ?
Mengikut pengiraan, keputusan akhir hendaklah 88.
Tetapi jangan lupa bahawa dalam gelung for..in, apa yang dilalui sentiasa menjadi kunci, bukan nilai Perkara yang sama berlaku untuk tatasusunan. Oleh itu, skor dalam gelung for..in di atas bukanlah siri nilai yang dijangkakan seperti 98, 74, dsb., tetapi satu siri indeks seperti 0, 1, dsb.
Jadi anda mungkin fikir keputusan akhir ialah:
(0 1 … 6) / 7 = 21
Tetapi jawapan ini juga salah. Satu lagi perkara penting ialah jenis kunci dalam gelung for..in sentiasa jenis rentetan, jadi pengendali di sini sebenarnya melakukan operasi penyambungan rentetan:
Jumlah akhir yang diperoleh sebenarnya rentetan 00123456. Nilai rentetan ini ditukar kepada jenis angka ialah 123456, dan kemudian dibahagikan dengan bilangan elemen, 7, untuk mendapatkan hasil akhir: 17636.571428571428
Jadi, untuk traversal tatasusunan, sebaiknya gunakan standard untuk gelung
4 Utamakan penggunaan kaedah traversal daripada gelung
Apabila menggunakan gelung, mudah melanggar prinsip KERING (Jangan Ulangi Sendiri). Ini kerana kita biasanya memilih kaedah salin-tampal untuk mengelakkan perenggan penyataan pekeliling tulisan tangan. Tetapi berbuat demikian akan menghasilkan banyak kod pendua dalam kod dan pembangun akan "mencipta semula roda" tanpa makna. Lebih penting lagi, adalah mudah untuk mengabaikan butiran dalam gelung semasa menyalin dan menampal, seperti nilai indeks permulaan, syarat penamatan, dsb.
Sebagai contoh, gelung berikut mempunyai masalah ini, dengan mengandaikan n ialah panjang objek koleksi:
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循环的原理,大家共同进步。