跟我学习javascript的循环

1、优先使用数组而不是Object类型来表示有顺序的集合

ECMAScript标准并没有规定对JavaScript的Object类型中的属性的存储顺序。

但是在使用for..in循环对Object中的属性进行遍历的时候,确实是需要依赖于某种顺序的。正因为ECMAScript没有对这个顺序进行明确地规范,所以每个JavaScript执行引擎都能够根据自身的特点进行实现,那么在不同的执行环境中就不能保证for..in循环的行为一致性了。

比如,以下代码在调用report方法时的结果就是不确定的:

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

如果你确实需要保证运行的结果是建立在数据的顺序上,优先使用数组类型来表示数据,而不是直接使用Object类型。同时,也尽量避免使用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"

另一个特别依赖于顺序的行为是浮点数的计算:

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

在Item 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的遍历顺序不一样时,最后得到的total结果也就不一样了,以下是两种计算顺序和其对应的结果:

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

当然,对于浮点数的计算这一类问题,有一个解决方案是使用整型数来表示,比如我们将上面的浮点数首先放大10倍变成整型数据,然后计算结束之后再缩小10倍:

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

2、绝不要向Object.prototype中添加可列举的(Enumerable)属性

如果你的代码中依赖于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
});

以上代码的关键部分就是将enumerable属性设置为false。这样的话,在for..in循环中就无法遍历该属性了。

3、对于数组遍历,优先使用for循环,而不是for..in循环

虽然上个Item已经说过这个问题,但是对于下面这段代码,能看出最后的平均数是多少吗?

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; // ?

通过计算,最后的结果应该是88。

但是不要忘了在for..in循环中,被遍历的永远是key,而不是value,对于数组同样如此。因此上述for..in循环中的score并不是期望的98, 74等一系列值,而是0, 1等一系列索引。

所以你也许会认为最后的结果是:
(0 + 1+ …+ 6) / 7 = 21

但是这个答案也是错的。另外一个关键点在于,for..in循环中key的类型永远都是字符串类型,因此这里的+操作符执行的实际上是字符串的拼接操作:

最后得到的total实际上是字符串00123456。这个字符串转换成数值类型后的值是123456,然后再将它除以元素的个数7,就得到了最后的结果:17636.571428571428

所以,对于数组遍历,还是使用标准的for循环最好

4、优先使用遍历方法而非循环

