数组的方法的总结和使用

前言

JavaScript 中的数组是一个很特别的存在,他不像Java ,专门搞了 List 这样的一整套的东西,JS终端数组完全可以当作栈或队列来使用,数组的四大操作:pop、push、shift、unshift。

我今天写这篇博客,主要是写一篇总结,以备以后查看。

对于数组方法,我们应该关心的只有两个问题,返回值是什么,会不会修改原数组,典型的例子就是 splice() 和 slice() 这两个方法。

接下来,带着这两个问题,我会相对详细的总结一下我们数组原型(数组实例)里面所拥有的方法和属性,另外,常用的方法,我会加上一个常用的例子(应用场景)

ps: 文中有些和数组方法不是太相关的知识,仅供了解,这里就不展开了(实际上也是我太垃圾了0 0,不懂) 具体我会在文末放上相关链接

下面将不再重复这些方法的 callbak 的参数

(find、findIndex、some、filter、every、map,forEach)的callback 被调用时会传入三个参数:当前元素值,元素的索引,原数组。这些方法都不会改变元素组,但是,如果你操作第三个参数,那就不一样了。

数组的属性

length

length 是Array的实例属性。返回或设置一个数组中的元素个数。该值是一个无符号 32-bit 整数,并且总是大于数组最高项的下标。

let arr = [1,2,3]
console.log(arr.length) // 3

length 属性的值是一个 0 到 232-1 的整数。

let arr1 = new Array(4294967296) // 2的32次方 = 4294967296 
// Uncaught RangeError: Invalid array length无效的数组长度
console.log(arr1)

let arr2 = new Array(-100) // 负号
// Uncaught RangeError: Invalid array length无效的数组长度
console.log(arr1)

你还可以通过 length 来截断数组

let arr = [1,2,3,4]
arr.length = 2
console.log(arr) // [1,2]

Array.length 属性的属性特性

属性
writable true
enumerable false
configurable false
  • Writable :如果设置为false,该属性值将不能被修改。
  • Configurable :如果设置为false,删除或更改任何属性都将会失败。
  • Enumerable :如果设置为 true ,属性可以通过迭代器for或for…in进行迭代。

看到这里,估计有人想问,既然 length 属性是可以修改的,那么我们可不可以重定义数组对象的 length 属性呢?答案是可以的,但是会受到一般的重定义限制。并且并不是所有浏览器都允许 Array.length 的重定义。这里就不展开了,如有兴趣请看文末的链接

数组的静态方法

Array.isArray()

在这个方法没出来之前,很多早期类库是通过下列代码来判断的(鸭子判断),文末送上玉伯大佬的链接,希望大家都去了解一下

function isArray(object) {
  return object != null && typeof object === "object" &&
    'splice' in object && 'join' in object
}

直到后来有神人出山,写出了一段神码,此代码一出,天下震惊,引各路类库竞折腰。这代码,不不仅仅解决了数组的问题,而是解决了 isXxx 一类问题。

function isArray(obj) {
  return Object.prototype.toString.call(obj) === "[object Array]"
}

对现代浏览器来说,上面的写法,依旧让各大浏览器引擎的实现者觉得很难受,于是直接有了Array.isArray方法

/**
 * Array.isArray(obj)
 * @param {Object} obj => 需要检测的值
 * ----------------------------------------------
 * 返回值
 * @param {Boolean} 如果对象是 Array, 则为true;否则为false。
 */
 Array.isArray([]) // true
 // 鲜为人知的事实:其实 Array.prototype 也是一个数组。
Array.isArray(Array.prototype) // true

Array.from()

Array.from() 方法从一个类似数组或可迭代对象中创建一个新的数组实例

/**
 * Array.from(arrayLike,mapFn,thisArg)
 * @param {Integer} arrayLike => 想要转换成数组的伪数组对象或可迭代对象。
 * @param {Function} mapFn => 如果指定了该参数,新数组中的每个元素会执行该回调函数。 (可选)
 * @param {Object} thisArg => 可选参数,执行回调函数 mapFn 时 this 对象。 (可选)
 * ----------------------------------------------
 * 返回值
 * @Array 一个新的数组实例
 */

console.log(Array.from('foo')) // ["f", "o", "o"]
console.log(Array.from([1, 2, 3], x => x + x)) // [2,4,6]
Array.from([1],function(x){
    console.log(this) // {a: 1}
    return x
},{a:1})

实际上,通过 Array.from 我们就可以做一些很常见的事了,比如我们的数组去重合并

const combine = (...arg)=>{
     let arr = [].concat.apply([], arg); 
    return Array.from(new Set(arr));
}
let m = [1, 2, 2], n = [2,3,3,4]; 
console.log(combine(m,n)) // [1, 2, 3, 4];     

Array.of()

Array.of() 方法创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。

Array.of() 和 Array 构造函数之间的区别在于处理整数参数:Array.of(7) 创建一个具有单个元素 7 的数组,而 Array(7) 创建一个长度为7的空数组(注意:这是指一个有7个空位的数组,而不是由7个undefined组成的数组)。

/**
 * Array.of(elementN)
 * @param {*} elementN => 任意个参数,将按顺序成为返回数组中的元素。
 * ----------------------------------------------
 * 返回值
 * @param {Array} 新的 Array 实例。
 */
Array.of(7);       // [7] 
Array.of(1, 2, 3); // [1, 2, 3]

Array(7);          // [ , , , , , , ]
Array(1, 2, 3);    // [1, 2, 3]

拷贝填充相关

concat

concat() 方法用于合并两个或多个数组。此方法不会更改原数组,而是返回一个新数组。

