JS数组

264 阅读9分钟

基本概念

  • index指的是下标 ,item指的是下标对应的内容
  • JS数组不是典型的数组类型
    • 别的语言中数组是一种数据类型,也是连续的内存分配
    • 但是JS中是利用对象去模拟的数组,所以JS内的数组有很大的对象的特性
  • JS只提供浅拷贝
  • 数组就是对象
  • 伪数组的原型链中是没有数组原型的
  • 索引越界从数组头部开始删是指你在尝试读取一个超出数组长度范围的item比如arr[arr.length] arr[负数]
    • 因为数组就是对象,读取obj[未定义过的属性] 值就是undefined
  • 读不到的数组item值为undefined,undefined就是undefined数据类型并不是一个对象
  • 并不是所有数组api都不修改原数组
  • 数组API中最最重要的,也是最难的是reduce和splice
  • 多用map而不是forEach,map就是带有返回值的forEach
  • reduce甚至可以替代map与filter

对数组遍历forEach与map的理解

结论: 两者都不会修改原数组的,如果你要得到一个数组的 你就用map 不需要得到数组的就用forEach

对forEach的理解 遍历执行不修改不输出

  • 与 map() 或者 reduce() 不同的是,forEach没有返回值,不会修改原数组,只是对数组遍历执行自定义操作而已
  • 对于forEach,你就想象成for循环对数组操作,只是简化了如下代码
let arr=[1,2,3]
for(let i=0;i<arr.length;i++){
	arr[i]=0
}
let arr=[1,2,3]
arr.forEach((element)=>element=0)
  • for循环代码是可以修改原数组的,但是forEach中的参数是不可能修改原数组的!
  • 因此 根本没有let arr1=arr.forEach(。。。) 这种写法

回调中的形参v才不会修改原值!

请看,这里如果是v++ 也就是让v自身增加1,则不会修改原值,但这里是arr[i]++ 这就会修改原值啦!

map的理解 遍历执行不修改但输出

JS数组中容易犯的错

  • arr[0] // 误以为这个0是数字,其实不然,这是字符串0
var arr = [1,2,3];
Object.keys(arr); // ["0", "1", "2"]
// 解析一下arr[0]是被JS如何理解的
arr[0] ===> JS调用toString方法把数字转成字符串  arr[(0).toString()] ===> arr['0']

字符串转数组

var arr='1,2,3'.split(','); //1
var arr='123'.split(''); //2
Array.from('123'); //3  

Array.from()可以把伪数组转化成数组

Array.from('123'); // 正确  因为每个字符串都有.length属性
Array.from({0:'a',1:'b',2:'c',length:3}) // 正确 因为这个对象有.length属性
Array.from({0:'a',1:'b',2:'c'}) // 错误 因为这个对象没有.length属性

伪数组

形成伪数组有两个条件

  • 键值0123...
  • 有.length

伪数组和数组的区别

伪数组的原型链中没有数组的原型

// 打开上面示例的console敲一下以下
var divList=document.querySelectorAll('.div');
console.log(divList);
// 把伪数组转化成数组
Array.from(divList);

创建数组

arr1.concat(arr2)
arr1.slice(1)
arr1.slice(0)

增删改查

删元素

第一种删除方式delete

因为数组就是对象,所以对象可以用delete删键值,数组当然也可以
但delete的方式会有一个坑,delete数组中的元素后,长度不变,被删的位置为empty

var arr=[1,2,3]
delete arr[0];
console.log(arr); // [empty, 2, 3]

稀疏数组: 稀疏数组没有任何好处,只有bug

var arr=[1,2,3]
delete arr[0];
delete arr[1];
delete arr[2];
console.log(arr); // [empty × 3]

第二种删除方式改length

这种方式不推荐

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

第三种删除方式数组api

var arr=[1,2,3];
arr.shift();// 从数组头部开始删
arr.pop();// 从数组尾部开始删
arr.splice(index,1)// 从数组中间开始删

arr.splice(index,deleteNumber,'...')

  • index:从下标几开始删除
  • deleteNumber:删几个
  • '...': 删完后再在原位置插入什么内容

查看数组元素 又叫遍历数组元素

var arr=[1,2,3];
arr.x='xxx';
  • 我们假设有一个数组arr,我们又很诡异地给它插入一个字母下标
  • 这会导致什么呢?
  • 这其实不是一个正常的数组,因为正常的数组的下标只是数字
  • 那么我们现在来讨论下如何去遍历

第一种方式用对象们都用的方式Object.keys/values去获取下标和值

