ホームページ >ウェブフロントエンド >jsチュートリアル >me_javascript スキルで JavaScript ループを学習する

me_javascript スキルで JavaScript ループを学習する

WBOY
WBOYオリジナル
2016-05-16 15:31:341111ブラウズ

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; // &#63; 

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
もちろん、浮動小数点数の計算などの問題の場合、解決策の 1 つは、整数を使用して表現することです。たとえば、最初に上記の浮動小数点数を 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; // &#63; 
計算すると、最終結果は 88 になるはずです。

ただし、for..in ループでは、値ではなく常にキーが扱われることを忘れないでください。配列についても同様です。したがって、上記の for..in ループのスコアは、98、74 などの予期される一連の値ではなく、0、1 などの一連のインデックスです。

最終結果は次のようになると思われるかもしれません:

(0 1 … 6) / 7 = 21

しかし、この答えも間違っています。もう 1 つの重要な点は、for..in ループ内のキーの型は常に文字列型であるため、ここでの演算子は実際に文字列の結合操作を実行することです。

取得された最終的な合計は、実際には文字列 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)) {
      // &#63;
    }
    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、总结

  • 在使用for..in循环时,不要依赖于遍历的顺序。
  • 当使用Object类型来保存数据时,需要保证其中的数据是无序的。
  • 当需要表示带有顺序的集合时,使用数组类型而不是Object类型。
  • 避免向Object.prototype中添加任何属性。
  • 如果确实有必要向Object.prototype中添加方法属性,可以考虑使用独立函数替代。
  • 使用Object.defineProperty来添加可以不被for..in循环遍历到的属性。
  • 当遍历数组时,使用标准的for循环,而不要使用for..in循环。
  • 在必要的场合考虑预先保存数组的长度,以提高性能。
  • 使用遍历方法Array.prototype.forEach和Array.prototype.map来代替循环,从而让代码更加清晰可读。
  • 对于重复出现的循环,可以考虑将它们进行抽象。通过第三方提供的方法或者自己实现。
  • 显式的循环在一些场合下还是有用武之地的,相应的也可以使用some或者every方法。

以上就是本文的全部内容,希望通过这篇文章大家更加了解javascript循环的原理,大家共同进步。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。