有个降维的例子会在 reduce() ,这里就不过讲解了,避免重复

/**
 * old_array.concat(valueN])
 * @param {*} valueN => 将数组和/或值连接成新数组。详情请参阅下文描述。
 * 
 * 返回值
 * @param {Array} 新的 Array 实例
 * 
*/
let arr1 = [1,2,3]
let arr2 = [4,5,6]
console.log(arr1.concat(arr2)) // [1,2,3,4,5,6]
console.log(arr1) // [1,2,3]
console.log(arr2) // [4,5,6]

实际应用中,我们可以用 concat 和 reduce 搭配,实现扁平化数组(降维)

/**
 * 多维数组变成一维数组
 * @param {Array} arr => 数组
 * @param {Boolean} shallow => 数组将只减少一维的嵌套
 */
const flatten =  (arr, shallow) => {
  if (shallow) {
      var arr = arr.reduce((arr, val) => arr.concat(val), [])
  } else {
      var arr = arr.reduce((arr, val) => arr.concat(Array.isArray(val) ? flatten(val) : val), [])
  }
  return arr
}

const arr = [1,[2,[3,4]],5]
console.log(flatten(arr,true)) // [1, 2, [3,4], 5]
console.log(flatten(arr)) // [1, 2, 3, 4, 5]

copyWithin

copyWithin() 方法浅复制数组的一部分到同一数组中的另一个位置,并返回它,而不修改其大小。此方法会更改现有数组

ps:参数target,start和end 必须为整数。如果start为负,则其指定的索引位置等同于length+start,length为数组的长度。end也是如此。(copyWithin,fill等方法的start、end参数同理)

/**
 * arr.copyWithin(target,start,end)
 * @param {Number} target => 0 为基底的索引,复制序列到该位置。如果是负数,target 将从末尾开始计算。
如果 target 大于等于 arr.length,将会不发生拷贝。如果 target 在 start 之后,复制的序列将被修改以符合 arr.length。
 * @param {Number} start => 0 为基底的索引,开始复制元素的起始位置。如果是负数,start 将从末尾开始计算
如果 start 被忽略,copyWithin 将会从0开始复制 (可选)
 * @param {Number} end => 0 为基底的索引,开始复制元素的结束位置。copyWithin 将会拷贝到该位置,但不包括 end 这个位置的元素。如果是负数, end 将从末尾开始计算。如果 end 被忽略,copyWithin 将会复制到 arr.length (可选)
 * 
 * 返回值
 * @param {Array} 改变了的数组
 * 
*/
let arr = [1,2,3,4,5]
console.log(arr.copyWithin(0, 3, 4)) // [4, 2, 3, 4, 5]
console.log(arr) // [4, 2, 3, 4, 5]
// -2表示复制到 ([1, 2, 3, 4, 5].length - 2) 的位置去,这里指3
console.log([1, 2, 3, 4, 5].copyWithin(-2)) // [1, 2, 3, 1, 2]
console.log([1, 2, 3, 4, 5].copyWithin(0, 3)) // [4, 5, 3, 4, 5]
console.log([1, 2, 3, 4, 5].copyWithin(-2, -3, -1)) // [1, 2, 3, 3, 4]

fill

fill() 方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。此方法会更改现有数组

/**
 * arr.fill(value,start,end)
 * @param {*} value => 用来填充数组元素的值。
 * @param {Number} start => 起始索引,默认值为0。(可选)
 * @param {Number} end => 起始索引,默认值为this.length。(可选)
 * 
 * 返回值
 * @param {Array} 修改后的数组
 * 
*/
let arr = [1,2,3,4]
console.log(arr.fill(0, 2, 4)) // [1, 2, 0, 0]
console.log([1, 2, 3].fill(4)) // [4,4,4]
console.log([1, 2, 3].fill(4,1)) // [1,4,4]
// 这里的-1实际上就是 (-1+3) 也就是 2
console.log([1, 2, 3].fill(4,-1)) // [1,2,4]
console.log([1, 2, 3].fill(4,-2,-1)) // [1,4,3]

搜索查找相关

find

find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。

/**
 * arr.find(callback,thisArg)
 * @param {Function} callback => 用来测试每个元素的函数
 * @param {Object} thisArg => 可选参数,执行回调函数 mapFn 时 this 对象。 (可选)
 * 
 * callback 被调用时会传入三个参数:当前元素值,元素的索引,原数组。
 * 
 *  返回值
 * @param {*} 当某个元素通过 callback 的测试时,返回数组中最先通过的值,否则返回 undefined
*/
const arr = [1,2,3,4]
console.log(arr.find(item=>item>2)) // 3
console.log(arr.find(item=>item>6)) // undefined

实际应用场景中,我们可以用对象的属性查找数组里的对象

const people = [{
    name:'gating',
    age:18
},{
    name:'blue',
    age:15
},{
    name:'family',
    age:18
}]
const findName = (arr) => arr.name === 'gating';
console.log(people.find(findName)) // {name: "gating", age: 18}

findIndex

findIndex()方法返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。

/**
 * arr.findIndex(callback,thisArg)
 * @param {Function} callback => 用来测试每个元素的函数
 * @param {Object} thisArg => 可选参数,执行回调函数 mapFn 时 this 对象。 (可选)
 * 
 * callback 被调用时会传入三个参数:当前元素值,元素的索引,原数组。
 * 
 *  返回值
 * @param {Number} 当某个元素通过 callback 的测试时,返回数组中最先通过的值的索引值,否则返回 -1
*/
const arr = [1,2,3,4]
console.log(arr.findIndex(item=>item>2)) // 2
console.log(arr.findIndex(item=>item>6)) // -1

