JavaScript数组

JavaScript数组的应用应该都比较熟悉了。先上一张神图,转自右下角大神。

  • forEach,map,filter
  • some,every
  • reduce,reduceRight
  • slice,splice
  • indexOf,lastIndexOf
  • sort
  • 类数组对象

forEach,map,filter

forEach遍历数组,函数声明:[].forEach( function(value, index, array) { … }, [thisArg] );。参照MDN

第一个参数是回调函数,它支持3个参数,第1个是遍历的数组内容,第2个是对应索引,第3个是数组自身。

第二个参数thisArg可选,可用于以改变回调函数里面的this指针

因为forEach是第一个被介绍的数组方法,所以稍微详细点用console.log看一下回调函数的3个参数。(之后的数组方法有兴趣的可以自己用console.log看一下回调函数,不赘述)

[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]

上面已经清晰地展现了遍历的结果,第一列是value,第二列是对应的index值,第三列是数组本身。

现在用forEach实现数组求和:

var price = 0;
[1.2, 2.4, 0.6, 7].forEach(function (value) {
    price += value;
});
console.log(price);     //11.2

相比for循环,上述代码除了更简单外,还避免了常见的for循环的起始,终止条件越界等错误。对于数组遍历来说,forEach和map是优于for循环的。

现在看看第二个参数thisArgs的作用,如果不指定该参数,回调函数内的this指向的是window(关于this可以参照这里),例如上例中的回调函数里,你可以写成this.price += value;,效果是一样的(当然前提是变量确实是window的全局属性)。但有时this指向window就不对了,如下:

var group = {
    members: ["Jack", "Andy", "Natasha"],
    joinParty: "Yes",
    getInfo: function (m) {
        this.isJoinParty(m);
        console.log(m + " " + this.joinParty);
    },
    isJoinParty: function (m) {
        switch(m) {
        case "Andy" :
            this.joinParty = "No";
            break;
        default:
            this.joinParty = "Yes";
            break;
        }
    }
};
group.members.forEach(group.getInfo);

代码很简单,小组内3人,Andy不参加聚会,另两人参加聚会。期望把统计结果打印出来。但很遗憾上面代码会报Error。按理说getInfo函数里的this应该指向group对象,但遗憾地是getInfo作为[].forEach的回调函数时相当于普通函数,因此getInfo里的this指向的是window。而window对象里显然不存在isJoinParty。

因此正确的调用方式是,添加第二个参数,明确指定this的绑定对象:

group.members.forEach(group.getInfo, group);
//Jack Yes
//Andy No
//Natasha Yes

map映射创建新数组,函数声明:[].map( function(value, index, array) { … }, [thisArg] );。和forEach一样,不赘述。唯一需要注意的的是回调函数需要有return值,否则新数组都是undefined。

其实map能干的事forEach都能干,你可以把map理解为forEach的一个特例,专门用于:通过现有的数组建立新数组。例如将旧数组中字符串都trim一下,去除空格后生成新数组:

var trimmed = ['    Jack','Betty    ','    Chirs    '].map(function(s) {
    return s.trim();    //需要return值,否则新数组里都是undefined
});
console.log(trimmed);    //["Jack", "Betty", "Chirs"]

在没有map之前是通过forEach来创建新数组的。你需要先定义一个空数组,再将每次trim后的字符串push到新数组内,比较麻烦。因为“通过现有的数组建立新数组”这个需求是如此的普遍,因此ES5中干脆追加了map方法,相比forEach代码简单优雅多了。

filter用于过滤数组,函数声明:[].filter( function(value, index, array) { … }, [thisArg] );。和forEach一样,不赘述。唯一需要注意的的是回调函数需要return布尔值true或false,如果忘记写return语句,返回得到的是空数组,表示一个都不匹配。例如:

var newArray = [0, 1, 2].filter(function(value) {});
console.log(newArray);      //[],没有return语句得到的是空数组

//过滤出不超过10的正数
var newArray2 = [0, 1, 2, 14].filter(function(value) {
    return value > 0 && value <= 10;
});
console.log(newArray2);   //[1, 2]

some,every

some表示只要某一个满足条件就OK,every表示全部满足条件才OK。

some的函数声明:[].some( function(value, index, array) { … }, [thisArg] );

every的函数声明:[].every( function(value, index, array) { … }, [thisArg] );

参照MDN。其实都和上面的forEach一样,不赘述。唯一需要注意的的是回调函数需要return布尔值true或false,如果忘记写return语句,表示不满足条件,返回false

[1, 10, 100].some(function(x) { x > 5; });      //false,忘记写return了
[1, 2, 3, 4, 5].every(function(x) { x > 0; });  //false,忘记写return了

