学习链接
数据类型-Array
普通的对象允许存储键值集合,但却无法管理元素顺序,比如无法在已有的元素之间插入一个新的属性。
对象的键名遍历顺序,非负整数键名升序👉非数字字符键名的添加顺序👉Symbol 键名的添加顺序。
为了解决元素的顺序管理问题,引入了数组这一数据结构。
数组,本质上是一种对象,用于存储和管理 有序集合,里面的元素都是按顺序排列的,并且提供 管理元素顺序 的方法。
[] // []
new Array(0) // []
Array(0) // []
数组内部
仍是对象
数组是一种特殊的对象。使用方括号来访问属性 arr[0]
实际上是来自于对象的语法。它其实与 obj[key]
相同,其中 arr
是对象,而数字用作键(key)。
数组是扩展了的对象,提供了特殊的方法来处理有序的数据集合以及 length
属性。但从本质上讲,它仍然是一个对象。
内部优化
数组真正特殊的是它们的内部实现。
JavaScript 引擎尝试把这些元素一个接一个地存储在连续的内存区域,同时还有一些其它的优化,以使数组运行得非常快。
但是,如果我们不像“有序集合”那样使用数组,而是像常规对象那样使用数组,这些就都不生效了。
从技术上说,数组是基于对象的,可以给它们添加任何属性。
let fruits = []; // 创建一个数组
fruits[99999] = 5; // 分配索引远大于数组长度的属性
fruits.age = 25; // 创建一个具有任意名称的属性
但是 JavaScript 引擎会发现,我们在像使用常规对象一样使用数组,那么针对数组的优化就不再适用了,然后对应的优化就会被关闭,这些优化所带来的优势也就荡然无存了。
数组误用的几种方式:
- 添加一个非数字的属性,比如
arr.test = 5
。 - 制造空洞,比如:添加
arr[0]
,然后添加arr[1000]
(它们中间什么都没有)。 - 以倒序填充数组,比如
arr[1000]
,arr[999]
等等。
数组在 JavaScript 引擎内部是经过特殊调整的,使得更好地作用于连续的有序数据,所以最好将数组视为有序数据的数据结构,用正确的方式使用数组。
如果需要任意键值,那很有可能实际上我们需要的是常规对象 {}
。
方法性能
push/pop
方法运行的比较快,而 shift/unshift
比较慢。
pop / push
方法不需要额外移动任何东西,因为其它元素都保留了各自的索引。
shift / unshift
方法需要改变剩余所有元素的索引值,因此性能会比 pop / push
差。
清空数组
arr.length = 0;
toString
数组有自己的 toString
方法的实现,会返回以逗号隔开的元素列表。
let arr = [1, 2, 3];
arr // 1,2,3
arr.toString() === '1,2,3' // true
[] + 1 // "1"
[1] + 1 // "11"
[1,2] + 1 // "1,21"
数组没有 Symbol.toPrimitive
,也没有 valueOf
,它们只能执行 toString
进行转换,所以这里 []
就变成了一个空字符串,[1]
变成了 "1"
,[1,2]
变成了 "1,2"
。
当 "+"
运算符把一些项加到字符串后面时,加号后面的项也会被转换成字符串,所以下一步就会是这样:
"" + 1 // "1"
"1" + 1 // "11"
"1,2" + 1 // "1,21"
数组方法
静态方法
Array.isArray()
返回布尔值,识别参数是否为数组。
Array.from()
可将类数组对象和可遍历对象对象转为数组。
Array.of()
将一组值转为数组,弥补 Array()
的不足。
实例方法
sort()
排序稳定性:排序关键字相同的项目,排序前后的顺序不变。
ES2019 规定 sort()
的默认排序算法是稳定的。
比较函数可以返回任何数字,正数表示 “大于”,一个负数表示 “小于”。
arr.sort((a, b) => a - b);
arr.sort((a, b) => b - a);
// 正数: a “大于” b => a 在 b 后
// 负数: a “小于” b => b 在 a 后
内部算法会在排序过程中,将一个元素与多个其他元素进行比较,但是它会尝试进行尽可能少的比较。
includes()、find()、findIndex()
都可识别 NaN
,弥补 indexOf
的不足
增删改查
方法 | 返回 | 对原数组产生影响 |
---|---|---|
push(e0, e1,… , eN) |
length |
✅ |
unshift(e0, e1,… , eN) |
length |
✅ |
pop() |
removed element | ✅ |
shift() |
removed element | ❌ |
splice(start, deleteCount, e1, e2,…, eN) |
removed element array | ✅ / ❌ |
concat(e0, e1,… , eN)/concat(array) |
new array | ❌ |
indexOf(element, fromIndex) |
index or -1 | ❌ |
lastIndexOf(element, fromIndex) |
index or -1 | ❌ |
includes(element, fromIndex) |
boolean | ❌ |
find((e, i, arr) => true/false, thisArg) |
element | ❌ |
findIndex((e, i, arr) => true/false, thisArg) |
index or -1 | ❌ |
some((e, i, arr) => true/false, thisArg) |
boolean | ❌ |
every((e, i, arr) => true/false, thisArg) |
boolean | ❌ |
转换/排序
方法 | 返回 | 对原数组产生影响 |
---|---|---|
sort(compareFn) |
original array | ✅ |
reverse() |
original array | ✅ |
join(separator) |
string | ❌ |
遍历数组
方法 | 返回 | 对原数组产生影响 |
---|---|---|
forEach((e, i, arr) => true/false, thisArg) |
undefined |
❌ |
filter((e, i, arr) => true/false, thisArg) |
new array | ❌ |
map((e, i, arr) => element, thisArg) |
new array | ❌ |
fill(value, start, end) |
original array | ✅ |
reduce((pre,cur, i, arr) => val, initVal) |
value | ❌ |
补充
驼峰转换
const camelize = str => {
return str
.split('-')
.map((element, index) =>
index === 0 ?
element :
element[0].toUpperCase() + element.slice(1))
.join('');
};
camelize("background-color") // backgroundColor
数组去重 ( 性能问题 )
function unique(arr) {
let result = [];
for (let str of arr) {
if (!result.includes(str)) {
result.push(str);
}
}
return result;
}
let strings = ["Hare", "Krishna", "Hare", "Krishna",
"Krishna", "Krishna", "Hare", "Hare", ":-O"];
unique(strings) // Hare, Krishna, :-O
代码有效,但其中存在潜在的性能问题。
方法 result.includes(str)
在内部遍历数组 result
,并将每个元素与 str
进行比较以找到匹配项。
所以如果 result
中有 100
个元素,并且没有任何一项与 str
匹配,那么它将遍历整个 result
并进行 100
次比较。如果 result
很大,比如 10000
,那么就会有 10000
次的比较。
这本身并不是问题,因为 JavaScript 引擎速度非常快,所以遍历一个有 10000
个元素的数组只需要几微秒。
但是我们在 for
循环中对 arr
的每个元素都进行了一次检测。
因此,如果 arr.length
是 10000
,我们会有 10000 * 10000
= 1 亿次的比较。那真的太多了。
所以该解决方案仅适用于小型数组。