实际应用场景同 find() ,这里就不重复了

indexOf

indexOf()方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。

indexOf 和 lastIndexOf 都是使用 === 来进行判断

/**
 * arr.indexOf(searchElement,fromIndex)
 * @param {*} searchElement => 要查找的元素
 * @param {Number} fromIndex => 开始查找的位置。如果该索引值大于或等于数组长度,意味着不会在数组里查找,返回-1。如果参数中提供的索引值是一个负值,则将其作为数组末尾的一个抵消,即-1表示从最后一个元素开始查找,-2表示从倒数第二个元素开始查找 ,以此类推。 注意:如果参数中提供的索引值是一个负值,并不改变其查找顺序,查找顺序仍然是从前向后查询数组。如果抵消后的索引值仍小于0,则整个数组都将会被查询。其默认值为0. (可选)
 * 
 *  返回值
 * @param {Number} 首个被找到的元素在数组中的索引位置; 若没有找到则返回 -1
*/

const arr = ['a','b','c','a']
console.log(arr.indexOf('a')) // 0
console.log(arr.indexOf('a',-1)) // 3
console.log(arr.indexOf('d')) // -1

实际应用场景中,我们不可能之只找出一个同名元素的索引值,我们可能需要找出同名的元素的所有位置,于是乎

// 存放索引的数组
const indices = [];
const arr = ['a','b','c','a']
// 要查找的元素
const searchElement = 'a'
// 当前的索引
let idx = arr.indexOf(searchElement);
while (idx != -1) {
  indices.push(idx);
  idx = arr.indexOf(searchElement, idx + 1);
}
console.log(indices) // [0, 3]

lastIndexOf

lastIndexOf() 方法返回指定元素(也即有效的 JavaScript 值或变量)在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。

/**
 * arr.lastIndexOf(searchElement,fromIndex = arr.length - 1)
 * @param {*} searchElement => 要查找的元素
 * @param {Number} fromIndex => 从此位置开始逆向查找。默认为数组的长度减 1,即整个数组都被查找。如果该值大于或等于数组的长度,则整个数组会被查找。如果为负值,将其视为从数组末尾向前的偏移。即使该值为负,数组仍然会被从后向前查找。如果该值为负时,其绝对值大于数组长度,则方法返回 -1,即数组不会被查找。 (可选)
 * 
 *  返回值
 * @param {Number} 数组中最后一个元素的索引,如未找到返回-1
*/

const arr = ['a','b','c','a']
console.log(arr.lastIndexOf('a')) // 0
console.log(arr.lastIndexOf('a',-1)) // 3
console.log(arr.lastIndexOf('d')) // -1

实例:查找所有元素

const indices = []
const array = ['a', 'b', 'a', 'c', 'a', 'd']
const element = 'a'
let idx = array.lastIndexOf(element)

while (idx != -1) {
  indices.push(idx);
  idx = (idx > 0 ? array.lastIndexOf(element, idx - 1) : -1);
}

console.log(indices) // [4, 2, 0];

注意,我们要单独处理idx==0时的情况,因为如果是第一个元素,忽略了fromIndex参数则第一个元素总会被查找。这不同于indexOf方法

includes

includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。

  1. 如果fromIndex 大于等于数组长度 ,则返回 false 。该数组不会被搜索

  2. 如果 fromIndex 为负值,计算出的索引将作为开始搜索searchElement的位置。如果计算出的索引小于 0,则整个数组都会被搜索。

/**
 * arr.includes(searchElement,fromIndex = 0)
 * @param {*} searchElement => 要查找的元素
 * @param {Number} fromIndex => 从该索引处开始查找 searchElement。如果为负值,则按升序从 array.length - fromIndex 的索引开始搜索。默认为 0。 (可选)
 * 
 *  返回值
 * @param {Boolean} 一个 Boolean 值
*/
const arr = [1,2,3,NaN]
console.log(arr.includes(1, -100)) // true
console.log(arr.includes(1, 5)) // false

实际的应用场景,通过可以解决我们找出两个数组的 数组交集 和 数组差集

// 数组交集
const intersection = (arr1, arr2) => arr1.filter(v => arr2.includes(v))
// 数组差集
const difference = (arr1, arr2) => [...arr1, ...arr2].filter(v => !arr1.includes(v) || !arr2.includes(v))

const arr1 = [1,2,3]
const arr2 = [2,3,4]
console.log(intersection(arr1,arr2)) // [2, 3]
console.log(difference(arr1,arr2)) // [1, 4]

操作相关

slice

slice() 方法返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝。此方法不会更改原数组

/**
 * Array.slice(begin,end)
 * @param {Number} begin => 从该索引处开始提取原数组中的元素(从0开始)。(可选)
 * @param {Number} end => 在该索引处结束提取原数组元素(从0开始)。slice会提取原数组中索引从 begin 到 end 的所有元素(包含begin,但不包含end)。(可选)
 * ----------------------------------------------
 * 返回值
 * @param {Array} 一个含有提取元素的新数组
 */
const arr = [1,2,3,4,5]
console.log(arr.slice()) // [1,2,3,4,5]
console.log(arr.slice(2)) // [3,4,5]
console.log(arr.slice(2, 4)) // [3,4]

// -1表示复制到 ([1, 2, 3, 4, 5].length - 1) 的位置去,这里指4
console.log(arr.slice(-1)) // [5]
console.log(arr.slice(-2,-1)) // [4]

实际应用中,slice 就可以做很多事情啦,比如说,我们的数组切割或者生成二维数组