在使用循环的时候,很容易违反DRY(Don't Repeat Yourself)原则。这是因为我们通常会选择复制粘贴的方法来避免手写一段段的循环语句。但是这样做回让代码中出现大量重复代码,开发人员也在没有意义地”重复造轮子”。更重要的是,在复制粘贴的时候很容易忽视循环中的那些细节,比如起始索引值,终止判断条件等。

比如以下的for循环就存在这个问题,假设n是集合对象的长度:

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、总结

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

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

(0)

相关推荐

  • JavaScript中对循环语句的优化技巧深入探讨

    循环是所有编程语言中最为重要的机制之一,几乎任何拥有实际意义的计算机程序(排序.查询等)都里不开循环. 而循环也正是程序优化中非常让人头疼的一环,我们往往需要不断去优化程序的复杂度,却因循环而纠结在时间复杂度和空间复杂度之间的抉择. 在 javascript 中,有3种原生循环,for () {}, while () {}和do {} while (),其中最为常用的要数for () {}. 然而for正是 javascript 工程师们在优化程序时最容易忽略的一种循环. 我们先来回顾一下for

  • javascript 循环读取JSON数据的代码

    服务端后台返回到客户端的JSON格式字符串: var str = '[{"uname":"王强","day":"2010/06/17"},{"uname":"王海云","day":"2010/06/11"}]'; 我们把它转换成JSON对象:var jsonList=eval("("+str+")");这时候

  • javascript下for循环用法小结

    我们最常用的一种是 for(循环变量初值;循环条件;递增值){语句;}  例 for(var x=0;x [Ctrl+A 全选 注:如需引入外部Js需刷新才能执行] 下面说下其它的用法  (1)省略表达式1,这个时候应在for语句前给循环变量赋初值,注意其后的;不能省略  例: var x=0; for(;x [Ctrl+A 全选 注:如需引入外部Js需刷新才能执行] (2) 省略表达式2,也就是循环条件循环无终止进行,也就是表达式2始终为真.  例: for(var x=0;;x++) { d

  • javascript中的循环语句for语句深入理解

    程序实现中经常要用到循环语句,其中for循环是多数语言都有的.在javascript中,for循环有几种不同的使用情况,下面就分别来讲述我的理解. 第一种:(通常情况,循环执行相关操作) 复制代码 代码如下: var objA=document.getElementsByTagName("a"); var i,max; for(i=0,max=objA.length;i<max;i++){ objA[i].onclick=function(){ alert(this.innerH

  • 跟我学习javascript的for循环和for...in循环

    大家都知道在JavaScript中提供了两种方式迭代对象: for 循环: for..in循环: 一.for循环 不足: 在于每次循环的时候数组的长度都要去获取: 终止条件要明确: 在for循环中,你可以循环取得数组或是数组类似对象的值,譬如arguments和HTMLCollection对象.通常的循环形式如下: // 次佳的循环 for (var i = 0; i < myarray.length; i++) { // 使用myarray[i]做点什么 } 这种形式的循环的不足在于每次循环的

  • JavaScript的递归之递归与循环示例介绍

    递归与循环 对于不同类型的需要重复计算的问题,循环和递归两种方法各有所长,能给出更直观简单的方案.另一方面,循环和递归的方法可以互相转换.任何一个循环的代码都可以用递归改写,实现相同的功能:反之亦然.在不失去其普遍性的前提下,可以把循环和递归分别用下列伪代码概括. 伪代码格式说明:循环采用while形式:变量不加定义:赋值用:=:条件表达式和执行的语句都写成函数的形式,圆括号内写上相关的值.其他语法方面,尽量接近Javascript的规范. 复制代码 代码如下: //pseudo code of

  • 简单学习JavaScript中的for语句循环结构

    可以直接看示例,用得太多了,很简单 (function() { for(var i=0, len=demoArr.length; i<len; i++) { if (i == 2) { // return; // 函数执行被终止 // break; // 循环被终止 continue; // 循环被跳过 }; console.log('demo1Arr['+ i +']:' + demo1Arr[i]); } })(); 关于for循环,有一下几点需要注意 for循环中的i在循环结束之后任然存在

  • javascript 循环语句 while、do-while、for-in、for用法区别

    前两个唯一的差别就是循环和判断的顺序不同,do-while比while多循环一次,我就不举例了. for循环相信大家也熟的不能再熟了,我们就看for-in这一句. 这个其实是针对数组的,js中数组的初始化也挺奇特的比如我们在script结点里写:(另外注意下数组的初始化,用的是中括号) "); var a=[3,4,5,7]; for(var test in a){ document.write(test+": "+a[test]+""); } -->

  • javascript之循环停顿上下滚动

    extractNodes函数解决各浏览器的节点计算问题.FireFox把换行符也算成一个节点,很不合理啊. 先用appendChild复制前面的四个节点到底部,使得头尾是一样的内容,以作后用.记得做flash补间循环运动的时候就是这样要头尾一样. 通过setInterval隔时运行rolltxt函数. rolltxt函数首先计算的是间隔停顿的高度,滚动多高就要停顿一下,这个高度我也不知道怎么算出来的,跟CSS有关系,我是试出来的   用求余数的方法可以判断是否已经滚过了所设的间隔高度.如果余数不

  • JavaScript中for..in循环陷阱介绍

    大家都知道在JavaScript中提供了两种方式迭代对象: (1)for 循环: (2)for..in循环: 使用for循环进行迭代数组对象,想必大家都已经司空见惯了.但是,使用for.. in循环时,大家可要注意了,为什么这么说呢?大家听我娓娓道来.... javascript提供了一种特殊的循环(也就是for .. in循环),用来迭代对象的属性或数组的每个元素,for...in循环中的循环计数器是字符串,而不是数字.它包含当前属性的名称或当前数组元素的索引. 案例一: 复制代码 代码如下:

  • javascript使用for循环批量注册的事件不能正确获取索引值的解决方法

    本文实例讲述了javascript使用for循环批量注册的事件不能正确获取索引值的解决方法.分享给大家供大家参考.具体分析如下: 可能不少朋友会遇到一个问题,那就是当使用for循环批量注册事件处理函数,然后最后通过事件处理函数获取当前元素的索引值的时候会失败,先看一段代码实例: 复制代码 代码如下: <!DOCTYPE html> <html> <head> <meta charset=" utf-8"> <meta name=&q

  • JavaScript中利用各种循环进行遍历的方式总结

    为了方便例子讲解,现有数组和json对象如下 var demoArr = ['Javascript', 'Gulp', 'CSS3', 'Grunt', 'jQuery', 'angular']; var demoObj = { aaa: 'Javascript', bbb: 'Gulp', ccc: 'CSS3', ddd: 'Grunt', eee: 'jQuery', fff: 'angular' }; for in for(var item in arr|obj){} 可以用于遍历数组和

  • javascript中利用数组实现的循环队列代码

    //循环队列 function CircleQueue(size){ this.initQueue(size); } CircleQueue.prototype = { //初始化队列 initQueue : function(size){ this.size = size; this.list = new Array(); this.capacity = size + 1; this.head = 0; this.tail = 0; }, //压入队列 enterQueue : functio

  • javascript forEach通用循环遍历方法

    复制代码 代码如下: var forEach = (function(){ //数组与伪数组的遍历 var _Array_forEach = function (array, block, context) { if (array == null) return; //对String进行特殊处理 if(typeof array == 'string'){ array = array.split(''); } var i = 0,length = array.length; for (;i < l

  • Javascript中的for in循环和hasOwnProperty结合使用

    与in操作符相比,for in 在循环对象的属性时也会遍历原型链,for in 不会读取不可枚举属性,如数组的length属性. 小结 当检测某个对象是否拥有某个属性时,hasOwnProperty 是唯一可以完成这一任务的方法,在 for in 循环时,建议增加 hasOwnProperty 进行判断,可以有效避免扩展本地原型而引起的错误. 与in操作符相比,for in 在循环对象的属性时也会遍历原型链,for in 不会读取不可枚举属性,如数组的length属性. 复制代码 代码如下: /

  • javascript闭包传参和事件的循环绑定示例探讨

    今天看到一个javascript的题目,按常理循环绑定事件,但是得到的结果却不是想要的. 复制代码 代码如下: <a href="#">text</a> <br> <a href="#">link</a> <script> var as = document.getElementsByTagName('a'); for ( var i = as.length; i--; ) { as[i].on

  • javascript实现图片循环渐显播放的方法

    本文实例讲述了javascript实现图片循环渐显播放的方法.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: <html> <title>图片的循环渐显播放效果代码</title> <head> <!--1.将下面的代码插入到HEML的<head></head>之间: --> <script language=javaScript> <!--// sandra0 = new Image()

随机推荐