JavaScript数组是最常用的数据类型之一,对于数组的操作,JavaScript也提供了一些非常方便的函数和方法,对这些函数与方法的熟练掌握和运用,能让程序编写更方便,也使程序结构更清楚、更容易理解,本文代码均来自modilla MDN开发者官网。
1. map()方法
在JavaScript中,数组的map方法原型为Array.prototype.map()。
map()方法调用一个函数,将函数应用在数组中每个元素上,然后创建并返回一个新数组(不会修改原数组) 。例如,要将一个函数中每个变量乘以2,可以像下面这样使用map方法:
const array1=[1,2,3,4];const map1=array1.map(x=>x*2);//map1=[2,4,6,8]
官方文档中定义的map方法用法如下,其中,callback函数包含一个currentValue(数组中当前要处理的元素)参数与两个可选的参数index(当前正在处理的元素索引)以及array(map方法调用的数组),以及一个可选的thisArg用来指定this的作用域。
var new_array = arr.map(function callback(currentValue[, index[, array]]) {// Return element for new_array}[, thisArg])
1.1 使用 map 重新格式化数组中的对象
以下代码使用一个包含对象的数组来重新创建一个格式化后的数组。
var kvArray = [{key: 1, value: 10},{key: 2, value: 20},{key: 3, value: 30}];var reformattedArray = kvArray.map(function(obj) {var rObj = {};rObj[obj.key] = obj.value;return rObj;});// reformattedArray 数组为: [{1: 10}, {2: 20}, {3: 30}],// kvArray 数组未被修改:// [{key: 1, value: 10},// {key: 2, value: 20},// {key: 3, value: 30}]/*也可以用ES6开始支持的箭头函数写成以下样式var rfArray=kvArray.map((obj)=>{let rObj={};rObj[obj.key]=obj.value;return rObj});*/
1.2 使用一个包含一个参数的函数来mapping(构建)一个数字数组
下面的代码表示了当函数需要一个参数时map的工作方式。当map循环遍历原始数组时,这个参数会自动被分配成数组中对应的每个元素。
var numbers = [1, 4, 9];var doubles = numbers.map(function(num) {return num * 2;});// doubles数组的值为: [2, 8, 18]// numbers数组未被修改: [1, 4, 9]
1.3 一般的map 方法
下面的例子演示如何在一个 String 上使用 map 方法获取字符串中每个字符所对应的 ASCII 码组成的数组:
var map = Array.prototype.mapvar a = map.call("Hello World", function(x) {return x.charCodeAt(0);})//也可以用箭头函数改写//var a = map.call("Hello World", x=> x.charCodeAt(0))// a的值为[72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]
1.4 quenrySelectorAll应用
下面代码展示了如何去遍历用 querySelectorAll得到的动态对象集合。在这里,我们获得了文档里所有选中的选项,并将其打印:
var elems = document.querySelectorAll('select option:checked');var values = Array.prototype.map.call(elems, function(obj) {return obj.value;});
1.5 使用技巧案例
通常情况下,map 方法中的 callback 函数只需要接受一个参数,就是正在被遍历的数组元素本身。但这并不意味着 map 只给 callback 传了一个参数。这个思维惯性可能会让我们犯一个很容易犯的错误。
考虑下例:
["1", "2", "3"].map(parseInt);
我们期望输出 [1, 2, 3], 而实际结果是 [1, NaN, NaN].
parseInt 经常被带着一个参数使用, 但是这里接受两个。第一个参数是一个表达式而第二个是callback function的基, Array.prototype.map 传递3个参数:
- the element
- the index
- the array
第三个参数被parseInt忽视了,, 但不是第二个。因此可能出现混淆。下面是迭代步骤的简明示例:
// parseInt(string, radix) -> map(parseInt(value, index))/* first iteration (index is 0): */ parseInt("1", 0); // 1/* second iteration (index is 1): */ parseInt("2", 1); // NaN/* third iteration (index is 2): */ parseInt("3", 2); // NaN
下面让我们来讨论解决方案:
function returnInt(element) {return parseInt(element, 10);}['1', '2', '3'].map(returnInt); // [1, 2, 3]// Actual result is an array of numbers (as expected)// Same as above, but using the concise arrow function syntax['1', '2', '3'].map( str => parseInt(str) );// A simpler way to achieve the above, while avoiding the "gotcha":['1', '2', '3'].map(Number); // [1, 2, 3]// But unlike parseInt(), Number() will also return a float or (resolved) exponential notation:['1.1', '2.2e2', '3e300'].map(Number); // [1.1, 220, 3e+300]// For comparison, if we use parseInt() on the array above:['1.1', '2.2e2', '3e300'].map( str => parseInt(str) ); // [1, 2, 3]
一个map方法调用 parseInt 作为一个参数的等效输出运行如下:
var xs = ['10', '10', '10'];xs = xs.map(parseInt);console.log(xs); // 输出结果为(3) [10, NaN, 2]// Actual result of 10,NaN,2 may be unexpected based on the above description.
1.6 Mapping 含 undefined的数组
当返回undefined 或没有返回任何内容时:
var numbers = [1, 2, 3, 4];var filteredNumbers = numbers.map(function(num, index) {if(index < 3) {return num;}});//index goes from 0,so the filterNumbers are 1,2,3 and undefined.// filteredNumbers is [1, 2, 3, undefined]// numbers is still [1, 2, 3, 4]
2.filter()方法
在JavaScript中,数组的filter方法原型为Array.prototype.filter()。
与map()方法类似,filter()也提供一个函数并返回一个新的数组(不修改原数组),filter()返回的数组包含了满足函数条件的所有元素。
官方文档中提供的filter()方法如下,其中,callback函数包含一个element(数组中当前要处理的元素)参数与两个可选的参数index(当前正在处理的元素索引)以及array(filter方法调用的数组),以及一个可选的thisArg用来指定this的作用域。如果未提供thisArg参数,在非严格模式下this代表全局对象,严格模式下为undefined.
var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])
2.1 筛选排除所有较小的值
下例使用 filter 创建了一个新数组,该数组的元素由原数组中值大于 10 的元素组成。
function isBigEnough(element) {return element >= 10;}var filtered = [12, 5, 8, 130, 44].filter(isBigEnough);// filtered is [12, 130, 44]//或者用箭头函数改写//var filtered = [12, 5, 8, 130, 44].filter(item=>item>=10);
2.2 过滤 JSON 中的无效条目
以下示例使用 filter() 创建具有非零 id 的元素的 json。
var arr = [{ id: 15 },{ id: -1 },{ id: 0 },{ id: 3 },{ id: 12.2 },{ },{ id: null },{ id: NaN },{ id: 'undefined' }];var invalidEntries = 0;function isNumber(obj) {return obj !== undefined && typeof(obj) === 'number' && !isNaN(obj);}function filterByID(item) {if (isNumber(item.id) && item.id !== 0) {return true;}invalidEntries++;return false;}var arrByID = arr.filter(filterByID);console.log('Filtered Array\n', arrByID);// Filtered Array// [{ id: 15 }, { id: -1 }, { id: 3 }, { id: 12.2 }]console.log('Number of Invalid Entries = ', invalidEntries);// Number of Invalid Entries = 5
2.3 在数组中搜索
下例使用 filter() 根据搜索条件来过滤数组内容。
var fruits = ['apple', 'banana', 'grapes', 'mango', 'orange'];/*** Array filters items based on search criteria (query)*/function filterItems(query) {return fruits.filter(function(el) {return el.toLowerCase().indexOf(query.toLowerCase()) > -1;})}console.log(filterItems('ap')); // ['apple', 'grapes']console.log(filterItems('an')); // ['banana', 'mango', 'orange']
2.4 ES2015 实现
const fruits = ['apple', 'banana', 'grapes', 'mango', 'orange'];/*** Array filters items based on search criteria (query)*/const filterItems = (query) => {return fruits.filter((el) =>el.toLowerCase().indexOf(query.toLowerCase()) > -1);}console.log(filterItems('ap')); // ['apple', 'grapes']console.log(filterItems('an')); // ['banana', 'mango', 'orange']
3. reduce()方法(与reduceRight())
在JavaScript中,数组的reduce方法原型为Array.prototype.reduce()。与reduce方法类似,数组还包含了一个reduceRight()方法,唯一区别是后者逆序遍历每个数组元组。
reduce()方法对数组中每个元素依次执行一个给定的reducer函数,并将结果汇总为 单个值并返回。
官方文档中提供的reduce()方法如下,其中,callback函数包含一个accumulator(累加器中当前的结果)参数,一个currentValue(当前处理的数组元素)与两个可选的参数index(当前正在处理的元素索引)以及array(filter方法调用的数组),以及一个可选的initialValue用来指定函数初值。如果未提供initialValue参数,reduce函数的执行结果可能会出现意外情况,这是在使用reduce方法时尤其要注意的。
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
3.1数组里所有值的和
var sum = [0, 1, 2, 3].reduce(function (accumulator, currentValue) {return accumulator + currentValue;}, 0);// 和为 6
你也可以写成箭头函数的形式:
var total = [ 0, 1, 2, 3 ].reduce(( acc, cur ) => acc + cur,0);
3.2 累加对象数组里的值
要累加对象数组中包含的值,必须提供初始值,以便各个item正确通过你的函数。
var initialValue = 0;var sum = [{x: 1}, {x:2}, {x:3}].reduce(function (accumulator, currentValue) {return accumulator + currentValue.x;},initialValue)console.log(sum) // logs 6
你也可以写成箭头函数的形式:
var initialValue = 0;var sum = [{x: 1}, {x:2}, {x:3}].reduce((accumulator, currentValue) => accumulator + currentValue.x,initialValue);console.log(sum) // logs 6
3.3 将二维数组转化为一维
var flattened = [[0, 1], [2, 3], [4, 5]].reduce(function(a, b) {return a.concat(b);},[]);// flattened is [0, 1, 2, 3, 4, 5]
你也可以写成箭头函数的形式:
var flattened = [[0, 1], [2, 3], [4, 5]].reduce(( acc, cur ) => acc.concat(cur),[]);
3.4 计算数组中每个元素出现的次数
var names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];var countedNames = names.reduce(function (allNames, name) {if (name in allNames) {allNames[name]++;}else {allNames[name] = 1;}return allNames;}, {});// countedNames is:// { 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 }
3.5 按属性对object分类
var people = [{ name: 'Alice', age: 21 },{ name: 'Max', age: 20 },{ name: 'Jane', age: 20 }];function groupBy(objectArray, property) {return objectArray.reduce(function (acc, obj) {var key = obj[property];if (!acc[key]) {acc[key] = [];}acc[key].push(obj);return acc;}, {});}var groupedPeople = groupBy(people, 'age');// groupedPeople is:// {// 20: [// { name: 'Max', age: 20 },// { name: 'Jane', age: 20 }// ],// 21: [{ name: 'Alice', age: 21 }]// }
3.6 使用扩展运算符和initialValue绑定包含在对象数组中的数组
// friends - 对象数组// where object field "books" - list of favorite booksvar friends = [{name: 'Anna',books: ['Bible', 'Harry Potter'],age: 21}, {name: 'Bob',books: ['War and peace', 'Romeo and Juliet'],age: 26}, {name: 'Alice',books: ['The Lord of the Rings', 'The Shining'],age: 18}];// allbooks - list which will contain all friends' books +// additional list contained in initialValuevar allbooks = friends.reduce(function(prev, curr) {return [...prev, ...curr.books];}, ['Alphabet']);// allbooks = [// 'Alphabet', 'Bible', 'Harry Potter', 'War and peace',// 'Romeo and Juliet', 'The Lord of the Rings',// 'The Shining'// ]
3.7 数组去重
注意: 如果你正在使用一个可以兼容Set 和 Array.from() 的环境, 你可以使用let orderedArray = Array.from(new Set(myArray)); 来获得一个相同元素被移除的数组。
var myArray = ['a', 'b', 'a', 'b', 'c', 'e', 'e', 'c', 'd', 'd', 'd', 'd'];var myOrderedArray = myArray.reduce(function (accumulator, currentValue) {if (accumulator.indexOf(currentValue) === -1) {accumulator.push(currentValue);}return accumulator}, [])console.log(myOrderedArray);
let arr = [1,2,1,2,3,5,4,5,3,4,4,4,4];let result = arr.sort().reduce((init, current) => {if(init.length === 0 || init[init.length-1] !== current) {init.push(current);}return init;}, []);console.log(result); //[1,2,3,4,5]
3.8 按顺序运行Promise
/*** Runs promises from array of functions that can return promises* in chained manner** @param {array} arr - promise arr* @return {Object} promise object*/function runPromiseInSequence(arr, input) {return arr.reduce((promiseChain, currentFunction) => promiseChain.then(currentFunction),Promise.resolve(input));}// promise function 1function p1(a) {return new Promise((resolve, reject) => {resolve(a * 5);});}// promise function 2function p2(a) {return new Promise((resolve, reject) => {resolve(a * 2);});}// function 3 - will be wrapped in a resolved promise by .then()function f3(a) {return a * 3;}// promise function 4function p4(a) {return new Promise((resolve, reject) => {resolve(a * 4);});}const promiseArr = [p1, p2, f3, p4];runPromiseInSequence(promiseArr, 10).then(console.log); // 1200
3.9 功能型函数管道
// Building-blocks to use for compositionconst double = x => x + x;const triple = x => 3 * x;const quadruple = x => 4 * x;// Function composition enabling pipe functionalityconst pipe = (...functions) => input => functions.reduce((acc, fn) => fn(acc),input);// Composed functions for multiplication of specific valuesconst multiply6 = pipe(double, triple);const multiply9 = pipe(triple, triple);const multiply16 = pipe(quadruple, quadruple);const multiply24 = pipe(double, triple, quadruple);// Usagemultiply6(6); // 36multiply9(9); // 81multiply16(16); // 256multiply24(10); // 240
3.10 使用 reduce实现map
if (!Array.prototype.mapUsingReduce) {Array.prototype.mapUsingReduce = function(callback, thisArg) {return this.reduce(function(mappedArray, currentValue, index, array) {mappedArray[index] = callback.call(thisArg, currentValue, index, array);return mappedArray;}, []);};}[1, 2, , 3].mapUsingReduce((currentValue, index, array) => currentValue + index + array.length); // [5, 7, , 10]
4. 函数的apply()方法
和前面三个方法不同,apply()方法虽然通常被用在数组上,但在JavaScript中,apply()是Function中的方法,其原型为Function.prototype.apply()。apply()方法调用一个给定this值的函数,以及一个作为数组提供的参数。
apply方法和call方法非常类似,不同的地方在于apply接受一个参数数组,而call接受一个参数列表。
官方文档中提供的apply()方法如下,其中,thisArg参数代表函数运行时使用的this值,在非严格模式下,指定为null或undefined可以自动替换为指向全局对象,argArray参数是可选的,其中的数组元素将作为参数传递给函数,如果该参数值为null或者undefined,则表示不需要传入任何参数。
4.1 用 apply 将数组添加到另一个数组
我们可以使用push将元素追加到数组中。并且,因为push接受可变数量的参数,我们也可以一次推送多个元素。但是,如果我们传递一个数组来推送,它实际上会将该数组作为单个元素添加,而不是单独添加元素,因此我们最终得到一个数组内的数组。如果那不是我们想要的怎么办?在这种情况下,concat确实具有我们想要的行为,但它实际上并不附加到现有数组,而是创建并返回一个新数组。 但是我们想要附加到我们现有的阵列……那么现在呢? 写一个循环?当然不是吗?apply来帮你!
var array = ['a', 'b'];var elements = [0, 1, 2];array.push.apply(array, elements);console.info(array); // ["a", "b", 0, 1, 2]
4.2 使用apply和内置函数
聪明的apply用法允许你在某些本来需要写成遍历数组变量的任务中使用内建的函数。在接下里的例子中我们会使用Math.max/Math.min来找出一个数组中的最大/最小值。
/* 找出数组中最大/小的数字 */var numbers = [5, 6, 2, 3, 7];/* 应用(apply) Math.min/Math.max 内置函数完成 */var max = Math.max.apply(null, numbers); /* 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */var min = Math.min.apply(null, numbers);/* 代码对比: 用简单循环完成 */max = -Infinity, min = +Infinity;for (var i = 0; i < numbers.length; i++) {if (numbers[i] > max)max = numbers[i];if (numbers[i] < min)min = numbers[i];}
但是当心:如果用上面的方式调用apply,会有超出JavaScript引擎的参数长度限制的风险。当你对一个方法传入非常多的参数(比如一万个)时,就非常有可能会导致越界问题, 这个临界值是根据不同的 JavaScript 引擎而定的(JavaScript 核心中已经做了硬编码 参数个数限制在65536),因为这个限制(实际上也是任何用到超大栈空间的行为的自然表现)是未指定的. 有些引擎会抛出异常。更糟糕的是其他引擎会直接限制传入到方法的参数个数,导致参数丢失。举个例子:如果某个引擎限制了方法参数最多为4个(实际真正的参数个数限制当然要高得多了, 这里只是打个比方), 上面的代码中, 真正通过 apply传到目标方法中的参数为 5, 6, 2, 3 而不是完整的数组。
如果你的参数数组可能非常大,那么推荐使用下面这种策略来处理:将参数数组切块后循环传入目标方法:
function minOfArray(arr) {var min = Infinity;var QUANTUM = 32768;for (var i = 0, len = arr.length; i < len; i += QUANTUM) {var submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)));min = Math.min(submin, min);}return min;}var min = minOfArray([5, 6, 2, 3, 7]);
4.3 使用apply来链接构造器
你可以使用apply来链接一个对象构造器,类似于Java。在接下来的例子中我们会创建一个全局Function 对象的construct方法 ,来使你能够在构造器中使用一个类数组对象而非参数列表。
Function.prototype.construct = function (aArgs) {var oNew = Object.create(this.prototype);this.apply(oNew, aArgs);return oNew;};
注意: 上面使用的Object.create()方法相对来说比较新。另一种可选的方法,请考虑如下替代方法:
Using Object.__proto__:
Function.prototype.construct = function (aArgs) {var oNew = {};oNew.__proto__ = this.prototype;this.apply(oNew, aArgs);return oNew;};
使用闭包:
Function.prototype.construct = function(aArgs) {var fConstructor = this, fNewConstr = function() {fConstructor.apply(this, aArgs);};fNewConstr.prototype = fConstructor.prototype;return new fNewConstr();};
使用 Function 构造器:
Function.prototype.construct = function (aArgs) {var fNewConstr = new Function("");fNewConstr.prototype = this.prototype;var oNew = new fNewConstr();this.apply(oNew, aArgs);return oNew;};
**
5. 数组的其他常用方法
5.1 indexOf(),lastIndexOf(),find()与findIndex()
返回数组中某个元素出现的位置,如果没有找到指定元素则返回-1,lastIndexOf()返回指定元素在数组中出现的最后位置,如果不存在则返回-1.可以看出,indexOf和lastIndexOf实际上分别是正向和反向查找数组元素。
除了indexOf()和lastIndexOf()之外,在JavaScript中用来查找元素的常用方法还有find()和findIndex(),需要注意的是, find和findIndex的查找条件是函数而不是元素内容,这和indexOf以及lastIndexOf是不同的。 find方法返回要查找的对象或者undefined(如果要查找的对象不存在),findIndex方法则和indexOf方法的使用类似,返回元素索引或者-1(如果未找到)
var fruits = ["Banana", "Orange", "Apple", "Mango","Apple"];var a = fruits.indexOf("Apple");console.log(`index of apple is ${a}`); //2var b=fruits.lastIndexOf("Apple");console.log(`last index of apple is ${b}`); //4console.log(fruits.find((item)=>item==="Orange"));//查询条件为函数,返回"Orange"console.log(fruits.findIndex((item)=>item==="Orange"));//1
5.2 slice()方法和splice()方法
slice()方法与splice()方法在使用上有一个重要区别,前者会创建一个新的数组,而后者会改变原有数组。这在编程时需要特别注意。事实上,数组的大部分方法,在使用时都要注意是否会改变原有数组。在函数式编程的理念中,splice()一类会改变原始数组的方法应该尽量避免,因为这会给程序调试、测试带来很多不便。
slice()方法用来读取数组中一部分并返回新的数组。
var fruits = ["Banana", "Orange", "Lemon", "Apple", "Mango"];var citrus = fruits.slice(1,3);//citrus=["Orange","Lemon"],注意,这里包含了索引为1的数组元素,但是不包含数组为3的数组元素。
splice()方法可以用来删除、添加或者替换元素,这取决于对其赋予的参数。splice方法是这样定义的array.splice(index,howmany,item1,item2...itemX)。其中,index表示开始删除/添加元素的位置,howmany表示要删除的元素数量,为0则不删除原有元素,从item1(可选参数)开始表示要插入的元素,不赋值则代表不添加新的元素。可以看出,当
howmany参数为0而存在item参数时,表示从index开始向数组插入元素,当howmany参数不为0而不存在item参数时,表示从index开始删除部分元素,当howmany参数不为0且存在item参数时 ,表示从index开始删除部分元素,再插入item元素。
var fruits = ["Banana", "Orange", "Apple", "Mango"];fruits.splice(2,2);//从第二个元素开始删除两个元素,返回["Banana","Orange"]var fruits = ["Banana", "Orange", "Apple", "Mango"];fruits.splice(2,1,"Lemon","Kiwi");//删除第3个元素并在该位置插入两个元素,返回//["Banana","Orange","Lemon","Kiwi","Mongo"]
5.3 every()方法与some()方法
every()方法测试是不是数组中所有元素都满足某一条件,如果其中有元素不满足则返回false。some()方法测试是不是数组中存在满足某一条件的元素,如果有则返回true,如果所有元素均不满足则返回false()。例如:
const array = [1, 2, 3, 4, 5];//数组中是否有偶数const resSome=array.some((element)=>element%2===0);console.log(resSome);//true//数组元素是不是均>0const resEvery=array.every((element)=>element>0);console.log(resEvery);//true
5.4 toString()方法和join()方法
数组的toString()方法将数组元素转换成一个用逗号连接的字符串并返回,join()方法同样返回一个字符串,但可以指定连接符。例如:
const array1 = [1, 2, 'a', '1a'];console.log(array1.toString());// "1,2,a,1a"console.log(array1.join('-'));// "1-2-a-1a"
5.5 push(),pop(),shift()与unshift()
push(),pop(),shift()与unshift()方法时向数组中添加和删除元素的几个常用方法,在使用时,需要特别注意每个方法返回的对象,以避免误用。
push()方法向数组末尾添加一个或多个元素,并 返回新的长度 。
pop()方法删除数组的最后一个元素,并 返回删除的元素 。
shift()方法删除数组的第一个元素,并返回 删除的元素 。
unshift()方法向数组的开头添加一个或多个元素,并返回 新的长度 。