/**
 * 返回一个根据subArrayNum的二维数组
 * @param {Array} arr => 一维数组
 * @param {Number} subArrayNum => 每组元素的个数
 */
const chunk = (arr, subArrayNum) => {
    const result = [];
    for (let i = 0; i < arr.length; i += subArrayNum) {
        result.push(arr.slice(i, i + subArrayNum));
    }
    return result;
}
console.log(chunk([1,2,3,4],2))  // [[1,2], [3,4]]

/**
 * 创建一个数组切片, 从array数组的起始元素开始提取n个元素。
 * @param {Array} arr => 要检索的数组
 * @param {Number} n => 要提取的元素个数
 */
const take = (arr, n) => arr.slice(0, n ? n : 1)
console.log(take([1,2,3,4],2)) // [1, 2]

splice

splice()方法通过删除现有元素和/或添加新元素来修改数组,并以数组返回原数组中被修改的内容。此方法会更改现有数组

/**
 * Array.splice(start,deleteCount,itemN)
 * @param {Number} start => 指定修改的开始位置(从0计数)。如果超出了数组的长度,则从数组末尾开始添加内容;如果是负值,则表示从数组末位开始的第几位(从-1计数);如果负数的绝对值大于数组的长度,则表示开始位置为第0位。
 * @param {Int} deleteCount  => 整数,表示要移除的数组元素的个数。如果deleteCount被省略,则其相当于(arr.length - start)。(可选) 
 * @param {*} itemN  => 要添加进数组的元素,从start 位置开始。如果不指定,则 splice() 将只删除数组元素。(可选)
 * ----------------------------------------------
 * 返回值
 * @param {Array} 由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。
 */

const arr = [1,3]
const res = arr.splice(1, 0, 2)
console.log(arr) // [1,2,3]
console.log(res) // []

const result = arr.splice(2, 1)
console.log(arr) // [1,2]
console.log(result) // [3]


// -1表示复制到 ([1,2].length - 1) 的位置去,这里指1
arr.splice(-1, 1)
console.log(arr) // [1]

// 只有一个参数时 fruits.splice(2) 相当于 fruits.splice(2,fruits.length-1)
const fruits = ['apple','banana','orange','pear']
const res1 = fruits.splice(2)
console.log(fruits) // ["apple", "banana"]
console.log(res1) // ["orange", "pear"]

因为 js 中只有 splice 这个方法删除数组元素,并且会修改数组长度,所以他的实际应用场景还挺多的0 0,但是我没有太好的例子可以距离

push

push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。 此方法会更改现有数组

/**
 * Array.push(elementN)
 * @param {*} elementN => 被添加到数组末尾的元素。
 * ----------------------------------------------
 * 返回值
 * @param {Number} 当调用该方法时,新的 length 属性值将被返回。
 */

const arr = [1,2,3]
console.log(arr.push(4)) // 4
console.log(arr) // [1,2,3,4]

push、pop、shift、unshift 方法有意具有通用性。该方法和 call() 或 apply() 一起使用时,可应用在类似数组的对象上。这个比较有意义,所以单独在这里送上MDN的链接,当然例子也是MDN的O(∩_∩)O

像数组一样使用对象这个例子 push、pop、shift、unshift 四个方法写法很类似,这里就重复写了

// 合并两个数组
var vegetables = ['parsnip', 'potato']
var moreVegs = ['celery', 'beetroot']
// 将第二个数组融合进第一个数组
// 相当于 vegetables.push('celery', 'beetroot')
Array.prototype.push.apply(vegetables, moreVegs)
console.log(vegetables)  // ['parsnip', 'potato', 'celery', 'beetroot']


// 像数组一样使用对象
var obj = {
    length: 0,
    addElem: function addElem (elem) {
        // obj.length is automatically incremented 
        // every time an element is added.
        [].push.call(this, elem);
    },
    popElem: function addElem () {
        // obj.length is automatically incremented 
        // every time an element is added.
        return [].pop.call(this);
        
    }
}
obj.addElem({})
obj.addElem({a:1})
console.log(obj.length) // 2
console.log(obj.popElem()) // {a: 1}
console.log(obj.length) // 1

pop

pop()方法从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。此方法会更改现有数组

如果你在一个空数组上调用 pop(),它返回 undefined。

/**
 * Array.pop()
 * ----------------------------------------------
 * 返回值
 * @param {*} 从数组中删除的元素(当数组为空时返回undefined)。
 */
const arr = [1,2,3]
console.log(arr.pop()) // 3
console.log(arr) // [1,2]

shift

shift() 方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。此方法会更改现有数组

/**
 * Array.shift()
 * ----------------------------------------------
 * 返回值
 * @param {*} 从数组中删除的元素(当数组为空时返回undefined)。
 */
const arr = [1,2,3]
console.log(arr.shift()) // 1
console.log(arr) // [2,3]

unshift

unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度。此方法会更改现有数组

/**
 * Array.unshift(elementN)
 * @param {*} elementN => 要添加到数组开头的元素。
 * ----------------------------------------------
 * 返回值
 * @param {Number} 当一个对象调用该方法时,返回其 length 属性值。
 */
const arr = [3,4,5]
console.log(arr.unshift(1,2)) // 5
console.log(arr) // [1,2,3,4,5]

但实际上,我们不建议用 unshift 方法往前面添加元素,为什么呢?从原理就可以知道,unshift()的效率是较低的。原因是,它每添加一个元素,都要把现有元素往下移一个位置。

下面我们简单测试一下,unshift 和 push 的性能区别

