啃犀牛书——数组
前言
基础不牢地动山摇找不到工作吃不饱饭睡不好觉
JS数组
数组是JS基础数据类型。
- JS数组是值的有序集合,无类型限制
- JS数组是动态的,按需增大缩小,最大索引值2^32-2
- JS数组可以是稀疏的,不一定连续索引
- JS数组是特殊的对象
JS数组从Array.prototype
继承属性买这里面很多数组方法都是泛型(用于参数化类型)的,所以同样可以用于任何类数组对象。
1 | Array.prototype.__proto__ = Object.prototype // true |
ES6增加一类新数组,统称为“定型函数”,固定长度、固定数组元素类型。(这种函数具有极高性能,且支持二进制数据字节级访问)
创建数组
数组字面量
1 | let empty = [] |
new构造函数
1 | let empty = new Array() |
奇妙的…可迭代对象(扩展操作符)
也算是打平,有的话,只打平一层。且对于空隙默认为未定义索引的**undefined
**。
1 | let a = [1, 2, 3] |
可迭代对象
- 数组,定型数组
- 字符串
- 类数组对象(每个属性键都是整数,且有length属性)
- set&map
- Generator对象
- 自定义可迭代对象
工厂方法
Array.of()
当使用构造函数创建数组时,如果传入数值参数,且仅为一个,那么他只会创建一个长度为1的空数组,而非包含值1的数组。
ES6中给出的这个工厂方法就可以解决这个问题。
1 | let arr = Array.of(1) // arr = [1] |
Array.from()
ES6新增的另一个工厂方法,是创建数组副本的简便方式。
参数
- iterable 可迭代对象
- mapFn 映射函数
- thisArg 指定函数this值
第二个参数为函数,当构建新数组时,原对象的每个元素都会传入该函数,返回值替代原始值加入新数组。(和map类似,但是在构造数组期间进行映射要比构建之后再映射 的效率高–前者效率高)
1 | let copy = Array.from(original) |
第三个参数用于设置参数函数的this
值,比较少用。即当mapFn
非箭头函数,而是一个普通函数时,它的this是由所处上下文环境决定的,但是放置在Array.from()
中,没有自己的执行的上下文,因此可以给它指定一个上下文对象。这便使得函数可以调用这个对象内容。
1 | const numbers = [1, 2, 3, 4, 5]; |
真细致!
读写数组元素
你的中规中矩,超乎你的想象
1 | let a = ['qvq'] |
数组的数组索引
0到2^32-2之间的正整数
以上代码可见数组特殊之处,就在于当使用小于2^32-1
非负整数作为属性名时(数值数组索引),数组会自动为我们维护length
。
需要再次注意的是,数组就是特殊的对象。数组的索引都是数组的属性名。JS会将数值数组索引转换为字符串作为属性名。
当我们使用恰好与这范围内数字值相等的字符串/浮点值的时候,也将属于数组数值索引
数组的对象属性
非0到2^32-2之间的正整数的值
数组索引和对象属性的区别
数组中可以用任意名字创建属性。前者后者不冲突,若是前者,会自动维护length,若是后者,不会维护length且输出是看不到的,但是能够正常查看
1 | let arr = [1, 2, 3] |
此外,在查看数组任何不存在属性的时候都不会导致错误,而是但会undefined
稀疏数组
稀疏数组就是其元素没有从0开始的索引的数组
简单介绍
↑书中对于稀疏数组的定义我无法赞同,缝隙存在于中间而不是开头也是稀疏数组啊。
1 | let bookArr = [,,1,2] |
稀疏数组指存有空隙(稀疏位置)的数组,某些索引没有对应元素。
其实接下来我在用各种数组方法验证它们对稀疏数组的操作时,发现还是需要绕一个弯的。稀疏数组的空隙是没有定义索引且没有定义元素值的。之所以遍历时候发现置空处对应索引的值是undefined
是因为当查看一个数组中不存在的索引值对应值的时候,默认是undefined
所以请理解且牢记上述↑,这对与后续各种数组方法的理解也有所帮助。
稀疏数组的创建
很简单,用new Array创建后加值或者直接给一个大于length的数组所以赋值等,都可以创建。
delete操作符也可以创建↓
1 | const arr = [1, 2, 3, 4, 5]; |
足够稀疏的数组通常较于稠密数组(就是常规数组) 慢但内存占用少 的方式实现的。查询稀疏数组与常规数组元素的时间相当。
数组长度
每个数组都有一length
属性,这个属性使得数组有别于常规Js对象。
- 对于稠密数组,长度即为元素个数
- 对于稀疏数组,长度大于元素个数
无论如何,任何数组元素索引都不会大于等于length。
为了维护↑这种不变式(invariant),数组有两个特殊行为:
- 行为1:当给索引为
i
的元素赋值时,如果i>=length
,那么length=i+1
- 行为2:当将
length
设置为一个小于当前值的非负整数n
,那么任何大于等于n
的数组元素都会从数组中删除。
添加和删除数组元素
添加
- 新索引赋值
a[1] = 1
- push()
- unshift()
删除
- delete操作符
delete(a[1])
这种操作不会修改length属性,而是留下空隙 - pop()
- shift()
- 更改length使得去除末尾的一些元素
- splice()
迭代数组
老式循环:行。
1
2
3
4
5
6
7
8
9
10
11// 在嵌套循环等关乎性能的环境中,我们最好不要每次迭代都调一次长度,这个时候就有两种方式
// 长度保存局部变量
for (let i = 0, len = letters.length; i < len; i++) {}
// 由后向前迭代
for (let i = letters.length; i >= 0; i--) {}
// 稀疏情况下,可以加一个判断跳过空隙
for (let i = 0, len = letters.length; i < len; i++) {
if (letters[i] === undefined)
continue
}for/of:
let letter of letters
&&let [index, letter] of letters.entries()
。forEach()
map()
filter()
多维数组
Js无多维数组,用数组嵌套模拟。
1 | let table = new Array(10) |
数组方法
迭代器方法
用于遍历数组元素,通常会对每个元素调用指定的函数。便于对数组进行迭代、映射、测试和归并。
接下来的这些数组迭代器方法,必须接收一个函数参数,函数中可传三类参数:数组元素值、数组索引、数组本身;第二参数可选,指定第一个参数内部this
值。
一般情况下,函数参数的返回值不重要,但是不同方法对于这个返回值的处理是不同的↓
以下所有方法都不会修改原数组(当然传入三参函数可能会修改原数组)。
forEach()
跳过稀疏数组空隙(空隙不会调用函数)。这个方法并没有提供一种终止迭代的方式。
1 | let data = [1, 2, 3, 4, 5], sum = 0 |
map()
该方法把调用它的数组的每个元素进行函数处理。一些列的函数返回值作为新数组元素。会跳过稀疏数组空隙,新数组稀疏程度与原数组稀疏程度相同。这个方法并没有提供一种终止迭代的方式。
1 | let a = [1, 2, 3] |
filter()
该方法返回调用它的数组的子数组。传参函数为一个断言函数。跳过稀疏数组的空隙,且返回的数组始终稠密。这个方法并没有提供一种终止迭代的方式。
1 | let a = [1, 2, 3, , 5] |
find()和奇妙的findIndex()
寻找第一个满足断言函数条件的元素/索引值。倘若遍历之后找不到,返回undefined/-1。
值得注意:find会跳过稀疏数组空隙,findIndex不太一样,它是按照数组索引遍历来的,所以走了空隙不存在的索引——没跳过。
1 | let a = [1, 2, 3, , 5] |
every()和some()
every()在“全称”时返回true;some()在“存在”时返回true。两者都会跳过稀疏数组空隙。牛起来了
1 | let a = [1, 2, 3, , 5] |
reduce()和reduceRight()
两种方法使用我们指定的函数归并数组元素,最终产生一个值。比较特殊,归并值在其他三个参数(当前值,当前索引,数组本身)的前面,且设置初始值的这个参数取代了指定this
的参数。前者从左到右,后者从右到左。
归并 也称 注入inject 或 折叠 fold 。
1 | // 演示一下用法 |
跳过空隙!
1 | let sum = a.reduce((sum, cur) => { |
基于多种数据类型、多维数组、嵌套对象、条件逻辑、归并顺序等方面,用普通的循环逻辑会更易读一些,因为数组归并表达的算法容易复杂化。
打平数组
不会修改原始数组。
flat()
用于展开数组中的嵌套数组。不传参时默认打平一层(1),传参则将参数作为打平层数。跳过空隙,过滤掉。
1 | let a = [1, [2, [3, [4, , 5]]]] |
flatMap()
与map()方法类似,但是返回的数组会被自动打平。类似于map(f).flat()
但效率远高于这样的组合。
1 | let parses = ['helo wld', 'geo rock'] |
允许把元素映射为空数组,打平后空数组元素不会出现在输出数组中。会跳过空隙。
1 | const sparseArray = [, 1, , 2, , 3]; // 稀疏数组 |
添加数组
不会修改原始数组。
concat()
新数组包含调用数组的元素和参数元素。如果参数包含数组,那么会打平一层。因为是合并操作,不会去管空隙。
1 | let a = [1, , 2, 3] |
栈、队列操作
不会打平,修改原数组了
栈操作之push()和奇妙的pop()
push():栈尾添加元素,对于空隙,笑死,根本不让你push一个单纯空,如果结合
...
,能给...
打的undefiend
。pop():去除栈尾元素,对于空隙,pop的机制是按索引排出,所以认为排出的元素是
undefined
。1
2
3let a = [1, 2, 3, , ,]
console.log(a.pop()) // => undefined
console.log(a) // => [1, 2, 3, ]
队列操作之unshift()和shift()
理论上讲,这两种方法的操作效率是不及push和pop的,因为每次操作都要重排后面的元素位置。
- unshift():队列头添加元素。**空隙
undefiend
**,注意一次性和多次的元素顺序。 - shift():去除队列头元素。空隙
undefiend
1 | let a = [,, 2, 3] |
连续区域处理
所谓连续区域,指数组的子数组/‘切片’。以下这几个方法用于提取、替换、填充和复制切片。
奇妙的slice()
该方法用于返回数组切片。不修改原数组。会纳入空隙。接收两个参数,范回的元素范围是[参数1,参数2)
。
- 如果只有一个参,那么返回从参到后所有元素;
- 如果任何一个参为负值,那代表的是倒数第几个元素
-1 => 数组最后一个元素
;
1 | let a = [1, , 2, 3, 4, 5] |
奇妙的splice()
用于对数组/插入删除。会修改原数组。 纳入空隙。接收多个参数,处理区间是[参数1, 参数1+参数2)
。这个方法返回的是被删除元素的数组!!
- 如果只有一个参数,那么删除这个参数及其后所有元素;
- 前两个参数是指定要删除哪些参数,后续参数是要插进删除位置的元素,不会打平。
1 | let a = [1, 2, 3, 4, 5] |
fill()
该方法将数组或切片元素设置为指定值。会修改原数组。 纳入空隙,空隙也替换。可传三个参数,第一个是要设置的值,第二个第三个类似slice
,是要替换的起始终止值。
- 如果只有一个参数,则数组全部元素替换为该值;
- 其实参数的传递规则请参考slice。
1 | let a = [1, 2, 3, , 5] |
copyWithin()
该方法把数组切片复制到数组中的新位置。会修改原数组。 纳入空隙。数组长度不变,超了就不要了。
- 第一个参数:指定要把第一个元素复制到目标索引。
- 第二个参数:起始索引。(若省略,默认从0开始)
- 第三个参数:终止索引。(若省略,默认数组长度)
1 | let a = [, 2, 3, 4, 5] |
索引、排序
indexOf() 和 lastIndexOf()
前者从左到右返回找到的第一个匹配元素的索引。找不到就返回-1
。忽略空隙。
后者从右到左返回找到的第一个匹配元素的索引。找不到就返回-1
。
- 第一个参数是搜索值;
- 第二个参数是搜索起始索引,可以是类似slice的负值,默认从0开始。
1 | let a = [, 2, 3, 4, 5] |
这两个方法使用===
操作符比较他们的参数和数组元素。所以如果数组包含对象而非原始值,那么这些方法检查两个引用是否来自同一个对象。如果想查找对象的内容,建议find(自定义func断言)
如何利用idnexOf()找多个符合条件的值?
1 | function findall (a, x) { |
奇妙的includes()
这个方法我个人比较常用到。接收一个参数,如果数组包含这个参数,返回true
,否则false
。**按照索引视空隙为undefined
**。
1 | let a = [, 2, 3, 4, 5] |
与indexOf()不同的地方,后者是使用===
判断,导致 非数值 与其他和自身都不相同,因此没法检测到NaN;但是includes()就可。
sort()
会修改原数组。 纳入空隙,未定义的元素会放在末尾。不传参的时候默认都按字母顺序排;传参就按参数给的比较函数规定的排。
比较函数:
- 返回值大于0:需要调换!
- 返回值小于0:无需调换。
- 返回值等于0:两值相等~
1 | // 基本比较函数 |
reverse()
反转数组。纳入空隙。修改原数组。
1 | let a = [1, 2, 3, ,] |
数组字符串转换
join() & split()
不修改原数组。纳入空隙。打平后用,
连接,与无需打平的元素用传参连接。
1 | let a = new Array(4) |
toString() & 奇妙的toLocalString()
这俩不传参。toString()用起来和不传参的join()一样,toLocalString()是前者的本地化版本。
- 前者直接连接。不修改原数组。纳入空隙。打平后用
,
连接。 - 后者先将所有元素转化为字符串然后再连接。不修改原数组。空隙报错。打平后用
,
连接。
1 | console.log([1, 2, ,].toString()) // => "1,2," |
静态数组函数
可以通过构造函数调用的三个静态函数。
- Array.of()
- Array.from()
- Array.isArray():判断一个未知值是否是数组
类数组对象
JS数组具有一些与其他对象不具备的特性(非本质)。
- length自动更新
- length为更小的值会截断函数
- 数组从Array.prototype继承有用的方法 => 类数组对象不能直接使用这些方法
- Array.isArray()返回true
只要对象有一个数值属性length,而且有相应的非负整数属性,那么就可以完全视同为数组。
常规对象边类数组对象
1 | let a = {} |
测试对象是都为类数组对象
1 | function isArrayLike(o) { |
类数组怎么才能使用数组方法
Function.call()
1 | let a = {'0': '1', '1': '2', '2': '3', length: 3} |
作为数组的字符串
字符串也可以像上面这样使用数组方法。字符串不是数组。
1 | let s = 'haha' |
字符串是不可修改的值,所以当拿它当数组用的时候,是只读的。所以push sort serverse splice
这些对字符串不起作用,静默失败,建议转数组后操作然后转回(麻烦捏)。
小结
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。
稀疏数组你到底是个什么样的庞然大物?我在理解这些数组方法的时候,大部分的时间都在考虑它们如何处理你。