ES5中新增的Array方法详细说明

这篇文章发布于 2013年04月25日,星期四,21:43,归类于 JS实例。 阅读 338567 次, 今日 20 次 46 条评论

 

一、前言-索引

ES5中新增的不少东西,了解之对我们写JavaScript会有不少帮助,比如数组这块,我们可能就不需要去有板有眼地for循环了。

ES5中新增了写数组方法,如下:

  1. forEach (js v1.6)
  2. map (js v1.6)
  3. filter (js v1.6)
  4. some (js v1.6)
  5. every (js v1.6)
  6. indexOf (js v1.6)
  7. lastIndexOf (js v1.6)
  8. reduce (js v1.8)
  9. reduceRight (js v1.8)

浏览器支持

  • Opera 11+
  • Firefox 3.6+
  • Safari 5+
  • Chrome 8+
  • Internet Explorer 9+

对于让人失望很多次的IE6-IE8浏览器,Array原型扩展可以实现以上全部功能,例如forEach方法:

// 对于古董浏览器,如IE6-IE8

if (typeof Array.prototype.forEach != "function") {
  Array.prototype.forEach = function () {
    /* 实现 */
  };
}

二、一个一个来

  1. forEach
    forEach是Array新方法中最基本的一个,就是遍历,循环。例如下面这个例子:

    [1, 2 ,3, 4].forEach(alert);

    等同于下面这个传统的for循环:

    var array = [1, 2, 3, 4];
    
    for (var k = 0, length = array.length; k < length; k++) {
      alert(array[k]);
    }

    Array在ES5新增的方法中,参数都是function类型,默认有传参,这些参数分别是?见下面:

    [1, 2 ,3, 4].forEach(console.log);
    
    // 结果:
    
    // 1, 0, [1, 2, 3, 4]
    // 2, 1, [1, 2, 3, 4]
    // 3, 2, [1, 2, 3, 4]
    // 4, 3, [1, 2, 3, 4]

    用来判断参数个数以及内容的测试输出截图 张鑫旭-鑫空间-鑫生活

    显而易见,forEach方法中的function回调支持3个参数,第1个是遍历的数组内容;第2个是对应的数组索引,第3个是数组本身。

    因此,我们有:

    [].forEach(function(value, index, array) {
        // ...
    });

    对比jQuery中的$.each方法:

    $.each([], function(index, value, array) {
        // ...
    });

    会发现,第1个和第2个参数正好是相反的,大家要注意了,不要记错了。后面类似的方法,例如$.map也是如此。

    现在,我们就可以使用forEach卖弄一个稍显完整的例子了,数组求和:

    var sum = 0;
    
    [1, 2, 3, 4].forEach(function (item, index, array) {
      console.log(array[index] == item); // true
      sum += item;
    });
    
    alert(sum); // 10

    再下面,更进一步,forEach除了接受一个必须的回调函数参数,还可以接受一个可选的上下文参数(改变回调函数里面的this指向)(第2个参数)。

    array.forEach(callback,[ thisObject])

    例子更能说明一切:

    var database = {
      users: ["张含韵", "江一燕", "李小璐"],
      sendEmail: function (user) {
        if (this.isValidUser(user)) {
          console.log("你好," + user);
        } else {
          console.log("抱歉,"+ user +",你不是本家人");	
        }
      },
      isValidUser: function (user) {
        return /^张/.test(user);
      }
    };
    
    // 给每个人法邮件
    database.users.forEach(  // database.users中人遍历
      database.sendEmail,    // 发送邮件
      database               // 使用database代替上面标红的this
    );
    
    // 结果:
    // 你好,张含韵
    // 抱歉,江一燕,你不是本家人
    // 抱歉,李小璐,你不是本家

    如果这第2个可选参数不指定,则使用全局对象代替(在浏览器是为window),严格模式下甚至是undefined.

    另外,forEach不会遍历纯粹“占着官位吃空饷”的元素的,例如下面这个例子:

    var array = [1, 2, 3];
    
    delete array[1]; // 移除 2
    alert(array); // "1,,3"
    
    alert(array.length); // but the length is still 3
    
    array.forEach(alert); // 弹出的仅仅是1和3

    综上全部规则,我们就可以对IE6-IE8进行仿真扩展了,如下代码:

    // 对于古董浏览器,如IE6-IE8
    
    if (typeof Array.prototype.forEach != "function") {
      Array.prototype.forEach = function (fn, context) {
        for (var k = 0, length = this.length; k < length; k++) {
          if (typeof fn === "function" && Object.prototype.hasOwnProperty.call(this, k)) {
            fn.call(context, this[k], k, this);
          }
        }
      };
    }
    

    现在拿上面“张含韵”的例子测下我们扩展的forEach方法,您可能狠狠地点击这里:兼容处理的forEach方法demo

    例如IE7浏览器下:
    IE7下forEach方法测试结果截图 张鑫旭-鑫空间-鑫生活

  2. map
    这里的map不是“地图”的意思,而是指“映射”。[].map(); 基本用法跟forEach方法类似:

    array.map(callback,[ thisObject]);

    callback的参数也类似:

    [].map(function(value, index, array) {
        // ...
    });

    map方法的作用不难理解,“映射”嘛,也就是原数组被“映射”成对应新数组。下面这个例子是数值项求平方:

    var data = [1, 2, 3, 4];
    
    var arrayOfSquares = data.map(function (item) {
      return item * item;
    });
    
    alert(arrayOfSquares); // 1, 4, 9, 16

    callback需要有return值,如果没有,就像下面这样:

    var data = [1, 2, 3, 4];
    var arrayOfSquares = data.map(function() {});
    
    arrayOfSquares.forEach(console.log);

    结果如下图,可以看到,数组所有项都被映射成了undefined
    全部项都成了undefined

    在实际使用的时候,我们可以利用map方法方便获得对象数组中的特定属性值们。例如下面这个例子(之后的兼容demo也是该例子):

    var users = [
      {name: "张含韵", "email": "zhang@email.com"},
      {name: "江一燕",   "email": "jiang@email.com"},
      {name: "李小璐",  "email": "li@email.com"}
    ];
    
    var emails = users.map(function (user) { return user.email; });
    
    console.log(emails.join(", ")); // zhang@email.com, jiang@email.com, li@email.com

    Array.prototype扩展可以让IE6-IE8浏览器也支持map方法:

    if (typeof Array.prototype.map != "function") {
      Array.prototype.map = function (fn, context) {
        var arr = [];
        if (typeof fn === "function") {
          for (var k = 0, length = this.length; k < length; k++) {      
             arr.push(fn.call(context, this[k], k, this));
          }
        }
        return arr;
      };
    }

    您可以狠狠地点击这里:兼容map方法测试demo

    结果显示如下图,IE6浏览器:
    map映射方法测试,IE6下截图

  3. filter
    filter为“过滤”、“筛选”之意。指数组filter后,返回过滤后的新数组。用法跟map极为相似:

    array.filter(callback,[ thisObject]);

    filtercallback函数需要返回布尔值truefalse. 如果为true则表示,恭喜你,通过啦!如果为false, 只能高歌“我只能无情地将你抛弃……”

    可能会疑问,一定要是Boolean值吗?我们可以简单测试下嘛,如下:

    var data = [0, 1, 2, 3];
    var arrayFilter = data.filter(function(item) {
        return item;
    });
    console.log(arrayFilter); // [1, 2, 3]

    有此可见,返回值只要是弱等于== true/false就可以了,而非非得返回 === true/false.

    因此,我们在为低版本浏览器扩展时候,无需关心是否返回值是否是纯粹布尔值(见下黑色代码部分):

    if (typeof Array.prototype.filter != "function") {
      Array.prototype.filter = function (fn, context) {
        var arr = [];
        if (typeof fn === "function") {
           for (var k = 0, length = this.length; k < length; k++) {
              fn.call(context, this[k], k, this) && arr.push(this[k]);
           }
        }
        return arr;
      };
    }

    接着上面map筛选邮件的例子,您可以狠狠地点击这里:兼容处理后filter方法测试demo

    主要测试代码为:

    var emailsZhang = users
      // 获得邮件
      .map(function (user) { return user.email; })
      // 筛选出zhang开头的邮件
      .filter(function(email) {  return /^zhang/.test(email); });
    
    console.log(emailsZhang.join(", ")); // zhang@email.com

    filter demo页面的结果截图 张鑫旭-鑫空间-鑫生活

    实际上,存在一些语法糖可以实现map+filter的效果,被称之为“数组简约式(Array comprehensions)”。目前,仅FireFox浏览器可以实现,展示下又不会怀孕:

    var zhangEmails = [user.email for each (user in users) if (/^zhang/.test(user.email)) ];
    
    console.log(zhangEmails); // [zhang@email.com]

    数组简约式(Array comprehensions)实现的map+filter效果

  4. some
    some意指“某些”,指是否“某些项”合乎条件。与下面的every算是好基友,every表示是否“每一项”都要靠谱。用法如下:

    array.some(callback,[ thisObject]);

    例如下面的简单使用:

    var scores = [5, 8, 3, 10];
    var current = 7;
    
    function higherThanCurrent(score) {
      return score > current;
    }
    
    if (scores.some(higherThanCurrent)) {
      alert("朕准了!");
    }

    结果弹出了“朕准了”文字。 some要求至少有1个值让callback返回true就可以了。显然,8 > 7,因此scores.some(higherThanCurrent)值为true.

    我们自然可以使用forEach进行判断,不过,相比some, 不足在于,some只有有true即返回不再执行了。

    IE6-IE8扩展如下:

    if (typeof Array.prototype.some != "function") {
      Array.prototype.some = function (fn, context) {
    	var passed = false;
    	if (typeof fn === "function") {
       	  for (var k = 0, length = this.length; k < length; k++) {
    		  if (passed === true) break;
    		  passed = !!fn.call(context, this[k], k, this);
    	  }
        }
    	return passed;
      };
    }

    于是,我们就有了“朕准了”的demo,您可以狠狠地点击这里:兼容处理后的some方法demo

    IE7浏览器下some扩展demo结果

  5. every
    some的基友关系已经是公开的秘密了,同样是返回Boolean值,不过,every需要每一个妃子都要让朕满意,否则——“来人,给我拖出去砍了!”

    IE6-IE8扩展(与some相比就是truefalse调换一下):

    if (typeof Array.prototype.every != "function") {
      Array.prototype.every = function (fn, context) {
        var passed = true;
        if (typeof fn === "function") {
           for (var k = 0, length = this.length; k < length; k++) {
              if (passed === false) break;
              passed = !!fn.call(context, this[k], k, this);
          }
        }
        return passed;
      };
    }

    还是那个朕的例子,您可以狠狠地点击这里:是否every妃子让朕满意demo

    if (scores.every(higherThanCurrent)) {
      console.log("朕准了!");
    } else {
      console.log("来人,拖出去斩了!");        
    }

    结果是:
    every方法结果

  6. indexOf
    indexOf方法在字符串中自古就有,string.indexOf(searchString, position)。数组这里的indexOf方法与之类似。

    array.indexOf(searchElement[, fromIndex])

    返回整数索引值,如果没有匹配(严格匹配),返回-1. fromIndex可选,表示从这个位置开始搜索,若缺省或格式不合要求,使用默认值0,我在FireFox下测试,发现使用字符串数值也是可以的,例如"3"3都可以。

    var data = [2, 5, 7, 3, 5];
    
    console.log(data.indexOf(5, "x")); // 1 ("x"被忽略)
    console.log(data.indexOf(5, "3")); // 4 (从3号位开始搜索)
    
    console.log(data.indexOf(4)); // -1 (未找到)
    console.log(data.indexOf("5")); // -1 (未找到,因为5 !== "5")

    兼容处理如下:

    if (typeof Array.prototype.indexOf != "function") {
      Array.prototype.indexOf = function (searchElement, fromIndex) {
        var index = -1;
        fromIndex = fromIndex * 1 || 0;
    
        for (var k = 0, length = this.length; k < length; k++) {
          if (k >= fromIndex && this[k] === searchElement) {
              index = k;
              break;
          }
        }
        return index;
      };
    }

    一个路子下来的,显然,轮到demo了,您可以狠狠地点击这里:兼容处理后indexOf方法测试demo

    下图为ietester IE6下的截图:
    IE6下indexOf扩展后测试结果截图

  7. lastIndexOf
    lastIndexOf方法与indexOf方法类似:

    array.lastIndexOf(searchElement[, fromIndex])

    只是lastIndexOf是从字符串的末尾开始查找,而不是从开头。还有一个不同就是fromIndex的默认值是array.length - 1而不是0.

    IE6等浏览器如下折腾:

    if (typeof Array.prototype.lastIndexOf != "function") {
      Array.prototype.lastIndexOf = function (searchElement, fromIndex) {
        var index = -1, length = this.length;
        fromIndex = fromIndex * 1 || length - 1;
    
        for (var k = length - 1; k > -1; k-=1) {
            if (k <= fromIndex && this[k] === searchElement) {
                index = k;
                break;
            }
        }
        return index;
      };
    }

    于是,则有:

    var data = [2, 5, 7, 3, 5];
    
    console.log(data.lastIndexOf(5)); // 4
    console.log(data.lastIndexOf(5, 3)); // 1 (从后往前,索引值小于3的开始搜索)
    
    console.log(data.lastIndexOf(4)); // -1 (未找到)

    懒得截图了,结果查看可狠狠地点击这里:lastIndexOf测试demo

  8. reduce
    reduce是JavaScript 1.8中才引入的,中文意思为“减少”、“约简”。不过,从功能来看,我个人是无法与“减少”这种含义联系起来的,反而更接近于“迭代”、“递归(recursion)”,擦,因为单词这么接近,不会是ECMA-262 5th制定者笔误写错了吧~~

    此方法相比上面的方法都复杂,用法如下:

    array.reduce(callback[, initialValue])

    callback函数接受4个参数:之前值、当前值、索引值以及数组本身。initialValue参数可选,表示初始值。若指定,则当作最初使用的previous值;如果缺省,则使用数组的第一个元素作为previous初始值,同时current往后排一位,相比有initialValue值少一次迭代。

    var sum = [1, 2, 3, 4].reduce(function (previous, current, index, array) {
      return previous + current;
    });
    
    console.log(sum); // 10

    说明:

    1. 因为initialValue不存在,因此一开始的previous值等于数组的第一个元素。
    2. 从而current值在第一次调用的时候就是2.
    3. 最后两个参数为索引值index以及数组本身array.

    以下为循环执行过程:

    // 初始设置
    previous = initialValue = 1, current = 2
    
    // 第一次迭代
    previous = (1 + 2) =  3, current = 3
    
    // 第二次迭代
    previous = (3 + 3) =  6, current = 4
    
    // 第三次迭代
    previous = (6 + 4) =  10, current = undefined (退出)

    有了reduce,我们可以轻松实现二维数组的扁平化:

    var matrix = [
      [1, 2],
      [3, 4],
      [5, 6]
    ];
    
    // 二维数组扁平化
    var flatten = matrix.reduce(function (previous, current) {
      return previous.concat(current);
    });
    
    console.log(flatten); // [1, 2, 3, 4, 5, 6]

    兼容处理IE6-IE8:

    if (typeof Array.prototype.reduce != "function") {
      Array.prototype.reduce = function (callback, initialValue ) {
         var previous = initialValue, k = 0, length = this.length;
         if (typeof initialValue === "undefined") {
            previous = this[0];
            k = 1;
         }
         
        if (typeof callback === "function") {
          for (k; k < length; k++) {
             this.hasOwnProperty(k) && (previous = callback(previous, this[k], k, this));
          }
        }
        return previous;
      };
    }

    然后,测试整合,demo演示,您可以狠狠地点击这里:兼容IE6的reduce方法测试demo

    IE6浏览器下结果如下图:
    reduce方法测试页面结构示意

  9. reduceRight
    reduceRightreduce相比,用法类似:

    array.reduceRight(callback[, initialValue])

    实现上差异在于reduceRight是从数组的末尾开始实现。看下面这个例子:

    var data = [1, 2, 3, 4];
    var specialDiff = data.reduceRight(function (previous, current, index) {
      if (index == 0) {
        return previous + current;
      }
      return previous - current;
    });
    
    console.log(specialDiff); // 0

    结果0是如何得到的呢?
    我们一步一步查看循环执行:

    // 初始设置
    index = 3, previous = initialValue = 4, current = 3
    
    // 第一次迭代
    index = 2, previous = (4- 3) = 1, current = 2
    
    // 第二次迭代
    index = 1, previous = (1 - 2) = -1, current = 1
    
    // 第三次迭代
    index = 0, previous = (-1 + 1) = 0, current = undefined (退出)

    为使低版本浏览器支持此方法,您可以添加如下代码:

    if (typeof Array.prototype.reduceRight != "function") {
      Array.prototype.reduceRight = function (callback, initialValue ) {
        var length = this.length, k = length - 1, previous = initialValue;
        if (typeof initialValue === "undefined") {
            previous = this[length - 1];
            k--;
        }
        if (typeof callback === "function") {
           for (k; k > -1; k-=1) {          
              this.hasOwnProperty(k) && (previous = callback(previous, this[k], k, this));
           }
        }
        return previous;
      };
    }

    您可以狠狠地点击这里:reduceRight简单使用demo

    对比FireFox浏览器和IE7浏览器下的结果:
    FireFox浏览器下reduceRight测试结果 对比IE7下reduceRight测试结果截图 张鑫旭-鑫空间-鑫生活