console.time('unshift所用时间')
const unshift = []
for (let i = 0; i < 10000; i++) {   
  unshift.unshift(i)
}
console.timeEnd('unshift所用时间')

console.time('push所用时间')
const push = []
for (let i = 0; i < 10000; i++) {   
  push.push(i)
}
console.timeEnd('push所用时间')

console.time('reverse所用时间')
push.reverse(); 
console.timeEnd('reverse所用时间')

大家可是复制一下这段代码,在我们数据量不是特别大的时候,他们的效率差别几十倍,因此,平时还是要慎用unshift(),特别是对大数组。此方法会更改现有数组

但是我们可以通过 reverse 和 push 的方法实现我们 unshift ,比如

const arr = [3,4,5]
console.log(arr.unshift(1,2)) // 5
console.log(arr) // [1,2,3,4,5]

const arr2 = [3,4,5]
// 从前一个代码知道reverse的性能也很快
arr2.reverse().push(2,1)
arr2.reverse()
console.log(arr2) // [1,2,3,4,5]

sort

sort() 方法用原地算法对数组的元素进行排序,并返回数组。排序算法现在是稳定的。默认排序顺序是根据字符串Unicode码点。此方法会更改现有数组

/**
 * Array.sort(compareFunction)
 * @param {*} compareFunction => 用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符的Unicode位点进行排序。(可选)
 * ----------------------------------------------
 * 返回值
 * @param {Array} 排序后的数组。请注意,数组已原地排序,并且不进行复制。
 */
const months = ['March', 'Jan', 'Feb', 'Dec'];
months.sort() 
console.log(months) // ["Dec", "Feb", "Jan", "March"]

如果没有指明 compareFunction ,那么元素会按照转换为的字符串的诸个字符的Unicode位点进行排序。例如 “Banana” 会被排列到 “cherry” 之前。当数字按由小到大排序时,9 出现在 80 之前,但因为(没有指明 compareFunction),比较的数字会先被转换为字符串,所以在Unicode顺序上 “80” 要比 “9” 要靠前。

所以我们怎么解决这个问题了,当然要使用我们的比较函数啦,而且通过我们的比较函数,可以实现很多我们需要的效果

const arr = ["1",2,"11",3,"21"]
console.log(arr.sort()) // ["1", "11", 2, "21", 3]

// 通过比较函数解决该问题
console.log(arr.sort((a,b)=>a-b)) // ["1", 2, 3, "11", "21"]


// 再比如,我们可以通过比较函数实现对数组对象的排序
const people = [{name:'gating',age:18},{name:'family',age:16},{name:'blue',age:21}]
people.sort((a,b)=>a.age - b.age) // [{name:'family',age:16},{name:'gating',age:18},{name:'blue',age:21}]

// 当然,通过 sort + Math.random 可以很巧妙的实现数组洗牌(数组乱序)的算法(当然,这个也是不推荐的,不过他可以用在小项目中,当作一个小技巧)
const numbers = [1,2,3]
numbers.sort(()=>Math.random()-0.5) // 多尝试几次,看看每次出来的结果是不是不一样的

关于数组乱序,正确的解法应该是 Fisher–Yates Shuffle,复杂度 O(n)。其实它的思想非常的简单,遍历数组元素,将其与之前的任意元素交换。因为遍历有从前向后和从后往前两种方式,所以该算法大致也有两个版本的实现。

这里送上两个方法,具体的请参考文末的数组乱序文章

// 从后往前的版本
const shuffle = (array) => {
    var _array = array.concat();

    for (var i = _array.length; i--;) {
        var j = Math.floor(Math.random() * (i + 1));
        var temp = _array[i];
        _array[i] = _array[j];
        _array[j] = temp;
    }

    return _array;
}
// 从前往后的版本
const shuffle =  (a) => {
    var length = a.length;
    var shuffled = Array(length);

    for (var index = 0, rand; index < length; index++) {
        rand = ~~(Math.random() * (index + 1));
        if (rand !== index)
            shuffled[index] = shuffled[rand];
        shuffled[rand] = a[index];
    }

    return shuffled;
}

reverse

reverse() 方法将数组中元素的位置颠倒。第一个数组元素成为最后一个数组元素,最后一个数组元素成为第一个。此方法会更改现有数组

/**
 * Array.reverse(compareFunction)
 * ----------------------------------------------
 * 返回值
 * @param {Array} 颠倒后的数组
 */
const months = ['March', 'Jan', 'Feb', 'Dec'];
months.reverse() 
console.log(months) // ["Dec", "Feb", "Jan", "March"]

join

join() 方法将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串。此方法不会更改原数组

/**
 * Array.reverse(separator)
 * @param {String} 如果需要(separator),将分隔符转换为字符串。如果省略(),数组元素用逗号分隔。默认为 ","。
 * ----------------------------------------------
 * 返回值
 * @param {String} 一个所有数组元素连接的字符串。如果 arr.length 为0,则返回空字符串
 */
const arr = [1,2,3]
console.log(arr.join()) // 1,2,3
console.log(arr.join('-')) // 1-2-3

迭代(循环遍历)相关

every

every() 方法测试数组的所有元素是否都通过了指定函数的测试。此方法不会更改原数组

/**
 * arr.every(callback,thisArg)
 * @param {Function} callback => 用来测试每个元素的函数
 * @param {Object} thisArg => 可选参数,执行回调函数 mapFn 时 this 对象。 (可选)
 * 
 * callback 被调用时会传入三个参数:当前元素值,元素的索引,原数组。
 * 
 * 返回值
 * @param {Boolean} 为true时,所有元素是否都通过了指定函数的测试,否则就是没有通过
*/
const arr = [1,2,4,2,4,56]
console.log(arr.every(item=>item<10)) // false

