数据结构之-数组

数组无处不在,当你需要多于一个数据的传输或者展示的时候,就会用到数组。

现在我们从定义说起。

数组定义

数组就是一组数据,表现形如:

[1,3,5,7]
复制代码

JavaScript中,数组元素可以是任意类型,不过通常,同一类型的一组数据更常见,也更有实际意义。

可以用下面几种方式定义数组:

  • 字面量
  • Array()构造函数

字面量

最简单直观,也是最常用之一。

let arr = [1,2,3]
复制代码

这里有两个需要注意的现象:

let arr = [1,,3]  //访问arr[1]会是undefined
let arr = [,,] //数组长度会是2
复制代码

Array()构造函数

构造函数创建数组是最合理的方式之一。就像下面这样。

let arr1 = new Array(1,2,3,4)   //[1, 2, 3, 4]
let arr2 = new Array(1)  // [ <1 empty item> ]
let arr3 = new Array('1')  // [ '1' ]
复制代码

注意:

  • 当构造函数的参数是多项时,会将每一项作为元素创建出来。
  • 当仅有一个数字时,表示数组的长度,元素并未填充,访问也会是undefined。
  • 仅有一个非数字,则又会创建一个单个元素的数组。

常见数组方法及应用

数组的方法多且用途广,它们是数组强大的关键。

如果你是初学者,不要指望靠看就能记住,想当年,笔者把《高程》3 的数组章节看了几遍都没记住,不过,并不代表没有帮助记忆的方法,将其和实际应用场景相结合,就可更好地记忆。

我们逐一清点。

类型判断——Array.isArray()

有些时候,我们要判断拿到的数据是不是数组,否则可能出现方法调用报错的情况,通常会选用以下几种方式:

  • instanceof

判断运算符的左侧是否是右侧类型的一个实例,即 a instanceof b。此时我们将b设为Array即可,返回布尔值。

  • constructor

前面我们聊创建对象的时候,提到过constructor,引用类型都有对应的constructor,数组实例的constructor是Array,所以,可用a.constructor == Array是否为true来检查a是否为数组实例。

  • Object.prototype.toString.call()

这个方法,旨在将实例的类型转为字符串然后输出,如果实例是数组类型,则会输出'[object Array]',进而可以做出判断。因为前两个方法可能存在一些不确定的问题,这个方法,曾被认为是最准确和可靠的方法,判断其他引用类型也同样。

ES6的出现提供了新的判断方法——isArray(),只需要Array.isArray(obj)即可判断obj是否为数组,提供了极大的便利。

添加及删除——push()/pop()、unshift()/shift()

数组被创建后,可能有元素,也可能没有元素,这两组方法,常被用来动态地向数组中添加或者删除元素,只是它们的方式有所局限,只能从数组的两端进行操作,但对于适合的场景来说够用了。

什么是适合的场景?只要求符合条件的元素,不讲究顺序,也没有其他附加条件,就可以这样简单粗暴地处理。

const arr = [],ele;
if(ele > 1){
    arr.push(ele)  //对应的删除即pop()
}
复制代码

另外一对同理。

任意位置添加或删除——splice()/slice()

既然上面的方法有局限,这组就更灵活,它的灵活体现在不再局限位置,可在任意位置进行添加、删除、替换,至于是哪一种,取决于传参的情况。

参数格式:splice(index,nums,item1,.....,itemX)

它们分别代表:

index——开始位置

nums——空出位置数

item1,.....,itemX——从空出的位置添加进哪些元素

前两个参数必填,第三个选填。由此可得出:

只要给index赋一个合法的值,就可以选定操作位置,第二位如果是0,则不删除元素,此时若第三个参数有值,则往指定位置添加元素

如果第二个参数是非0的正整数,则删除指定数量的元素,此时第三个参数如有数据,则填到删除了元素的位置,起到元素替换的效果。

那么slice()又是什么,它有何不同?

slice看起来跟splice很像,只差一个字母,但用途大不同,主要两点区别:

一、slice接收两个参数,begin 和 end,决定了截取源数组的哪些部分,截取出的部分包括begin,但不包括end