var arr=[1,2,3];
arr.x='xxx';
Object.keys(arr); // ["0", "1", "2", "x"]
Object.values(arr); // [1, 2, 3, "xxx"]
  • 那么,很显然,得到的结果是没有任何问题的,因为数组本来就是对象
  • 但问题在于,我们怎么能把'x'下标也算作是数组的一部分呢?因此需要在遍历时排除'x'这样类似的字母下标

第二种方式也是用遍历对象键值的方式for in去获取下标和值

var arr=[1,2,3];
arr.x='xxx';
for (let i in arr){
	console.log(i,arr[i]);
}
  • 那么,很显然,得到的结果是没有任何问题的,因为数组本来就是对象
  • 但问题在于,我们怎么能把'x'下标也算作是数组的一部分呢?因此需要在遍历时排除'x'这样类似的字母下标

第三种方式是用最普通的for循环去获取下标和值 --- 推荐

var arr=[1,2,3];
arr.x='xxx';
for (let i=0;i<arr.length;i++){
	console.log(i,arr[i]);
}
  • 显然这是完美的

第四种方式是用forEach去获取下标和值 --- 推荐

var arr=[1,2,3];
arr.x='xxx';
arr.forEach(function(item,index){
console.log(index,item);
})
  • 显然这也是完美的
  • for forEach基本是一样的
  • 我们对比一下for循环和forEach的区别:
    • for循环支持break continue关键字,所以用起来在特定条件下会更加强大
    • forEach一旦开始就无法中止
    • for是块级作用域 forEach是函数作用域
  • forEach需要写一个回调函数,理解起来并不容易,因为你不知道它如何实现的
  • 那么我们来解析一下回调函数的基本实现逻辑

理解回调函数能更好的理解forEach的原理

function forEach(arr,fn){
	for(let i=0;i<arr.length;i++){
    	fn(i,arr[i])
	}
}
var arr=[1,2,3];
function fn(index,item){console.log(index,item)};
forEach(arr,fn);

查找数组元素想知道它在不在数组内

第一种方式indexOf

  • 用于简单的查找一个东西在不在数组里面
var arr=[1,7,8,2,12,22];
arr.indexOf(12);

第二种方式find 与 findIndex

  • 用于复杂查找一个满足某项条件的东西在不在数组里面
  • 比如我想知道除以2余数为0的数有没有
// find
var arr=[1,7,8,2,12,22];
arr.find(function(x){
	return x%2===0;
})
  • 永远只会返回第一个满足条件的item
  • 但是不会让你知道index
// findIndex
var arr=[1,7,8,2,12,22];
arr.findIndex(function(x){
	return x%2===0;
})

增加数组元素

第一种方式直接用下标赋值 不推荐的方式

var arr=[1,2,3]
arr[100]=100;
console.log(arr) //神奇的发现arr.length也变成了101

第二种方式unshift/push

  • 注意数组的头尾哈
  • unshift是从头部添加
  • push是从尾部添加

第三种方式splice

var arr=[1,2,3]
arr.splice(3,0,4) // 在index为3的位置不删除反而新增一个item为4
console.log(arr) 

排序数组元素

reverse方法会修改原来的数组

var arr=[1,2,3];
arr.reverse();
console.log(arr);

一道经典的关于reverse的考题

如何把字符串顺序颠倒?

  • 字符串是没有reverse()的
var str='abcde'; // 如何把这个顺序颠倒?
var arr=str.split("");
arr.reverse();
var str1=arr.join("");
console.log(str1);

sort方法实现自定义item大小排序

sort方法的回调函数只需要用户规定好前后两元素哪个大哪个小,排序的工作不需要管

var arr=[1,2,3];
arr.sort((x,y)=>x-y) //	[1,2,3]
arr.sort((x,y)=>y-x) // [3,2,1]

数组变换 --- 数组高级用法 重点在reduce


记住,reduce是最重要的,reduce甚至可以替代map与filter

  • map 把数组的每个item都扔过去cook,cook完的结果再依次返回出来
  • filter 特定条件过滤
  • reduce 一整个数组拿去消化处理,处理完的结果返回出来
假设未经处理的arr是n个items,则经处理后的items数量为:
map: 	n ===> n  一一映射
filter: n ===> m
reduce: n ===> 1

map

var arr=[1,2,3];
console.log( arr.map((item)=>item*2) )

filter

var arr=[1,2,3];
console.log( arr.filter((item)=>item%2) ) // 注意这里是return的 只不过是arrow省略了

reduce

  • reduce的思想是拿回调函数的第一个形参sum作为箩筐,不断地往里装item,但至于是累加呢还是拼接在一起呢都可以
  • reduce第二个参数是sum的初始值

先介绍reduce的常规用法:

var arr=[1,2,3];
console.log( arr.reduce((sum,item)=>sum+item,0) );
  • 注意点1 : 要注意如果你不持续把sum return出去(sum的初始值为[]或者0或者别的你设定的),那么下一次用到的sum就会是undefined
  • 这个地方很容易导致报错的