some

some() 方法测试数组中的某些元素是否通过由提供的函数实现的测试。此方法不会更改原数组

some() 和 every() 的区别在于every是数组所有元素都需要通过测试才返回true,而some只需要有一个通过测试就返回true

对于放在空数组上的任何条件,此方法返回false。

/**
 * arr.some(callback,thisArg)
 * @param {Function} callback => 用来测试每个元素的函数
 * @param {Object} thisArg => 可选参数,执行回调函数 mapFn 时 this 对象。 (可选)
 * 
 * callback 被调用时会传入三个参数:当前元素值,元素的索引,原数组。
 * 
 * 返回值
 * @param {Boolean} 为true时,有其中一个或多个元素是否都通过了指定函数的测试,否则就是没有通过
*/
const arr = [1,2,4,2,4,56]
console.log(arr.some(item=>item<10)) // true

实际的应用场景,可以实现类似于 includes 的功能 (这里或许你会问,为啥实现类似 includes 的功能,直接用不就好了吗?

因为 includes 的兼容性太差了,IE是不兼容的,但是 some 在IE9下却可以使用,所以,你懂的

var fruits = ['apple', 'banana', 'mango', 'guava'];

function checkAvailability(arr, val) {
  return arr.some(arrVal => val === arrVal);
}

checkAvailability(fruits, 'kela');   // false
checkAvailability(fruits, 'banana'); // true

forEach

forEach() 方法对数组的每个元素执行一次提供的函数。

for 和 forEach 的区别,forEach中已删除或者未初始化的项将被跳过(例如在稀疏数组上),而 for 不会

注意: 没有办法中止或者跳出 forEach 循环,除了抛出一个异常。

/**
 * arr.forEach(callback,thisArg)
 * @param {Function} callback => 用来测试每个元素的函数
 * @param {Object} thisArg => 可选参数,执行回调函数 mapFn 时 this 对象。 (可选)
 * 
 * callback 被调用时会传入三个参数:当前元素值,元素的索引,原数组。
 * 
 * 返回值
 * @param {undefined} undefined
*/
const arr1 = [1,2,3]
arr1.forEach(item=>{
  console.log(item) // 1 2 3
})

// for 和 forEach 的区别
const arr2 = [1,2,,4]
for (let index = 0; index < arr2.length; index++) {
    console.log(arr2[index]) // 1 2 undefined 4
}

arr2.forEach(item=>{
  console.log(item) // 1 2 4
})

实际的应用场景,我们可以通过 forEach 实现对对象的复制,当然,这只是对象复制的其中一种方式

const copy = obj => {
  const copy = Object.create(Object.getPrototypeOf(obj))
  const propNames = Object.getOwnPropertyNames(obj)
  propNames.forEach(function(name) {
    const desc = Object.getOwnPropertyDescriptor(obj, name);
    Object.defineProperty(copy, name, desc);
  })
  return copy;
}

const obj1 = {name:'gating'}
const obj2 = copy(obj1)
console.log(obj2) // {name:'gating'}

map

map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。此方法不会更改原数组

/**
 * arr.map(callback,thisArg)
 * @param {Function} callback => 用来测试每个元素的函数
 * @param {Object} thisArg => 可选参数,执行回调函数 mapFn 时 this 对象。 (可选)
 * 
 * callback 被调用时会传入三个参数:当前元素值,元素的索引,原数组。
 * 
 * 返回值
 * @param {Array} 一个新数组,每个元素都是回调函数的结果。
*/

const arr = [1,2,3]
console.log(arr.map(item=>item*2)) // [2,4,6]

map 的应用场景很多,比如我想给数组增加一个id的属性,再比如可以重新格式化我们的数组

const arr = [{name:'gating'},{name:'family'}]
const res = arr.map((item,index)=>{
  item.id = index
  return item
})
console.log(res) // [{name:'gating',id:0},{name:'family',id:1}]

由一条面试题引发的思考

通常情况下,map 方法中的 callback 函数只需要接受一个参数,就是正在被遍历的数组元素本身。但这并不意味着 map 只给 callback 传了一个参数。

// 下面的语句返回什么呢:
["1", "2", "3"].map(parseInt);
// 你可能觉的会是[1, 2, 3]
// 但实际的结果是 [1, NaN, NaN]

// 通常使用parseInt时,只需要传递一个参数.
// 但实际上,parseInt可以有两个参数.第二个参数是进制数.
// 可以通过语句"alert(parseInt.length)===2"来验证.
// map方法在调用callback函数时,会给它传递三个参数:当前正在遍历的元素, 
// 元素索引, 原数组本身.
// 第三个参数parseInt会忽视, 但第二个参数不会,也就是说,
// parseInt把传过来的索引值当成进制数来使用.从而返回了NaN.

function returnInt(element) {
  return parseInt(element, 10);
}

['1', '2', '3'].map(returnInt); // [1, 2, 3]
// 意料之中的结果

// 也可以使用简单的箭头函数,结果同上
['1', '2', '3'].map( str => parseInt(str) );

// 一个更简单的方式:
['1', '2', '3'].map(Number); // [1, 2, 3]
// 与`parseInt` 不同,下面的结果会返回浮点数或指数:
['1.1', '2.2e2', '3e300'].map(Number); // [1.1, 220, 3e+300]

filter

filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。此方法不会更改原数组

/**
 * arr.filter(callback,thisArg)
 * @param {Function} callback => 用来测试每个元素的函数
 * @param {Object} thisArg => 可选参数,执行回调函数 mapFn 时 this 对象。 (可选)
 * 
 * callback 被调用时会传入三个参数:当前元素值,元素的索引,原数组。
 * 
 *  返回值
 * @param {Array} 过滤后的数组
*/
const arr = [{sex:'male',name:'gating'},{sex:'female',name:'blue'},{sex:'male',name:'family'}]
// 过滤掉性别为 male 的人
console.log(arr.filter(item=>item.sex==='female'))

实际的应用场景,通过可以 filter 过滤掉我们不需要的值

const delListRep = (arr, key) => {
    var keys = []
    for (let i = 0; i < arr.length; i++) {
        keys.push(arr[i][key])
    }
    return arr.filter((ele, i, arr) => keys.indexOf(ele[key]) == i)
}
const arr = [{sex:'male',name:'gating'},{sex:'female',name:'blue'},{sex:'male',name:'gating'}]
console.log(delListRep(arr,'sex')) //  [{sex:'male',name:'gating'},{sex:'female',name:'blue'}]

reduce

reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。不会更改原数组

如果没有提供initialValue,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue,从索引0开始。

/**
 * arr.filter(callback,initialValue)
 * @param {Function} callback => 执行数组中每个值的函数
 * @param {*} initialValue => 作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。 (可选)
 * 
 * callback 被调用时会传入三个参数:accumulator,currentValue,currentIndex,array
 * 
 * accumulator => 累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue(见于下方)。
 * currentValue => 数组中正在处理的元素。
 * currentIndex => 数组中正在处理的当前元素的索引。 如果提供了initialValue,则起始索引号为0,否则为1。 (可选)
 * array => 调用reduce()的数组。 (可选)
 * 
 *  返回值
 * @param {*} 函数累计处理的结果
*/
const arr = [1, 2, 3, 4];
const reducer = arr.reduce((accumulator, currentValue) => accumulator + currentValue)
console.log(reducer) // 10
console.log(arr) // [1, 2, 3, 4]

const res = arr.reduce((accumulator, currentValue) => accumulator + currentValue,5)
console.log(res) // 15

// initialValue 有值 和 没有值的区别
// 因为 reduce 里,如果没有提供初始值,那么 accumulator 会默认使用数组的第一个元素,因为取了数组的第一个元素了,那么下标当然没有必要再从0开始了
arr.reduce((accumulator, currentValue, index) => {
  console.log(index) // 0 1 2 3
})

arr.reduce((accumulator, currentValue, index) => {
  console.log(index) // 1 2 3
},5)

看到了 reduce 的用法,实际上也可以想到,reduce 的参数这么丰富,是不是可以做很多我们想要的事,我想说,是的,在实际应用中,reduce 能做到的事,比我们想象中的还要多

比如,数组扁平化、数组去重、统计数组中每个元素出现的次数、根据属性对Object分类等等等等这种很cool的事,接下来,我们就一一用 reduce 实现吧

reduce 很重要,请务必掌握这个方法 ps:字符串的 replace 也很重要哦!!!也要掌握

/**
 * 根据所提供的字符分组
 * @param {Array} arr => 数组
 * @param {String} key => 需要分组的字段 
 */
function groupBy(arr, key) {
    return arr.reduce(function (newObj, obj) {
        if (!newObj[obj[key]]) {
            newObj[obj[key]] = [];
            newObj[obj[key]].push(obj);
        } else {
            newObj[obj[key]].push(obj);
        }
        return newObj;
    }, {});
}

const people = [
  { name: 'gating', age: 18 },
  { name: 'family', age: 16 },
  { name: 'blue', age: 20 }
];
const groupedPeople = groupBy(people, 'age') // {16: Array(1), 18: Array(1), 20: Array(1)}


// 统计数组中每个元素出现的次数 Object同理
// 这里用 Map 是为了避免 数字1 和 字符串1 同名键值导致的bug
const names = ['gating', 'family', 'gating', 'blue', 'family']
const countNum1 = (arr) => arr.reduce((m, x)=> m.set(x, (m.get(x) || 0) + 1), new Map())
const allName = countNum1(names) // {"gating" => 2, "family" => 2, "blue" => 1}


// 数组去重
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]