二、slice返回一个新数组,这个新数组是源数组的一个浅拷贝,源数组不受影响

所以,这两种方法的使用可简单概括为:如果想要在源数组的基础上做处理,截取某部分,但不改变源数组,用slice,其他情况用splice。

特定元素的索引——indexOf()/findIndex()

前面的方法中,我们提到了“元素”和‘位置’,很多时候并不知道某元素所在的位置,要动态获取,这时候查找索引就派上了用场。

这两种方法所得结果类似,但用法存在差异。

  • indexOf(searchElement[, fromIndex])

indexOf()方法需要传入具体的查找元素,和起始索引(可选)。

const nums = [1, 2, 3, 4, 5];

nums.indexOf(3); //2
复制代码
  • findIndex()方法则是传入一个回调函数

函数支持三个可选参数:元素、索引、数组本身。通常,使用前两个,甚至一个参数就够了。像下面这样:

const nums = [1, 2, 3, 4, 5];
let targetIndex = nums.findIndex(target => target  == 3) //2
复制代码

要特别注意的是,有时候结果可能跟期望不同,即当数组中有多个相同目标元素的时候,它们都只会返回第一个目标元素的位置。

const nums = [1, 2, 3, 3, 5];
nums.indexOf(3); //2
let targetIndex = nums.findIndex(target => target  == 3) //2
复制代码

这是正常情况,如果异常,比如元素不存在,二者均会返回-1。

查找元素——includesOf()/find()

上一组方法,是找到某元素在数组中的位置,当然,顺便可以通过返回值是不是-1来判断元素是否存在,而这一组方法,则是直接得到元素是否存在于数组中。

  • includesOf()——返回布尔值
const nums = [1, 2, 3, 3, 5];
nums.includes(1) //true
nums.includes(8) //false
复制代码
  • find()-返回目标值本身
const nums = [1, 2, 3, 3, 5];
nums.includes(1) //1
nums.includes(8) //undefined
复制代码

填充——fill()

上面讲创建数组的时候,说可以创建一个空数组,然后往里添加,也可使用字面量创建现成的数组,也可使用splice对数组进行增、删、改,但还有一种方式可以用来改变数组——fill()。

看看用法。

const arr = new Array()
arr.fill(1) //[]
复制代码

哦豁~好像翻车了,说好的填充呢,怎么还是空数组?

且慢,fill()方法不是这么用滴,使用它的前提是,数组中已有一定数量的元素。比如:

可以这样:

const arr = new Array(1,2,3,4)
arr.fill(5) // [ 5, 5, 5, 5 ]
复制代码

也可以这样

const arr = new Array(4)
arr.fill(5) // [ 5, 5, 5, 5 ]
复制代码

由此,能够得出一个快速建立具备某数量的非空数组的方法。

现在来看看完整语法:arr.fill(value[, start[, end]])

似曾相识吧,它也接收两个位置参数,一个起始位置,一个结束位置,上面我们没有传的时候,它们默认是从头到尾,我们可以设定试试看。

const arr = [1,2,3,4,5,6]
arr.fill(5,2,4) // [ 1, 2, 5, 5, 5, 6 ]
复制代码

但是,fill()方法有个易犯的错误,当填充的元素是引用类型时,其填充的值都会是同一个引用,比如,初始化一个商品列表。

const goods = {
    name:'珠宝',
    price:10,
    weight:'20'
}

const goodsList = new Array(8)
goodsList.fill(goods)
复制代码

此时的商品列表数据会是这样:

[
  { name: '珠宝', price: 10, weight: '20' },
  { name: '珠宝', price: 10, weight: '20' },
  { name: '珠宝', price: 10, weight: '20' },
  { name: '珠宝', price: 10, weight: '20' },
  { name: '珠宝', price: 10, weight: '20' },
  { name: '珠宝', price: 10, weight: '20' },
  { name: '珠宝', price: 10, weight: '20' },
  { name: '珠宝', price: 10, weight: '20' }
]
复制代码

然后我们编辑第一个商品,将价格改为8

goodsList[0].price = 8
复制代码