var arr=[1,2,3,4,5,6];
console.log( arr.reduce((sum,item)=> console.log(sum,item),[]) )

这里来介绍一下reduce如何替代map

// 让一个数组遍历平方
var arr=[1,2,3,4,5,6];
console.log( arr.reduce((sum,item)=>sum.concat(item),[]) )

这里来介绍一下reduce替代filter的几种写法

功能是为了过滤出偶数

// 这是一个最最正确的例子If else
// 下面另外一种写法仅仅是替换了if else
var arr=[1,2,3,4,5,6];
console.log( arr.reduce((sum,item)=>
{
	if(item%2===0){
    	return sum.concat(item)
	}else{
    	return sum
	}
}
,[]) ) 
// 这是一个错误的例子
var arr=[1,2,3,4,5,6];
console.log( arr.reduce((sum,item)=>item%2===0?sum.concat(item),[]) ) 
// 这样写很显然是错误的,三元运算符中,判断语句?结果语句1:结果语句2
// 结果语句1:结果语句2都必须存在
// 这是一个正确的例子
var arr=[1,2,3,4,5,6];
console.log( arr.reduce((sum,item)=>item%2===0?sum.concat(item):sum,[]) ) 
// 这样写很显然是错误的,三元运算符中,判断语句?结果语句1:结果语句2
// 结果语句1:结果语句2都必须存在
// 因为写了如果奇数就返回sum自己,那么就能保证每一轮循环中,sum的值是一定存在的而不是undefined
// 这是一个错误的例子
var arr=[1,2,3,4,5,6];
console.log( arr.reduce((sum,item)=>item%2===0&&sum.concat(item),[]) ) 
// 这样写很显然是错误的,因为短路逻辑仅仅替代的是if(判断条件){执行语句}
// 但是如果不满足判断条件的话,根本不会return sum,这就不满足 注意点1

一道reduce相关面试题

数组变对象

let arr=[
	{name:'动物',id:1,parent:null},
    {name:'狗',id:2,parent:1},
    {name:'猫',id:3,parent:1},
]
// 转换成
{
	id:1,name:'动物',children:[
    	{id:2,name:'狗',children:null},
		{id:3,name:'猫',children:null},
	]
}

思路如下: reduce

//我自己写的正确代码
arr.reduce((sum,item)=> {
    if(!sum.children){
        sum.children=[];
    }  //这一段完全可以放在初始的对象内
    if(item.parent===null){
        sum.name=item.name
        sum.id=item.id
        return sum
    }else{    
       delete item.parent;
       item.children=null;
       sum.children[sum.children.length]=item; // 增加数组元素更好的方式是push
       return sum;
    }
},{})
// 以下正确代码
arr.reduce((result,item)=>{
if(item.parent===null){
result.id = item.id;
result['name'] = item['name'];
}else{
result.children.push(item);
delete item.parent
item.children = null;
}
return result
},{id:null,children:[]})

索引越界

你一辈子读不到arr[arr.length],arr[负数]的值

var arr=[1,2,3];
for(let i=0;i<=arr.length;i++)
{
	console.log(arr[i]) // 读不到的数组item值为undefined,undefined就是undefined数据类型并不是一个对象
}

for 循环与 while 循环

  • while循环能做的 for循环都可以做,但while循环往往语义更好些
  • while循环的逻辑是 如果条件真 就一直做

以下这个示例就在告诉你,while和for循环都可以做,但是语义上差很多
遍历#test里面的node都存放到一个数组里面去并删除node

  • 缺失语句1和语句3的for循环很难理解
  • 为什么这里不能用for循环的常规套路判断length呢?
  • 因为删除一个节点以后length发生变化,同时我们始终是要把第一个放进数组里面

数组api经验以及思路

把一个数组的内容放到另外一个数组中去

  • 思路1 数组遍历 遍历arr1 然后一个一个arr2.push() 不推荐
  • 思路2 数组拼接 直接var newArr=arr2.concat(arr1) 推荐

想知道一个东西是不是已经存在于数组内

  • indexOf(...)!==-1

把两个数组铺平摆放到新的数组中去

let arr1=[1,2,3];
let arr2=["a","b","c"];
let arr3=[];
arr3.push(arr1); // 这是错误的,旧数组的结构还保留着呢
arr3.push(arr2);
console.log(arr3); // [Array(3), Array(3)]

let arr4=[];
arr4.push(...arr1); // 这才是对的,把原有结构打开铺平塞进去
arr4.push(...arr2);
console.log(arr4);

把数组里的每一项拼接在一起形成一串长字符串

let arr=['a','b','c','d']
let string=arr.join('')
console.log(string)