其实 reduce 还可以做很多更牛逼的事,看到这里,你是不是也想用 reduce 做更多强大的事呢?那就赶紧把 reduce 学会

reduceRight

reduceRight() 方法接受一个函数作为累加器(accumulator)和数组的每个值(从右到左)将其减少为单个值。不会更改原数组

/**
 * arr.filter(callback,initialValue)
 * @param {Function} callback => 执行数组中每个值的函数
 * @param {*} initialValue => 作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。 (可选)
 * 
 * callback 被调用时会传入三个参数:accumulator,currentValue,currentIndex,array
 * 
 * accumulator => 累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue(见于下方)。
 * currentValue => 数组中正在处理的元素。
 * currentIndex => 数组中正在处理的当前元素的索引。 如果提供了initialValue,则起始索引号为0,否则为1。 (可选)
 * array => 调用reduce()的数组。 (可选)
 * 
 *  返回值
 * @param {*} 函数累计处理的结果
*/

const arr = [[0, 1], [2, 3], [4, 5]].reduceRight(
  (accumulator, currentValue) => accumulator.concat(currentValue)
)
console.log(arr) // [4, 5, 2, 3, 0, 1]

reduce 与 reduceRight 之间的区别

const arr = ['1', '2', '3', '4', '5']; 
const left  = arr.reduce((prev, cur)=>prev + cur); 
const right = arr.reduce((prev, cur)=>prev + cur); 
console.log(left);  // "12345"
console.log(right); // "54321"