[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

reduce,reduceRight

两者都是用于迭代运算。区别是reduce从头开始迭代,reduceRight从尾开始迭代。

reduce的函数声明:[].reduce( function(previousValue, currentValue, currentIndex, array) { … }, [initialValue] );

第一个参数是回调函数,有4个参数:previousValue,currentValue,currentIndex,array。看名字也能知道意思:前一个值,当前值,当前索引,数组本身。

第二个参数initialValue可选,表示初始值。如果省略,初始值为数组的第一个元素,这样的话回调函数里previousValue就是第一个元素,currentValue是第二个元素。因此不设initialValue的话,会少一次迭代。例如:

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

//给它加上initialValue初始值10
var sum2 = [1, 2, 3, 4].reduce(function (previous, current) {
    return previous + current;
}, 10);
console.log(sum2);    //20

上图清楚地表明了各个运算步骤,很容易理解。如果不设initialValue,会少一次迭代。

reduceRight的函数声明:[].reduceRight( function(previousValue, currentValue, currentIndex, array) { … }, [initialValue] );。和reduce一样,不赘述

用reduce和reduceRight很容易就能实现二维数组扁平化,如下:

var flat1 = [[0, 1], [2, 3], [4, 5]].reduce(function(a, b) {
    return a.concat(b);
});
console.log(flat1);    //[0, 1, 2, 3, 4, 5]

var flat2 = [[0, 1], [2, 3], [4, 5]].reduceRight(function(a, b) {
    return a.concat(b);
});
console.log(flat2);    //[4, 5, 2, 3, 0, 1]

slice,splice

两者做的事情还不太一样,但名字实在太像了,所以放一起介绍。

slice用于复制数组,复制完后旧数组不变,返回得到的新数组是旧数组的子集。函数声明:[].slice(begin, [end])。参照MDN

第一个参数begin是开始复制的位置,需要注意的是,可以设负数。设负数表示从尾往前数几个位置开始复制。例如slice(-2)将从倒数第2个元素开始复制。另外需要注意的是,该参数虽未标注为可选,但实际上是可以省略的,省略的话默认为0。

第二个参数end可选,表示复制到该位置的前一个元素。例如slice(0,3)将得到前3个元素,但不包含第4个元素。不设的话默认复制到数组尾,即等于array.length。

var fruits = ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango'].slice(0, 3);
console.log(fruits);    //["Banana", "Orange", "Lemon"]

var fruits2 = ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango'].slice(-1);
console.log(fruits2);    //["Mango"]

当然slice最常见的是用在将类数组arguments对象转换为真正的数组:

var args = [].slice.call(arguments);

splice用于剥离数组,从旧数组中移除元素,返回得到的新数组是被移除的元素。函数声明:[].splice(start, deleteCount, [item…])。参照MDN

第一个参数start是开始剥离的位置,需要注意的是,可以设负数。设负数表示从尾往前数几个位置开始剥离。例如splice (-2)将从倒数第2个元素开始剥离。

第二个参数deleteCount是要剥离的元素个数,设0表示一个都不剥离。

第三个参数开始可选,用于替换旧数组中被移除的元素

var oldArray = ['a', 'b', 'c'];
var newArray = oldArray.splice(1, 2, 'Jack', 'Betty', 'Andy');
console.log(oldArray);     //["a", "Jack", "Betty", "Andy"]
console.log(newArray);     //["b", "c"]

一个常见的应用就是删除数组内某元素,用delete的话会留下空洞,应该用splice方法:

//错误的方法用delete
var numbers = [0, 1, 2, 3, 4];
delete numbers[2];
console.log(numbers);     //[0, 1, undefined, 3, 4]

//正确的方法用splice
numbers.splice(2, 1); 
console.log(numbers);     //[0, 1, 3, 4]

indexOf,lastIndexOf

两者都用于返回项目的索引值。区别是indexOf从头开始找,lastIndexOf从尾开始找。如果查找失败,无匹配,返回-1

indexOf的函数声明:[].indexOf( searchElement, [fromIndex = 0] );。参照MDN

lastIndexOf的函数声明:[].lastIndexOf( searchElement, [fromIndex = arr.length – 1] );

第一个参数searchElement即需要查找的元素。第二个参数fromIndex可选,指定开始查找的位置。如果忽略,indexOf默认是0,lastIndexOf默认是数组尾。

['a', 'b', 'd', 'e'].indexOf('b');      //1
['a', 'b', 'd', 'e'].indexOf('b', 2);   //-1,从2号位开始找没找到
['a', 'b', 'd', 'e'].indexOf('c');      //-1,没找到

['a', 'b', 'd', 'e'].lastIndexOf('b');      //1
['a', 'b', 'd', 'e'].lastIndexOf('b', 2);   //1,逆向2号位等价于正向1号位
['a', 'b', 'd', 'e'].lastIndexOf('c');      //-1,没找到

sort

sort用于排序数组,函数声明:[].sort( [sortfunction] );。参照MDN

它就一个参数,就是排序函数指针。而且是可选的,不设的话有默认的排序函数,数字的话会升序排列,string会根据Unicode升序排列。

var sumArray = [4, 3, 1, 0, 2];
var sumArray2 = ['d', 'z', 'a'];
sumArray.sort();
sumArray2.sort();
console.log(sumArray);  //[0, 1, 2, 3, 4]
console.log(sumArray2); //["a", "d", "z"]

但是内置的默认排序函数,是不可靠的,如下:

var scores = [1, 10, 2, 21]; 
scores.sort(); 
console.log(scores);    //[1, 10, 2, 21]

因此保险起见最好自定义排序函数:

var scores = [1, 10, 2, 21]; 
function compareNumbers(x, y) {
    if (x < y) { return -1; } 
    if (x > y) { return 1; }
    return 0;
}
scores.sort(compareNumbers);
console.log(scores);        //[1, 2, 10, 21]

而且如果数组内是对象,或排序逻辑复杂的话,那默认排序函数更是力不从心了,必须自定义排序函数:

var items = [
    { name: 'Jack', value: 37 },
    { name: 'Betty', value: 21 },
    { name: 'Andy', value: 45 }
];
items.sort(function (a, b) {
    if (a.value < b.value) { return -1; } 
    if (a.value > b.value) { return 1; }
    return 0;
});
console.log(items);
//[{ name="Betty", value=21},
// { name="Jack", value=37}, 
// { name="Andy",  value=45}]

剩下的比较简单,大致说一下,就不详细介绍了。

pushpop用于数组尾处压入和弹出元素。

unshiftshift用于数组头部压入和弹出元素。

reverse用于反转数组,concat用于连接数组,join用于数组元素间插入些东西后拼接成string。

类数组

JS里有很多类数组对象。什么叫类数组对象呢?它们首先是对象,并没有继承Array,但长的却很像数组。最典型的如arguments对象,HTMLCollection对象。

类数组对象不能直接使用数组方法,但数组方法是如此简单便利,要在类数组对象身上使用数组方法,需要让数组函数通过call绑定类数组对象。

处理arguments对象:

var args = [].slice.call(arguments);

处理HTMLCollection对象:

//用forEach遍历页面所有div,输入className
var divs = document.getElementsByTagName("div");
Array.prototype.forEach.call(divs, function(div) {  
    console.log("该div类名是:" + (div.className || "空"));
});

//下面这样直接调用forEach将报错,因为divs是HTMLCollection对象而非Array
divs.forEach(function(div) {
    console.log("该div类名是:" + (div.className || "空"));
});

处理字面量对象:

var arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
var result = Array.prototype.map.call(arrayLike, function(s) {
    return s.toUpperCase();
});     
console.log(result);    //["A", "B", "C"]

处理字符串:

var result = Array.prototype.map.call("abc", function(s) {
    return s.toUpperCase();
});
console.log(result);    //["A", "B", "C"]

但Array的concat会检查参数的[[Class]]属性,只有参数是一个真实的数组才会将数组内容连接起来,否则将作为单个元素来连接。要完全实现连接,我们需要自己在对象上增加slice方法:

//单用concat的话,arguments对象将作为一个单一整体被连接
function namesColumn() {
    return ["Jack"].concat(arguments);
}
var newNames = namesColumn("Betty", "Andy", "Chris");
console.log(newNames);    //["Jack", ["Betty", "Andy", "Chris"]]

//配合slice能实现完全连接
function namesColumn() {
    return ["Jack"].concat([].slice.call(arguments));
}
var newNames = namesColumn("Betty", "Andy", "Chris");
console.log(newNames);    //["Jack", "Betty", "Andy", "Chris"]

总结

最后再提一个小经验,JS的一个常见的错误就是在必须使用数组时使用对象,或反之。经验是:当属姓名是小而连续的整数时用数组。否则用对象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • 数组的定义 数组是按序号排列的一组值,每个值的位置都有编号(从0开始)。数组本质上是一种特殊的对象。它的键名是按(...
    Allin_Lin阅读 517评论 0 0
  • 数组对于一个编程语言而言可谓举足轻重,当然 JavaScript 也对其相当重视,下面我就将自己接触到的数组有关的...
    ghwaphon阅读 711评论 0 2
  • 整理一下常用的数组方法(含ES6) Array.from(arrayLike[, mapFn[, thisArg]...
    慢飞鸟阅读 237评论 0 0
  • concat 注意,a数组并没有改变,只是返回了一个新数组。 copyWithin 它接受三个参数。target ...
    Funwt阅读 651评论 0 10
  • 裹着一身江南烟雨回家过年 妻子已经准备好总统套房 十多天后 旅客已走,客房依旧 后记,对于两地分居,当家成了旅馆,...
    鸿蒙一叶阅读 314评论 0 4