却发现每个商品的价格都被改变了。

[
  { name: '珠宝', price: 8, weight: '20' },
  { name: '珠宝', price: 8, weight: '20' },
  { name: '珠宝', price: 8, weight: '20' },
  { name: '珠宝', price: 8, weight: '20' },
  { name: '珠宝', price: 8, weight: '20' },
  { name: '珠宝', price: 8, weight: '20' },
  { name: '珠宝', price: 8, weight: '20' },
  { name: '珠宝', price: 8, weight: '20' }
]
复制代码

这显然不是想要的效果。

不仅如此,别忘了数组也是引用类型,所以,在初始化二维数组时同样会有这个问题,因此,如果有这样一个需求——在页面初始化时,需要准备好一组待编辑/修改的数据项,就不适合用这种方法来创建了。

排序——sort()

排序是个常见需求,凡涉及列表,定有排序,按时间、按价格、按销量等。

最简单的,给一组数字排序。

const numSort = [3,2,8,6,5,7,9,1,4].sort()
numSort //[1, 2, 3, 4, 5, 6, 7, 8, 9]
复制代码

这么简单,有什么可说?

当然有,一个小例子就成功欺骗了我们,它是按照数字大小从小到大排列?非也,不信再看。

我们将上面的数组改一下:

const numSort = [3,2,8,10,6,20,5,7,9,1,4].sort()
//[1, 10, 2, 20, 3, 4, 5, 6, 7, 8, 9]
复制代码

神奇的事情发生了,10比2小?20比3小?

注意了,sort()方法实际接收一个函数,以函数的返回值来指定按某种顺序排列,如果省略函数,则按照将元素转为字符串的各字符的Unicode位点进行排序。所以,如果这里想要按照数字的真实大小排序,可以这样写。

[3,2,8,10,6,20,5,7,9,1,4].sort((a,b) => a-b)
复制代码

依据是什么?是排序函数的算法规则:

  • 如果 a - b 小于 0 ,a 会被排列到 b 之前;
  • 如果 a - b 等于 0 ,a 和 b 的相对位置不变;
  • 如果 a - b 大于 0 ,b 会被排列到 a 之前。

如果比较对象是字符串,方法也一样,所以,一般情况下,不要偷懒,我们可以充分运用这个特点,对需要的规则进行定制。

上面讨论的是对数字或者字符串进行排序,日常需求中,往往不会这么简单,可能会对一列包含多个属性的对象数组进行排序,比如开始提到的:价格、销量等。

怎样根据某个属性对数组排序。

其实也不难,同样道理,拿上面的商品列表(goodsList)为例,如果以价格排序,只需要这样:

goodList.sort((a,b)=>{
    return (a.price - b.price)
})
复制代码

就可以了。

说了排序,顺便说下反转(reverse),反转也是一种排序,只是它没什么规则可言,直接将一组元素首尾颠倒,[1,2,3]会变成[3,2,1],['a','f','c']变成[ 'c', 'f', 'a' ]。

合并——concat()/扩展运算

理想情况下,我们获取一个数组,操作一个数组是最好,但有时数据来源不止一个,可能是两个或多个,在展示或传递的时候,又需要合为一个,就要用合并方法,传统方法是concat()。

const primary = [1,2,3,4,5,6], 
      middle = [7,8,9], 
      high = [10,11,12];
const grade = primary.concat(middle,high)  
//[1,  2, 3, 4,  5,  6,  7, 8, 9, 10, 11, 12]
复制代码

但是,如果觉得仅此而已,就又错了。

  • concat()不仅可以用来合并数组,还可以合并一个数组和其他类型的值,比如数字、字符串等。
primary.concat(7,8) // [1,  2, 3, 4,  5,  6, 7, 8]
复制代码
  • concat()在合并数组时,不改变原数组,而是返回新数组,但是,新数组包含的是原数组的浅拷贝。
const primary = [{a:1}], 
      middle = [{b:2}];
const grade = primary.concat(middle)
//[ { a: 1 }, { b: 2 } ]
primary[0].a = 2
middle[0].b = 3
// [ { a: 2 }, { b: 3 } ]
复制代码