由于 reduceRight 和 reduce 区别不是很大,这里的例子就参考 reduce 就行了

迭代相关(生成一个迭代器)

暂时没有太多的应用场景,不详细讲解

entries

entries() 方法返回一个新的Array Iterator对象,该对象包含数组中每个索引的键/值对。

/**
 * arr.entries()
 * 返回值
 * @param {Iterator} 一个新的 Array 迭代器对象
*/
const arr1 = [1,2,3]
const iterator1 = arr1.entries();

// 迭代器通用的两种迭代方式(entries、keys、value同样)
// 1. for of 循环
for (let number of iterator1) {
    console.log(number) // [0, 1] , [1, 2] , [2, 3] 输出三次
}

const arr2 = [1,2,3]
const iterator2 = arr2.entries();
console.log(iterator2.next().value); // [0, 1]
console.log(iterator2.next().value); // [1, 2]
console.log(iterator2.next().value); // [2, 3]
console.log(iterator2.next().value); // undefined,并且 iterator2.next().done 变成true

keys

keys() 方法返回一个包含数组中每个索引键的Array Iterator对象。

/**
 * arr.keys()
 * 返回值
 * @param {Iterator} 一个新的 Array 迭代器对象
*/
const arr = ['a','b','c']
const iterator = arr.keys();
  
for (let key of iterator) {
  console.log(key); //  0 1 2
}

values

values() 方法返回一个新的 Array Iterator 对象,该对象包含数组每个索引的值

/**
 * arr.values()
 * 返回值
 * @param {Iterator} 一个新的 Array 迭代器对象
*/
const arr = ['a','b','c']
const iterator = arr.values()
for (let value of iterator) {
  console.log(key) // 'a' 'b' 'c'
}

不知名的两个方法

toString

toString() 返回一个字符串,表示指定的数组及其元素

/**
 * arr.toString()
 * 返回值
 * @param {String} 一个表示指定的数组及其元素的字符串。
*/
const arr = ['a','b','c']
console.log(arr.toString()) // 'a','b','c'

toLocaleString

toLocaleString() 返回一个字符串表示数组中的元素。数组中的元素将使用各自的 toLocaleString 方法转成字符串,这些字符串将使用一个特定语言环境的字符串(例如一个逗号 “,”)隔开。

/**
 * arr.toLocaleString(locales,options)
 * @param {String?}  locales => 带有BCP 47语言标记的字符串或字符串数组,关于locales参数的形式与解释,请看Intl页面。(可选)
 * @param {Object}  options => 一个可配置属性的对象,对于数字 Number.prototype.toLocaleString(),对于日期Date.prototype.toLocaleString() (可选)
 * 返回值
 * @param {String} 表示数组元素的字符串
*/
var array1 = [1, 'a', new Date('21 Dec 1997 14:12:00 UTC')]
var localeString = array1.toLocaleString('en', {timeZone: "UTC"})

console.log(localeString) // 1,a,12/21/1997, 2:12:00 PM


var prices = ['¥7', 500, 8123, 12] 
prices.toLocaleString('ja-JP', { style: 'currency', currency: 'JPY' }) // "¥7,¥500,¥8,123,¥12"

这个方法我用的真不多,所以这里还是直接放上MDN的地址供大家了解,或许以后就用到了呢?

文中某些知识点参考链接

重定义数组对象的 length 属性

MDN地址,文中很多例子都采用于MDN,推荐

数组类型判断,玉伯大佬博客,推荐

avaScript 数组乱序

数组的完全随机排列

鉴于 reduce 和 reduceRight 太过好用(太过牛逼),这里提供他们两个的MDN地址

reduce

reduceRight

总结

针对于我们一开始关心的两个问题,我在这里做一个小小的总结

ps: 对于测试性的方法,这里就不做展开了,因为用不上= =

方法名 返回值 是否修改原数组
concat 返回合并后的 Array 实例
copyWithin 改变了的数组
entries 一个新的 Array 迭代器对象( [key,value])
every 布尔值,表示数组中所有元素是否通过 every 测试
fill 修改后的数组
filter 一个新的通过测试的元素的集合的数组,如果没有通过测试则返回空数组
find 当某个元素通过 callback 的测试时,返回数组中的一个值,否则返回 undefined
findIndex 返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。
forEach undefined
includes 布尔值, 判断一个数组是否包含一个指定的值
indexOf 首个被找到的元素在数组中的索引位置; 若没有找到则返回 -1
join 一个所有数组元素连接的字符串。如果 arr.length 为0,则返回空字符串
keys 一个新的 Array 迭代器对象( [key])
lastIndexOf 数组中最后一个元素的索引,如未找到返回-1
map 一个新数组,每个元素都是回调函数的结果
pop 从数组中删除的元素(当数组为空时返回undefined)
push 当调用该方法时,新的 length 属性值将被返回。
reduce 函数累计处理的结果
reduceRight 执行之后的返回值
reverse 返回颠倒后的 Array
shift 返回被删除元素的值
slice 一个含有提取元素的新数组
some 布尔值,表示数组中至少有一个元素是否通过 every 测试
sort 排序后的数组
splice 由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。
toLocaleString 表示数组元素的字符串
toString 个表示指定的数组及其元素的字符串
unshift 返回其 length属性值。
values 一个新的 Array 迭代器对象( [value])

最后,感谢各位观众老爷观看啦O(∩_∩)O

这是我的原创文章,如果觉得不错,可以打个赏~
0%