三、更进一步的应用

我们还可以将上面这些数组方法应用在其他对象上。

例如,我们使用forEach遍历DOM元素。

var eleDivs = document.getElementsByTagName("div");
Array.prototype.forEach.call(eleDivs, function(div) {
    console.log("该div类名是:" + (div.className || "空"));
});

可以输出页面所有div的类名,您可以狠狠地点击这里:Array新方法forEach遍历DOM demo

结果如下,IE6下,demo结果:
forEach方法遍历DOM的使用演示 张鑫旭-鑫空间-鑫生活

等很多其他类数组应用。

四、最后一点点了

本文为低版本IE扩展的Array方法我都合并到一个JS中了,您可以轻轻的右键这里或下载或查看:es5-array.js

以上所有未IE扩展的方法都是自己根据理解写的,虽然多番测试,难免还会有细节遗漏的,欢迎指出来。

参考文章:
JavaScript array “extras” in detail
Array.forEach

(本篇完)

分享到:


发表评论(目前46 条评论)

  1. 一只菜鸟前端攻城狮说道:

    6的头皮发麻

  2. Ricky说道:

    谢天谢地,我终于弄明白forEach了,感谢!

  3. 鲁西西说道:

    reduceRight的解析错了,index应该分别是:2, 1, 0。希望大神能看到并纠正

  4. libing_cheer说道:

    4. some
    function higherThanCurrent(score) {
    return score > current;
    }
    中的score应该为scores

  5. 大神方便加个微信吗说道:

    好几年前的都给我受益匪浅,多谢

  6. 求助:react中这样的怎么做map遍历进行数据绑定?怎么嵌套说道:

    <SubMenu key="sub1" title={{slidedata}}>
    Option 1
    Option 2
    Option 3
    Option 4

    <SubMenu key="sub2" title={{slidedata}}>
    Option 5
    Option 6
    Option 7
    Option 8

    ……..

  7. 求指教说道:

    forEach (js v1.6)

    括号里面的 js v1.6 是啥意思啊 ??? js version 1.6 什么鬼

  8. aspwebchh说道:

    函数式编程的方法

  9. 牛奶007说道:

    大神,在reduce第一个例子中,在第二次迭代 current应该等于6吧。
    《以下原文》
    // 第二次迭代
    previous = (3 + 3) = 6, current = 4

    // 第三次迭代
    previous = (6 + 4) = 10, current = undefined (退出)

    • 阿磊说道:

      // 初始设置
      previous = initialValue = 1, current = 2

      // 第一次迭代
      previous = (1 + 2) = 3, current = 3

      // 第二次迭代
      previous = (3 + 3) = 6, current = 4

      // 第三次迭代
      previous = (6 + 4) = 10, current = undefined (退出)

      *********************************************************************
      你在好好琢磨下第一次迭代。current代表的是下一个数。

  10. 说道一下说道:

    大神,请问你们公司校招什么时候开始?招聘前端吗?谢谢!

  11. 杨金凯说道:

    大神,这个遍历既然指定了起始索引就没有必要在从头开始遍历了吧?
    我自己写了一个类似的
    Array.prototype.myIndexOf = function(val,index){
    var i = -1;
    index = Math.floor(index*1) || 0; //去掉小数位
    for(; index<this.length;index++){
    if(this[index] === val){
    i = index;
    break;
    }
    }
    return i;
    }

  12. Guyw说道:

    学习了~

  13. 12cat说道:

    居然没有张含韵图,差评。

  14. ChieveiT说道:

    好文,感谢分享。另外reduce的意思是“归约”,通过一个过程把一个结果集转化成一个结果,通常说的map-reduce算法中的reduce也是这个意思

  15. ileason说道:

    很好,學到東西了

  16. 刺客说道:

    学习了!

  17. 我在山上砍柴说道:

    比书上还详细,费心了。

  18. 刺客说道:

    很详细!

  19. Saku说道:

    学习了

  20. matrix说道:

    Reduce 应该是归纳的意思~ ca

  21. 12sa说道:

    你好 你说的那个forEach中的第二个参数改变上下文环境的例子中,callback函数本身就是database.sendemail,是对象方法调用,本身的this指向不就是database对象吗?为什么会指向全局对象呢?全局对象不是在作为一个函数调用时才指向全局对象吗?

  22. tim说道:

    why call it reduce: 直到reduce到只剩accumulator(你称为previous值),返回它!

  23. ylxdzsw说道:

    Lisper一般把reduce翻译成“归约”

  24. 子不语说道:

    之所以叫 reduce , 是要跟 map 对应

  25. tianyn说道:

    一个小小小问题:
    原生的indexOf方法的第二个参数支持负数,表示从末尾向前计算,张老板忽略了。

  26. code_bunny说道:

    还有个问题,在使用every的时候,即使是[7,8,,9]这个数组,如果判断是不是>6,它会忽略空项,所以得到的结果也是true,所以在扩展的时候也应该加个hasOwnProperty判断

  27. code_bunny说道:

    hi:

    .map方法的扩展,是不是也需要判断一下hasOwnProperty? 否则当数组中的其中一项没有的时候,在垃圾浏览器里会多一项出来,比如如果是计算,就会多一项NaN…

  28. canvast说道:

    forEach在IE6-8的扩展函数中应该不需要Object.prototype.hasOwnProperty.call(this, k)吧?
    参考文章http://martinrinehart.com/frontend-engineering/engineers/javascript/arrays/array-loops.html?utm_source=javascriptweekly&utm_medium=email

    • 深红说道:

      Object.prototype.hasOwnProperty.call(this, k)不是为了跳过数组中那些被delete或未被赋值的项吗?

      • 说道:

        Object.prototype.hasOwnProperty对delete后的数据有过滤作用,undefined的数据还是返回true呀

  29. 大超超。说道:

    不明觉厉,ES5用的地方多不多呀?平时都是拿jQuery做js方面的开发。

  30. airoschou说道:

    额,算是hack吗?

  31. 小方说道:

    DYB 兄弟,不能这样用。console.log函数的形参类型可以有多个,比如这样 用console.log(‘你好%s’,’好人’);这时,forEach(callback),callback(a[i],i,a);明显实参与形参不匹配了

  32. bennyrice说道:

    forEach 那个例子里~~ 即使不指定database为执行上下文,红色的this也还是会是database本身吧?因为它出现在的那个函数是database的方法。默认会指向对象本身。

    • 张 鑫旭说道:

      @bennyrice 不要误导小朋友。如果不指定database为执行上下文,指向的是database.email而不是database. 会出错的哦!

      • goss说道:

        database.sendEmail并没有直接被调用,只是做了方法的传递,如果是database.sendEmail()就跟@bennyrice说的一样,以database为上下文;但是database.sendEmail,就要看具体调用了,很有可能是undefined。

      • 说道:

        不指定database的话,应该this 为window吧,我在chrome中得到的this也为window。

  33. bennyrice说道:

    @DYB 可以使用 [1,2,3].forEach(console.log,console) ~~~

  34. DYB说道:

    很不幸。chrome的console不支持[1,2,3].forEach(console.log)

  35. VVG说道:

    你再不得到张含韵她就老了!!!