JavaScript即学即用教程[3]-数组操作

数组的应用非常广泛,在大多数的数据结构中,都要以数组作为基础来进行实现。所以数组的地位举足轻重。

数组类型判定

  • Array.isArray(arr), 返回true。 无论你是否改掉 arr.__proto__ ,该方法都可用。
  • arr instanceof Array, 返回true, 如果你硬去改掉了 arr.__proto__ ,则失效。
  • Object.prototype.toString.call(arr), 会返回’[object Array]’
  • arr.constructor.name , 在你没有修改 arr.__proto__的情况下,是可以这么玩的,相当于Array.name,会返回’Array’

其中instanceof,constructor,在前端多窗口对象之间进行判定时可能会因不同的宿主环境而导致判定失败,所以不靠谱。
最靠谱的就是Array.isArray()

数组遍历

1
2
3
arr.forEach
或循环
for(var i = 0; i < arr.length; i++)

for in 作为遍历对象的key的方法也可以遍历数组。(因为数组的下标相当于是对象的key),但是如同对象遍历一样,for in 遍历时对key是没有固定的次序排列的,无法保证key出现的顺序跟数组下标完全一致的哦。所以也不靠谱。

函数式方法

arr.some() 判断数组是否包含某种元素
arr.every() 判断数组中是否全部是符合某种条件的元素
arr.filter() 过滤出符合条件的一些项目来
arr.forEach() 遍历每一项做一个操作
arr.map() 对数组每一项做一个映射

以上方法,都不会改变原始数组!

API

  • toString, 变成逗号间隔的字符串
  • join,可以用自己的分隔符构造成字符串
  • [不改变原数组] concat数组连接, [].concat(‘’, [], [])
  • [不改变原数组] slice, 沾出一个新数组,按照给出的索引,类似String的slice。注意slice的时候包含前面坐标,但不包含后面那个坐标(又叫前包后不包)。
  • [改变原数组]截断数组,可以直接修改数组的length属性。这应该是js特有的,不过其他基于顺序存储的语言应该也可以实现这样的功能。
  • [改变原数组]push, pop, 栈方法。 push–shift, unshift–pop, 队列方法
  • [改变原数组]splice,删除或插入数组。会改变原数组,返回值为删掉的项目组成的数组,没删则返回空数组。
  • [改变原数组]sort, reverse,排序和反转数组的项目。

下面是几个跟查找有关的函数:

  • 查找等于某个值的坐标: indexOf(item, index), 第二个参数表示起始查找位置。
  • 查找满足条件的坐标:findIndex()
  • 查找满足条件的值: find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
  • some: 判断是否有满足条件的元素

不由让我们想起字符串里的一些查找相关的函数:

  • includes() 判断是否有等于某个值的子串
  • indexOf 查找等于某个值的坐标
  • search() 查找满足某正则条件的坐标

记住 includes 是字符串的函数;他可以用search和indexOf实现。而在数据中没有include这样的函数,但一样可以用indexOf, some, find, findIndex任意一个来实现。

应用

快速复制一个数组出来

方法1是用concat拼接函数,无参数

1
return arr.concat()

方法2是利用slice无参数的函数。
1
return arr.slice()

过滤掉数组中为空或者空字符的项,留下不为空的项目

相当于过滤掉数组中逻辑判定为false的项目,因此就简单的这样写了:

1
2
3
arr.filter(item=>{
return item
})

凡是为空的item都会返回false,filter函数会自动放弃他们。

数组求和

以下方法实现都不太严谨,因为一开始都搞了个默认值0,包括reduce也是用默认值0开始算的。一旦数组为空,函数还是返回0. 当然,这不是重点,重点是去学习如何实现的这个算法。

利用reduce方法

1
2
3
arr.reduce(function (preV, item, index, arr) {
return preV + item
}, 0)

forEach方法

1
2
3
4
5
6
7
function sum (arr) {
var rel = 0
arr.forEach(function (item, index, arr) {
rel += item
})
return rel
}

递归。思想: 数组的和,等于数组第一项加上后面所有项的和。停止条件: 数组只有一项,则这一项的值就是数组的和。

1
2
3
4
5
6
7
8
9
10
11
12
function sum (arr) {
var len = arr.length
if (len == 0) {
return 0 // 只有arr为空的时候才走这,一般不会走到这里
}
else if (len == 1) {
return arr[1] // 这是递归的停止条件
}
else {
return arr[0] + sum(arr.slice(1))
}
}

常规循环

1
2
3
4
5
6
7
function sum (arr) {
var rel = 0
for (var i = arr.length-1; i >= 0; i--) {
rel += arr[i]
}
return rel
}

奇技淫巧: 利用eval. 相当于构造了一个代码 “a+b+c+d”,然后交给eval执行,并返回出结果

1
2
3
function sum (arr) {
return eval(arr.join('+'))
}

移除数组中的元素,返回新数组(不改变原数组)

其实这个问题就是说不适用splice的情况下,如何返回一个剔除掉某些元素的新数组。
方法1: 先slice一个副本,再对副本splice

1
2
3
4
5
6
7
8
9
function remove (arr, item) {
var copyArr = arr.slice(0)
var itemIndex = copyArr.indexOf(item)
if (itemIndex > -1) {
copyArr.splice(itemIndex, 1)
}
return copyArr
}
}

方法2: 遍历

1
2
3
4
5
6
7
8
9
function remove (arr, item) {
var rel = []
for (var i = 0; i < arr.length; i++) {
if (arr[i] != item) {
rel.push(arr[i])
}
}
return rel
}

对比,方法2中要遍历原数组一遍,存在n次寻址判等操作。其实方法1中splice删除一个元素的过程中,肯定底层也存在挪动元素(因为数组是一种顺序存储结构嘛),这挪动时最坏情况也是得寻址+数据拷贝n-1次. 而且其在splice之前还要indexOf找到待删除元素的索引,indexOf底层又是一个遍历arr.length次的过程。所以方法1实际上还不如方法2快。

查找数组元素位置

要处理可能不支持indexOf的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function findIndex (arr, item) {
if (Array.prorotype.indexOf) {
return arr.indexOf(item)
}
else {
for (var i = 0; i < arr.length; i++) {
if (arr[i] == item) {
return i
}
}
}
// 上方都没return,则没找到咯
return -1
}

找出数组中比前面都大比后面都小的数

数组洗牌

例如,有个播放器上的歌单列表,有个按钮可以随机重排这个列表,有什么方法

sort函数里使用随机数也可以,但是看起来不够乱。

一个长度为100的数组,如何优雅的求出数组前10项的和

不适用循环创建一个100长度的数组,每个元素的值等于数组的下标

注意这里不能 new Array(100) 之后再map,因为js里面数组是稀疏数组,100个元素不赋值的时候他就是个空数组,无法map。只好 Array(100).join(‘,’).split(‘,’) 形成一个100元素的数组。

一个数组里面有很多不同的数字,请从里面取出一部分数字,使其加起来的和等于M;并给出时间复杂度和空间复杂度。

扩展阅读

数组在chrome V8底层是根据代码不同采用两种方式实现的,一种是快元素一种是慢元素。
对chrome源码和js数组实现感兴趣的,可以参考这篇知乎专栏: https://zhuanlan.zhihu.com/p/26388217

涉及到底层太多的东西,我暂时也没有勇气去研究…