Maison  >  Article  >  interface Web  >  Apprenez les boucles javascript avec les compétences me_javascript

Apprenez les boucles javascript avec les compétences me_javascript

WBOY
WBOYoriginal
2016-05-16 15:31:341089parcourir

1. Utilisez des tableaux au lieu de types d'objets pour représenter des collections ordonnées

La norme ECMAScript ne spécifie pas l'ordre de stockage des propriétés dans le type Objet de JavaScript.

Mais lorsque vous utilisez une boucle for..in pour parcourir les propriétés dans Object, vous devez vous fier à un certain ordre. Précisément parce qu'ECMAScript ne standardise pas explicitement cette séquence, chaque moteur d'exécution JavaScript peut être implémenté selon ses propres caractéristiques, de sorte que la cohérence comportementale de la boucle for..in ne peut pas être garantie dans différents environnements d'exécution.

Par exemple, le résultat du code suivant lors de l'appel de la méthode report est incertain :

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 }]); 
// ? 

Si vous avez vraiment besoin de vous assurer que les résultats d'exécution sont basés sur l'ordre des données, donnez la priorité à l'utilisation du type tableau pour représenter les données au lieu d'utiliser directement le type Objet. En même temps, essayez d'éviter d'utiliser des boucles for..in et d'utiliser des boucles for explicites :

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" 

Un autre comportement particulièrement dépendant de l'ordre est le calcul des nombres à virgule flottante :

var ratings = { 
  "Good Will Hunting": 0.8, 
  "Mystic River": 0.7, 
  "21": 0.6, 
  "Doubt": 0.9 
}; 

Au point 2, il a été mentionné que l'opération d'addition de nombres à virgule flottante ne peut même pas satisfaire la loi commutative :
Les résultats de (0,1 0,2) 0,3 et 0,1 (0,2 0,3) sont respectivement
0,600000000000001 et 0,6

Donc, pour les opérations arithmétiques sur les nombres à virgule flottante, un ordre arbitraire ne peut pas être utilisé :

var total = 0, count = 0; 
for (var key in ratings) { // unpredictable order 
  total += ratings[key]; 
  count++; 
} 
total /= count; 
total; // &#63; 

Lorsque l'ordre de parcours de for..in est différent, le résultat total final obtenu est également différent. Voici les deux ordres de calcul et leurs résultats correspondants :

(0.8 + 0.7 + 0.6 +0.9) / 4 // 0.75
(0.6 + 0.8 + 0.7 +0.9) / 4 // 0.7499999999999999

Bien sûr, pour des problèmes tels que le calcul de nombres à virgule flottante, une solution consiste à utiliser des nombres entiers pour les représenter. Par exemple, nous agrandissons d'abord le nombre à virgule flottante ci-dessus 10 fois en données entières, puis le réduisons après. le calcul est effectué 10 fois :

(8+ 7 + 6 + 9) / 4 / 10  // 0.75
(6+ 8 + 7 + 9) / 4 / 10  // 0.75

2. N'ajoutez jamais d'attributs énumérables à Object.prototype

Si votre code s'appuie sur des boucles for..in pour parcourir les propriétés du type Object, n'ajoutez aucune propriété énumérable à Object.prototype.

Cependant, lors de l'amélioration de l'environnement d'exécution JavaScript, il est souvent nécessaire d'ajouter de nouvelles propriétés ou méthodes à l'objet Object.prototype. Par exemple, vous pouvez ajouter une méthode pour obtenir tous les noms d'attributs dans un objet :

Object.prototype.allKeys = function() { 
  var result = []; 
  for (var key in this) { 
    result.push(key); 
  } 
  return result; 
}; 

Mais le résultat est comme ceci :

({ a: 1, b: 2, c: 3}).allKeys(); // ["allKeys", "a", "b","c"]

Une solution possible consiste à utiliser des fonctions au lieu de définir de nouvelles méthodes sur Object.prototype :

function allKeys(obj) { 
  var result = []; 
  for (var key in obj) { 
    result.push(key); 
  } 
  return result; 
} 

Mais si vous avez vraiment besoin d'ajouter une nouvelle propriété à Object.prototype et que vous ne souhaitez pas que la propriété soit parcourue dans la boucle for..in, vous pouvez utiliser la méthode Object.defineProject fournie par l'environnement 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 
}); 

L'élément clé du code ci-dessus est de définir l'attribut énumérable sur false. Dans ce cas, la propriété ne peut pas être parcourue dans la boucle for..in.

3. Pour le parcours de tableau, utilisez la boucle for au lieu de la boucle for..in

Bien que ce problème ait été mentionné dans l'article précédent, pour le code suivant, pouvez-vous voir quelle est la moyenne finale ?

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; 

Par calcul, le résultat final devrait être de 88.

Mais n'oubliez pas que dans la boucle for..in, ce qui est parcouru est toujours la clé, pas la valeur. Il en va de même pour les tableaux. Par conséquent, le score dans la boucle for..in ci-dessus n'est pas la série de valeurs attendue telle que 98, 74, etc., mais une série d'index tels que 0, 1, etc.

Vous pourriez donc penser que le résultat final est :
(0 1 … 6) / 7 = 21

Mais cette réponse est également fausse. Un autre point clé est que le type de clé dans la boucle for..in est toujours un type chaîne, donc l'opérateur ici effectue réellement l'opération d'épissage des chaînes :

Le total final obtenu est en fait la chaîne 00123456. La valeur de cette chaîne convertie en type numérique est 123456, puis divisée par le nombre d'éléments, 7, pour obtenir le résultat final : 17636.571428571428

Donc, pour le parcours de tableaux, il est préférable d'utiliser la norme for loop

4. Privilégier l'utilisation de méthodes de parcours plutôt que de boucles

Lors de l'utilisation de boucles, il est facile de violer le principe DRY (Don't Repeat Yourself). En effet, nous choisissons généralement la méthode copier-coller pour éviter d’écrire à la main des paragraphes de déclarations circulaires. Mais cela entraînera de nombreuses duplications de code dans le code, et les développeurs « réinventeront la roue » sans aucun sens. Plus important encore, il est facile de négliger les détails de la boucle lors du copier-coller, tels que la valeur de l'index de départ, la condition de fin, etc.

Par exemple, la boucle for suivante a ce problème, en supposant que n est la longueur de l'objet de collection :

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循环的原理,大家共同进步。

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn