我是靠谱客的博主 辛勤水杯,最近开发中收集的这篇文章主要介绍2.4、JavaScript 数据类型 - 数组,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

几乎所有的编程语言中,都存在一个名叫“数组”的特殊数据结构,它能存储有序的集合。JavaScript 自然也不例外。

下面就让我们看看,JavaScript 中的数组是什么样子的。


1、如何声明数组

有两种方法可以声明一个一维数组(下面简称为数组):

let arr = new Array(); // 第一种方式
let arr = []; // 第二种方式

通常情况下,推荐使用第二种方式。

除了 new Array() 的方式比 [] 更复杂之外,它还有一个棘手的特性。

如果你指定了数组的长度,那么它会创建一个指定了长度却没有任何值的数组,就像这样:

let arr = new Array(3);

arr.push(1, 2, 3);

console.log(arr); // [empty × 3, 1, 1, 1] 
console.log(arr.length); // 6

发现了没有,创建了 arr 之后我们 push 三个数字进去,但是数组的长度却变成了 6。这是因为前三个位置被占了,但是却没有存放任何值。

因此,下面都会直接使用 [] 的方式声明数组。

声明数组时,可以同时为数组赋初值,使用下标来获取元素,就像这样:

let names = ['张三', '罗翔老师', '小明'];

// 注意,数组下标是从 0 开始
console.log( names[0] ); // 张三
console.log( names[1] ); // 罗翔老师
console.log( names[2] ); // 小明

// 可以用 length 获取数组元素总个数
console.log( names.length ); // 3

数组类可以存放任何类型的元素,就像这样:

let arr = [ 
    '张三',
    { father: '张大三' },
    true,
    function(age) { console.log(`张三今年${age}岁了!`); }
];

2、数组的常用方法


(1)push,pop,shift,unshift

这四种方法属于最基本的方法,因为比较简单就放着一起看下。

push(…eles) —— 从尾端添加元素,
pop() —— 从尾端提取元素,
shift() —— 从首端提取元素,
unshift(…eles) —— 从首端添加元素。

就像这样:

// 现在我们有三位同学
let names = ['张三', '罗翔老师', '李四'];

// 这时候老师说需要把王五同学添加在李四后面
names.push('王五');
console.log( names ); // ['张三', '罗翔老师', '李四', '王五']

// 把王五又给踢出去
names.pop();
console.log( names ); // ['张三', '罗翔老师', '李四']

// 然后老师说,哎呀张三太调皮了给踢了吧
names.shift();
console.log( names ); // ['罗翔老师', '李四']

// 结果于心不忍,又把张三添加到原来的位置
names.unshift('张三');
console.log( names ); // ['张三', '罗翔老师', '李四']

(2)splice

splice 方法可以说是处理数组的全能好手,它可以实现所有的事情:添加,删除和插入元素。

语法:array.splice(index, howmany, item1,.....,itemX)

参数含义:

1、index:(必选)该参数是开始插入和(或)删除的数组元素的下标,必须是数字。

2、howmany:规定应该删除多少元素。必须是数字,但可以是 “0”。如果未规定此参数,则删除从 index 开始到原数组结尾的所有元素。

3、item1, …, itemX:要添加到数组的新元素

返回值: 如果从当前数组中删除了元素,则返回的是含有被删除的元素的数组。若没有删除则返回空数组。

注意:这种方法会改变原始数组。

下面就让我们看看它的具体例子吧:

// 此时我们同样用这三位同学
let names = ['张三', '罗翔老师', '李四'];

// 首先我们删去罗祥老师(删除)
names.splice(1, 1);
console.log( names ); // ['张三', '李四']

// 我们在刚刚罗祥老师的位置加入王五同学(插入)
names.splice(1, 0, "王五");
console.log( names ); // ['张三', '王五', '李四']

// 此时我们又想把罗翔老师加回来了,就加在最后吧(添加)
names.splice(names.length, 0, "罗翔老师"); 
console.log( names ); // ['张三', '王五', '李四', '罗翔老师']

// splice 第一个参数支持负数,-1 表示末端的前一位
names.splice(-1, 0, "师娘"); 
console.log( names ); // ['张三', '王五', '李四', '师娘', '罗翔老师']

(3)slice

slice 方法相比于 splice 方法简单的多,它仅仅是从原数组中截取一段,返回一个新数组。

注意: slice() 方法不会改变原始数组。

语法: slice(start, end)

参数含义:

1、start:规定从何处开始选取。如果该参数为负数,则表示从原数组中的倒数第几个元素开始提取

2、end:规定从何处结束选取。该参数是数组片断结束处的数组下标(不包含)。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果该参数为负数, 则它表示在原数组中的倒数第几个元素结束抽取。

来看看这几个例子你就明白了:

// 此时我们同样用这几位位同学
let names = ['张三', '罗翔老师', '李四', '王五', '小花'];

// 两个参数都不传,返回整个数组
const newNames1 = names.slice();
console.log( newNames1 ); // ['张三', '罗翔老师', '李四', '王五', '小花']

// 只传 start 参数,则表示从 start 截取到最后一位
const newNames2 = names.slice(1);
console.log( newNames2 ); // ['罗翔老师', '李四', '王五', '小花']

// 只传 start 参数且为负数,表示从倒数第二位截取到数组末端
const newNames3 = names.slice(-2);
console.log( newNames3 ); // ['王五', '小花']

// 传两位参数且都为负数,表示从倒数第二位截取到倒数第一位
const newNames4 = names.slice(-4, -2);
console.log( newNames4 ); // ['罗翔老师', '李四']

// 传两位参数且都为正数
const newNames5 = names.slice(1, 4);
console.log( newNames5 ); // ['罗翔老师', '李四', '王五']

我们可以使用 slice 方法不传入任何参数来获取一个数组的副本,但是需要主要的是,这样的方式属于非完全深拷贝。关于这一点可以参考:JS 浅拷贝与深拷贝

(4)concat

该方法创建一个了新数组(不会改变原数组),其中包含来自于其他数组和其他项的值。该方法比较简单,我们直接看例子:

const num1To2 = [1, 2];

// 1、连接两个数组
let num1To6 = num1To2.concat([3, 4], [5, 6])
console.log( num1To6 ); // [1, 2, 3, 4, 5, 6]

// 2、也可以直接传入值
num1To6 = num1To2.concat([3, 4], 5, 6);
console.log( num1To6 ); // [1, 2, 3, 4, 5, 6]

// 3、添加类数组对象时,通常会被当作一个整体添加
let arrayLike1 = { 0: "hello", 1: "world", length: 2 };
num1To6 = num1To2.concat([3, 4], arrayLike1)
console.log( num1To6 ); // [1, 2, 3, 4, { 0: "hello", 1: "world", length: 2 }]

// 4、但是,如果类数组对象中有 Symbol.isConcatSpreadable 属性时,就会被当作一个数组来添加
let arrayLike2 = { 0: "hello", 1: "world",  [Symbol.isConcatSpreadable]: true, length: 2 };
num1To6 = num1To2.concat([3, 4], arrayLike2);
console.log( num1To6 ); // [1, 2, 3, 4, 'hello', 'world']

(4)数组中的搜索

1)indexOf/lastIndexOf

indexOf(item, from) 方法实现从索引 from 开始搜索 item,如果找到则返回索引,否则返回 -1。

lastIndexOf 和上面方法形同,只是从右向左搜索

看看这个例子:

let arr = [2, 1, false];

console.log( arr.indexOf(2) ); // 0
console.log( arr.indexOf(false) ); // 2
console.log( arr.indexOf(null) ); // -1

注意,在 indexOf 和 lastIndexOf 中,判断相同是严格的 ===,因此如果搜索的 false,那就会精准到 false,而不会搜索到 0。

2)includes

如果仅仅想知道数组是否包含某个元素而不需要知道准确的索引,那么 includes 是一个不错的选择。

let arr = [2, 1, false];
console.log( arr.includes(1) ); // true

需要注意的是,includes 使用的是 SameValueZero 的相等比较算法。该种比较算法与 === 的区别在于它能正确处理 NaN。

let arr = [2, 1, false, NaN];

// (应该是 3,但是严格相等 === 的比较算法对 NaN 无效)
console.log( arr.indexOf(NaN) ); // -1(这个结果是错的)

console.log( arr.includes(NaN) );// true(这个结果是对的)

3)find 和 findIndex

find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。

find(function(item, index, array) 接受一个函数参数,其中 item 是元素,index 是它的索引,array 是数组本身。

针对对象数组时,它会返回满足条件的对象元素,就像这样:

const students = [
  {id: 101, name: "张三"},
  {id: 102, name: "李四"},
  {id: 103, name: "王五"}
];

let student = students.find(item => item.id == 101);
console.log(student); // {id: 101, name: "张三"}

find 方法检索的时第一个满足条件的值,如果找到则立即停止检索。

findIndex 方法 find 方法基本上是一样的,只不过它返回找到元素的索引,而不是元素本身。并且在未找到任何内容时返回 -1。

4)filter

上面我们提到的 find 方法搜索的是使函数返回 true 的第一个(单个)元素。那么如果我们需要找到所有满足条件的元素该怎么办呢?

filter 是一个不错的选择,它的语法和 find 大致相同,但是 filter 返回的是所有匹配元素组成的数组。

看看这个例子吧:

const students = [
  {id: 101, name: "张三"},
  {id: 102, name: "李四"},
  {id: 103, name: "王五"}
];

let student = students.filter(item => item.id <= 102);
console.log(student); // [{id: 101, name: "张三"}, {id: 102, name: "李四"}]

因此,如果你只希望找到第一个满足条件的元素可以使用find;但是如果你希望找到所有满足条件的元素则 filter 是首选。

(4)reverse

reverse 方法用于颠倒 arr 中元素的顺序。

let names = ['张三', '罗翔老师', '李四', '王五', '小花'];

names.reverse();
console.log(names); // ['小花', '王五', '李四', '罗翔老师', '张三']

(5)split

split(delim) 方法可以通过给定的分隔符 delim 将字符串分割成一个数组。

let names = "张三,罗翔老师,李四,王五,小花";
  
let namesArr = names.split(",");
console.log(namesArr); // ['张三', '罗翔老师', '李四', '王五', '小花']

split 方法有一个可选的第二个数字参数:对数组长度的限制。如果提供了,那么额外的元素会被忽略。

let names = "张三,罗翔老师,李四,王五,小花";
  
let namesArr = names.split(",", 3);
console.log(namesArr); // ['张三', '罗翔老师', '李四']

split 如果传入空字符串是,则可以用于拆分字母。

let str = "hello!";
  
let strArr = str.split("");
console.log(namestrArrsArr); // ['h', 'e', 'l', 'l', 'o', '!']

(6)join

join(glue) 与 split 相反。它会在它们之间创建一串由 glue 粘合的 arr 项。

let names = ['张三', '罗翔老师', '李四', '王五', '小花'];

let namesStr = names.join(",");
console.log( namesStr ); // 张三,罗翔老师,李四,王五,小花

namesStr = names.join("!--");
console.log( namesStr ); // 张三!--罗翔老师!--李四!--王五!--小花

split 和 join 是相反的一对方法,注意对比学习。


(7)reduce/reduceRight

arr.reduce 方法和 arr.reduceRight 用于根据数组计算单个值。

下面以 reduce 为例为大家进行介绍。

首先我们确定语法

[0, 1].reduce(function(accumulator, item, index, array) {
  // ...
}, initial);

reduce 接受两个参数,第一个是一个回调函数,其中参数含义为:

1、accumulator:是上一个函数调用的结果,第一次等于 initial(如果提供了 initial 的话);

2、item:当前的数组元素;

3、index:当前索引;

4、arr:数组本身

reduce 的第二个参数是:传递给函数的初始值(可选)。

应用函数时,上一个函数调用的结果将作为第一个参数传递给下一个函数。

看看这个例子:

let arr = [1, 2, 3, 4, 5];

let result = arr.reduce((sum, current) => sum + current, 0);

console.log(result); // 15

我们来分析一些,首先我们给定了 reduce 的第二个参数为 0,那么则表示初始值为 0。

然后看 reduce 的第一个参数: (sum, current) => sum + current;

其中 sum 正是回调函数的第一个参数,因为 我们提供了 initial 参数,那么 sum 的初始值也就为 0 了。

现在我们修改一下这个回调函数,打印一下 sum 的值:

let result = arr.reduce((sum, current) => {
    console.log(sum); // 0 1 3 6 10 
    return sum + current;
}, 0);

可以看到,sum 的第一个值正是 0,随后的 sum 不正是 arr 中数字的逐渐累加值嘛。

需要注意一点的是,sum 值只到了 10,这是因为 sum = 10, current = 5 时,这已经时最后一次累加,因此计算完 10 + 5 之后就直接返回了最终结果。

那么如果我们不用 reduce ,我们该如何计算数组中元素的累加值呢?

let arr = [1, 2, 3, 4, 5];

let sum = 0;
arr.forEach(ele => sum += ele);

console.log(sum); // 15

可以看到,我们需要首先声明一个 sum 变量,然后遍历数组进行累加。

而在 reduce 中,回调函数的第一个参数不正是我们在这里声明的 sum 吗。

我们也可以不为 reduce 提供初始值,就像这样:

let result = arr.reduce((sum, current) => {
    console.log(sum); // 1 3 6 10
    return sum + current;
});

请注意,你会发现这一次 reduce 少执行了一次,这不是一个错误,而是 reduce 会将数组的第一个元素作为初始值,并从第二个元素开始迭代。


还有一点,当我们不提供初始值的时候要小心,因为如果数组为空且没有初始值的时候,调用 reduce 会出现异常。

const arr = [];

// Error: Reduce of empty array with no initial value
// 如果初始值存在,则 reduce 将为空数组返回初始值。
arr.reduce((sum, current) => sum + current);

所以在这里建议当我们使用 reduce 方法时要始终指定初始值。

reduceRight 和 reduce 方法的功能一样,只是遍历为从右到左。

(8)toString

数组有自己的 toString 方法的实现,会返回以逗号隔开的元素列表。

let names = ['张三', '罗翔老师', '李四', '王五', '小花'];

console.log( names.toString() ); // 张三,罗翔老师,李四,王五,小花

此外,让我们看看这个:

console.log( [] + 1 ); // "1"
console.log( [1] + 1 ); // "11"
console.log( [1,2] + 1 ); // "1,21"

数组没有 Symbol.toPrimitive,也没有 valueOf,它们只能执行 toString 进行转换,所以这里 [] 就变成了一个空字符串,[1] 变成了 “1”,[1,2] 变成了 “1,2”。

当 “+” 运算符把一些项加到字符串后面时,加号后面的项也会被转换成字符串。

这就是为什么上面代码中会出现这样的结果。

关于 toString 方法大家目前只需要知道它会返回以逗号隔开的元素列表,其他的要等看完对象一章后再回过头来理解即可。


(9)其他方法

上面提到的方法是常用的一些方法,我们没有必要列举出所有方法,各位也没有必要去死记硬背这些方法,只需要在使用的时候有不清楚的情况查阅文档即可。

在这里我们简单的提一提其他可能会用到的方法:

1)some(fn)/every(fn) 检查数组:

对数组的每个元素调用函数 fn。如果任何/所有结果为 true,则返回 true,否则返回 false。

这两个方法的行为类似于 || 和 && 运算符:如果 fn 返回一个真值,arr.some() 立即返回 true 并停止迭代其余数组项;如果 fn 返回一个假值,arr.every() 立即返回 false 并停止对其余数组项的迭代。

2)fill(value, start, end):填充

从索引 start 到 end,用重复的 value 填充数组。

3)flat(depth)/arr.flatMap(fn) :扁平化

从多维数组创建一个新的扁平数组。

4)sort(fn) 排序

可以进行数组排序。

完整的数组方法列表,可以参考:Array 数组


3、判断数组的方法

由于数组是基于对象的,因此使用 typeof 无法帮我们准确判断数组,那么有什么方法可以帮助我们区分数组和对象呢?

请看下面的示例:

let names = ['张三', '罗翔老师', '李四', '王五', '小花'];

// 方式一:依据原型链查找的方式
console.log( names instanceof Array ); // true

// 方式二:Array 自身提供的方法
console.log( Array.isArray(names) ); // true

// 方式三:借用 Object 函数的 toString 方法
console.log( Object.prototype.toString.call(names) === '[object Array]' ); // true

其中方式一用于判断引用类型,方式二只能用于判断是否为数组;

最值得一提的是方式三,这是一种特殊的方式,通过它我们可以判断任意一种类型。

console.log( Object.prototype.toString.call([]) ); // [object Array]

console.log( Object.prototype.toString.call({}) ); // [object Object]

console.log( Object.prototype.toString.call(() => {}) ); // [object Function]

console.log( Object.prototype.toString.call(1) ); // [object Number]

console.log( Object.prototype.toString.call(false) ); // [object Boolean]

console.log( Object.prototype.toString.call(Symbol(1)) ); // [object Symbol]

console.log( Object.prototype.toString.call("string") ); // [object String]

console.log( Object.prototype.toString.call(1000n) ); // [object BigInt]

console.log( Object.prototype.toString.call(undefined) ); // [object Undefined]

console.log( Object.prototype.toString.call(null) ); // [object Null]

4、数组循环

在数组中,循环是一个很重要的功能。

(1) for 循环

遍历数组最古老的方式就是 for 循环,

let names = ['张三', '罗翔老师', '李四', '王五', '小花'];

for (let i = 0; i < names.length; i++) {
    console.log( names[i] ); // 张三, 罗翔老师, 李四, 王五, 小花
}

(2) for … of 循环

for…of 不能获取当前元素的索引,只是获取元素值。当你不需要用到索引时,这是一个不错的选择。

let names = ['张三', '罗翔老师', '李四', '王五', '小花'];

for (let item of names) {
    console.log( item ); // 张三, 罗翔老师, 李四, 王五, 小花
}

(3) for … in 循环

for…in 循环会遍历 所有属性,不仅仅是这些数字属性。

看看这个例子:

let names = ['张三', '罗翔老师', '李四', '王五', '小花'];

// 数组基于对象,因此我们可以直接为数字添加属性
names.info = "这是保存名字的一个数组";

for (let key in names) {
    // 张三, 罗翔老师, 李四, 王五, 小花,这是保存名字的一个数组
    console.log( names[key] ); 
}

你会发现,它竟然把数组的 info 属性也一起打印了出来。

因此,在这里我们需要明确,for…in 循环适用于普通的对象,它做了对应的优化。但是不适用于数组,因此速度要慢 10-100 倍。当然即使是这样也依然非常快。只有在遇到瓶颈时可能会有问题。

因此一般情况下,建议不要使用 for … in 来遍历数组。


(4) forEach 方法

数组对象本身自带的循环方法。

names.forEach((ele, index) => {
    console.log( ele, index ); // 张三, 罗翔老师, 李四, 王五, 小花
});

(5)map 方法

map 方法也是数组对象本身自带的循环方法。与 forEach 不同的是,forEach 不建议对元素就行修改,但 map 可以对数组的每个元素都调用函数进行修改,并返回结果数组。

map 方法是最有用和经常使用的方法之一。

语法:

let result = arr.map(function(item, index, array) {
  // 返回的是新值而不是当前元素,因此必须 return
})

看看这个例子,我们会为每个学生增加年龄

const students = [
  {id: 101, name: "张三"},
  {id: 102, name: "李四"},
  {id: 103, name: "王五"}
];

students.map(ele => {
  if (ele.name === "张三") {
    ele.age = 24;
  }
  ele.age = 26;
  return ele;
});

 console.log(students); // ...

大家可以自行运行下,就会看到我们的 students 数组中每个对象都添加 age 这个属性


看到这里相信不少同学已经有些疲倦了,不妨先休息休息,接下来的内容会让你更加了解数组,但是需要你保持清醒的头脑。

如果你已经做好准备,就让我们一起开始吧!


5、数组内部

在前面的知识中,我们已经提到过了,数组是基于对象的,或者说数组本身就是一种特殊的对象。

我们知道数组可以通过方括号 [] 加下标的方式来访问数组中的值,其实这和对象中使用方括号 [] 加 key 相同,只不过数组中的键用下标来代替。

很多方面都可以佐证这一点:

let names1 = ['张三', '罗翔老师', '李四', '王五', '小花'];

// 1、通过引用来复制,其复制行为本身复制的是地址
let names2 = names1;
console.log( names1 === names2 ); // true

// 2、通过 instanceof 判断
console.log(names1 instanceof Object); // true

// 3、通过 typeof 判断
console.log( typeof names1 ); // object

那么就算我们知道了数组本质是一个对象有什么意义呢?

意义在于数组是一个特殊的对象,既然是特殊的,那么就一定有与一般对象不同的地方。

数组真正特殊的是它们的内部实现。JavaScript 引擎尝试把这些元素一个接一个地存储在 连续 的内存区域,并且做了一些其它的优化,以使数组运行得非常快。

但是,如果我们像普通对象那样使用数组,那么 JavaScript 引擎所做的优化则不再生效,就像这样:

let names1 = ['张三', '罗翔老师', '李四', '王五', '小花'];

names.info = "这是保存名字的一个数组"; // 创建一个具有任意名称的属性

names1[1000] = "余罪"; // 分配索引远大于数组长度的属性

从技术上讲,我们这样写是不会抛出异常的。

但是 Javascript 引擎会识别出来我们在像使用常规对象一样使用数组,那么引擎针对数组的优化就不再适了,对应的优化就会被关闭,这些优化所带来的优势也就荡然无存了。

数组有几种常见的误用方式,刚开始接触的同学可能会犯这样的错误:

1、添加一个非数字的属性: names.info = "这是保存名字的一个数组";
2、制造空洞:添加 names[0],然后添加 names[1000] (它们中间什么都没有)。
3、以倒序填充数组: arr[1000],arr[999] 等等。

请大家在使用数组的时候,务必要把数组视为一个 连续的,有序的 数据结构。

如果你想使用任意键值,那么实际上使用常规对象 {} 会更合适。

6、性能

实际上,在一般的数组使用过程中,我们一般不用太考虑性能方面。这是因为以现在计算机的计算能力与 JavaScript 引擎的优化,一般层级的数据量和操作不会给我们太直观的差距。

但是我们仍然需要了解这方面的知识,以防在碰到相关问题的时候有思考的方向。

在前面的内容中我们已经提到,数组是在内存中连续存放的,那么首先程序会在内存中开辟一块区域用于存放数组的值。


通常情况,我们会在数组的末尾进行增加或者删除元素,就像这样:

let names = ['张三', '罗翔老师', '李四', '王五', '小花'];

names.push("小明"); // 在数组末端添加一个元素
console.log(names); // ['张三', '罗翔老师', '李四', '王五', '小花', '小明']

names.pop(); // 弹出数组末端元素
console.log(names); // ['张三', '罗翔老师', '李四', '王五', '小花']

这样并没有什么问题,那么有这样一个需要,需要你把小明添加到张三的前面,也就是添加到第一个改怎么办呢?你应该已经想到,我们在前面提到的 unshift 方法,来看看这个例子:

let names = ['张三', '罗翔老师', '李四', '王五', '小花'];

names.unshift("小明"); // 在数组首端添加一个元素
console.log(names); // ['小明', '张三', '罗翔老师', '李四', '王五', '小花']

names.shift(); // 弹出数组首端元素
console.log(names); // ['张三', '罗翔老师', '李四', '王五', '小花']


这里之所提到这个,实际上是为了对两者进行比较。因为这么小的数据量根本体现不出来。下面我们来增加一下数据量:

function getPushTime() {
    let nums = [];

    const startTime = new Date().getTime();
    for (let i = 0; i < 100000; i += 1) {
        nums.push(i + 1);
    }
    const endTime = new Date().getTime();

    console.log(endTime - startTime);
}

function getUnshiftTime() {
    let nums = [];

    const startTime = new Date().getTime();
    for (let i = 0; i < 100000; i += 1) {
        nums.unshift(i + 1);
    }
    const endTime = new Date().getTime();

    console.log(endTime - startTime);
}

getPushTime()
getUnshiftTime()

大家可以自行运行一下这两个函数,你会发现,使用 unshift 来添加参数时使用的时间无论怎样都比使用 push 要多得多。

好了,现在大家已经知道这样一个结论:在数组中添加元素,从尾部添加/删除的效率要比从头部添加/删除的效率高的多。

那么为什么从数组的首端添加/删除元素这么耗费性能呢?实际上我们需要首先明确一点,那就是数组时连续存放在数组中的。

下面有一个数组,我们先来看看从首端添加元素

1、现在我们要把 100 添加到首端:

0123456

2、可以看到,首端是没有位置给我们添加新元素的,因此我们首先需要把后面所有的元素整体向后移动一位,修改对应的索引,为 100 空出一个位置:

0123456

3、然后再把 100 插入首位:

1000123456

4、更新 length 属性


问题就出在上面的第二步,为了插入一个元素,我们把后面的每一位都向后移动了一位,试想一下,如果数组有十万个元素,那么我们就会多移动十万次元素,这个代价不可谓不大,也正是这一步耗费了时间与性能。

接下来再来看看从首端删除元素。

1、现在我们要把 100 从首端删除:

1000123456

2、我们删除首端元素:

0123456

3、你以为这样就结束了吗?并没有,数组还需要把所有的元素向左移动,把索引 1 改成 0,2 改成 1 以此类推,对其重新编号:

0123456

4、更新 length 属性

同样的,为了删除一个元素,我们需要把被删除元素后面的所以元素集体向左移动一位。和添加的情况一样,需要耗费大量的时间与性能。

因此,我们可以得出结论:数组里的元素越多,移动它们就要花越多的时间,也就意味着越多的内存操作。

那 push/pop 是什么样的呢:
1、如果从末端移除一个元素,pop 方法只需要清理索引值并缩短 length 就可以了。
2、如果从末端添加一个元素,push方法只需要增加一位,并增加 length 就可以了。

push/pop 方法不需要移动任何东西,因为其它元素都保留了各自的索引。这就是为什么 push/pop 会特别快。


7、数组的比较

我们前面提到,数组是一种特殊的对象。

我们能用 == 或者 === 来比较数组吗?答案当然是不行。

let names1 = ['张三', '罗翔老师', '李四', '王五', '小花'];
let names2 = ['张三', '罗翔老师', '李四', '王五', '小花'];

console.log( names1 == names2 ); // false
console.log( names1 === names2 ); // false

从表面上看,names1 和 names2 是一模一样的数组,但是这只是对我们阅读者而言,对于内存来说,实际上使用 == 或者 === 比较的是两个数组存放于栈内存中的数组首元素的地址而已,真正的数组值是存放于堆内存中的。

因此这样的比较方式是不行的,如果你确实需要对比两个数组,那么可以使用我们前面提到的 every 方法自定义对比方案。但是这样的对比仅仅是非完全的深比较。

关于这一点,我不在这里展开,因为要讲的内容会比较多,感兴趣的同学可以参考:JS 浅拷贝与深拷贝


8、thisArg

几乎所有调用函数的数组方法 —— 比如 find,filter,map,除了 sort 是一个特例,都接受一个可选的附加参数 thisArg。

那么 thisArg 到底是什么呢?它是一个可选参数,如果你不传这个参数,那么函数内的 this 就是 undefined,如果传了 thisArg 这个参数,那么 this = thisArg。

也许到这你仍然不太明白,没有关系,下面我们会用 find 方法来举个例子,会逐步讲解它的具体使用。

首先我们来看看不传 thisArg 的情况:

const students = [
    {id: 101, name: "张三"},
    {id: 102, name: "李四"},
    {id: 103, name: "王五"}
];

students.find(function (ele) {
    if (ele.id === 101) {
        console.log(this); // undefined
    }
});

可以看到,正常情况下在 find 的回调函数中,this 是 undefined 。

那么如果我们传了 thisArg 呢:

const students = [
    {id: 101, name: "张三"},
    {id: 102, name: "李四"},
    {id: 103, name: "王五"}
];

students.find(function (ele) {
    if (ele.id === 101) {
        console.log(this); // {id: 100, name: '罗翔老师'}
    }
}, {id: 100, name: "罗翔老师"});

这时候打印的 this 就变成了我们传入的对象。

这就印证了我们在一开始提到的:它是一个可选参数,如果你不传这个参数,那么函数内的 this 就是 undefined,如果传了 thisArg 这个参数,那么 this = thisArg。


接下来我们看一看另一个实际的例子:

// 学生开始上学年龄最小是 7 岁
const school = {
    minAge: 7,
    canInSchool(stu) {
        return stu.age >= this.minAge;
    }
};

let children = [
    { name: "张三",  age: 5 },
    { name: "罗翔老师",  age: 30 },
    { name: "李四",  age: 6 },
    { name: "王五",  age: 4 },
    { name: "小花",  age: 7 }
];

let students = children.filter(school.canInSchool, school)

console.log(students); // [{ name: "罗翔老师",  age: 30 },{ name: "小花",  age: 7 }]

可以看到,在 filter 中传入的回调函数 canInSchool 中,存在对 this 的调用,而这时候我们指定了 this = thisArg,所以程序能正常执行。

你可以尝试不传 school ,那么此时的 this 就是 undefined ,那么一定会抛出异常:

let students = children.filter(school.canInSchool)

console.log(students); // error:Cannot read properties of undefined (reading 'minAge')

但是在实际的生产开发过程中,大家应该很少见到会这么使用,这是因为在 es6 中出现的箭头函数更加容易解决 this 为 undefined 的问题。就像这样:

let students = children.filter(stu => school.canInSchool(stu))

console.log(students); // [{ name: "罗翔老师",  age: 30 },{ name: "小花",  age: 7 }]

因为箭头函数本身没有 this ,它的 this 会从它外部的第一个普通函数中获取。因此这里的 this 就是 school。

关于箭头函数 和 this 你可以从本系列中对象的章节里了解到,在这里我们不对这些内容展开。


本一小节的内容大家仅作了解即可,一般情况下我们不会采用传入 thisArg 的方式获取 this 。


9、多维数组

数组里的项也可以是数组。我们可以将其用于多维数组,例如存储矩阵:

let matrix = [
    [1, 2, 3, 4, 5, 6],
    [4, 5, 6, 7],
    [7, 8, 9]
];

console.log( matrix.length ); // matrix 的长度:3
console.log( matrix[0].length ); // matrix 中第一个元素的长度: 6
console.log( matrix[0][0] ); // matrix 中第一个元素数组中第一个值:1

使用数组提供的 flat 方法可以帮助我们实现二维数组的扁平化:

let matrix = [
    [1, 2, 3, 4, 5, 6],
    [4, 5, 6, 7],
    [7, 8, 9]
];

console.log( matrix.flat() ); // [1, 2, 3, 4, 5, 6, 4, 5, 6, 7, 7, 8, 9]



至此,关于数组的内容到这里就告一段落了,希望大家阅读完后能有所收获。


接下来我会针对 JavaScript 中最复杂的数据类型(对象:Object)进行介绍,由于对象的知识内容较多,因此可能会分为上中下三部分,敬请期待哦!!!


欢迎大家点赞收藏关注!!!

同时欢迎大家关注我的微信公众号:火锅只爱鸳鸯锅

最后

以上就是辛勤水杯为你收集整理的2.4、JavaScript 数据类型 - 数组的全部内容,希望文章能够帮你解决2.4、JavaScript 数据类型 - 数组所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(42)

评论列表共有 0 条评论

立即
投稿
返回
顶部