引用类型总是带给我们“惊喜”,在使用时要多加注意。

当然,扩展运算符依然是简洁。上面的操作只需要这样就可以:

const grade = [...primary,...middle,...high]
复制代码

返回新数组——map()/filter()

新数组是什么意思?

大部分情况下,我们拿到的数据都是由对象组成的数组,对象是集合,会包含很多东西,它本身的数据、它关联的其他数据等,少则几条,多则几十条,但在传递或者展示的时候并不需要把它们都带着,或者,我们需要在原有基础上进行处理,这时候就可以按需返回处理后的新数组。

比如下面这样:

[
  { name: '珠宝', price: 1000, weight: '20' },
  { name: '手机', price: 2000, weight: '20' },
  .
  .
  .
  { name: '电脑', price: 5000, weight: '20' }
]
复制代码

我们得到一个商品列表,但只需要把name拿出来用,就可以这样。

let nameList = goodsList.map(item=>{
    return item.name
})
['珠宝'.'手机',...,'电脑']
复制代码

又或者,我们需要在原价的基础上,对所有商品进行打折处理,就可以这样:

let priceDiscountList = goodsList.map(item=>{
    item.price *= 0.5 
    return item
})
[
  { name: '珠宝', price: 500, weight: '20' },
  { name: '手机', price: 1000, weight: '20' },
  .
  .
  .
  { name: '电脑', price: 2500, weight: '20' }
]
复制代码

当然,实际项目中这个环节不会这么干,不然每有变动都要改JS逻辑代码,从易用性、效率和维护上都不利,只是借此说明用法。

介绍完map,再看filter,从字面意思很好理解,过滤,符合条件才会被筛选出来,它同样是接收一个函数。

比如我们将价格超过500的找出来。

let priceLowList = goodsList.filter(item=>{
    return item.price > 500
})
[
  { name: '手机', price: 1000, weight: '20' },
  .
  .
  .
  { name: '电脑', price: 2500, weight: '20' }
]
复制代码

这两个方法在实际项目中极为常见,唯独需要注意的是它们的工作方式,它们都是生成新的数组,map需要返回的是数组元素,fliter则是筛选条件,千万记得”return“哦!

迭代处理——forEach()

这个方法,和上面两个极为相似,从底子上,都是可以访问到数组的每个元素,然后进行相应处理,区别在于,此方法仅用于迭代,好比以前常用的for循环,当然,功能的简单意味着可操作空间更大。

比如,我们可以这样实现类似map的效果。

let nameList = []
goodsList.forEach(item=>{
    nameList.push(item.name)
})
复制代码

也可以这样实现类似filter的效果。

let priceLowList = []
goodsList.forEach(item=>{
    if(item.price > 500){
        priceLowList.push(item)
    }
})
复制代码

是的,你可以写任何想要的逻辑,且它的执行效率比for循环更高,也更符合函数式编程范式。

元素判断——some()/every()

同样用于检查,接收回调函数,写入判断逻辑,区别在于,some()是“存在符合”即为true,而every()是“所有符合”才为true,类似 || 和 &&。比较简单,不再赘述。

去重

当用户的操作是大量的、不确定的,难免有重复,有时候我们只需要知道某个值是否存在,而不是多个重复的值,这时就需要对数组进行去重(当然,还有其他方法保证单一值,这里重点是去重)。

去重方法有很多,原理是类似的——通过遍历数组做比较,保证值唯一。列三种大家参考:

  • includes
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array =[];
    for(var i = 0; i < arr.length; i++) {
        if( !array.includes( arr[i]) ) {   //检测数组是否有某个值
            array.push(arr[i]);
        }
    }
    return array
}
复制代码
  • filter
function unique(arr) {
    return arr.filter(function(item, index, arr) {
      //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
      return arr.indexOf(item, 0) === index;
    });
  }
复制代码
  • Set
function unique (arr) {
  return Array.from(new Set(arr))
}


 

上一篇:nginx负载均衡


下一篇:哈夫曼树的构建(c语言描述)