8. Array 数组
朋友们,大家好!本期内容是《你应该掌握的 JavaScript 高阶技能》的第八期,之前写过一篇数组讲解文章,但不够全面,留下了许多坑,所以本文的主要内容:es5数组方法总结(面试常客),望本文能对您有所帮助!☀️
(本文参考❤️)
- JavaScript 高级程序设计(第四版)
- JavaScript 忍者秘籍(第二版)
目录
8.1 数组创建
- 使用数组字面量
//字面量
let arr = [];
- 使用内置 Array 构造函数
// 构造函数
// 语法:let arr = new Array(2);
// Array() 构造函数的参数为 2 ,表示生成一个两个成员的数组,每个位置都是空值。
// length 属性 --> 数组大小
let arr = new Array(2);
console.log(arr.length);// 2
console.log(arr);// []
// 没有使用 new 关键字
// let arr1 = Array(2);
// 等同于
let arr1 = new Array(2);
let arr2 = ["red","green","blue"];
console.log(arr[3] === undefined);// true
-
考虑到语义性,以及与其他构造函数用法保持一致,建议总是加上
new
。 -
使用
Array()
构造函数,不同的参数个数会导致不一致的行为。
注意事项
- 优先使用数组字面量创建数组。
- 由于 JavaScript 的高度动态特性,无法阻止修改内置的 Array 构造函数,也就意味着
new Array()
创建的不一定是数组。因此,推荐坚持使用数组字面量。
- JavaScript 在
length
属性上,表现出一种特殊的功能: 可以手动修改length
属性的值。将length
值改为比原有值大的数,数组会被扩展,新扩展出的元素均为undefined
;将length
值改为比原有值小的数,数组会被裁减。
- 数组空位问题
8.2 检测数组
- 一个经典的
ECMAScript
问题是判断一个对象是不是数组。通常情况下,我们使用instanceof
操作符。
let arr = [];
let obj = {};
console.log(arr instanceof Array);// true
console.log(obj instanceof Object);// true
// typeof 运算符无法判断
console.log(typeof arr);// "object"
let arr = [];
console.log(Array.isArray(arr));// true
8.3 实例方法
8.3.1 转换方法
valueOf
、toString
方法是一个所有对象都拥有的方法。其中,valueOf()
返回的还是数组本身;toString()
返回由数组中每个值的等效字符串拼接而成的一个逗号分隔的字符串。
var arr = [1, 2, 3];
arr.valueOf(); // [1, 2, 3]
arr.toString(); // "1,2,3"
var arr2 = [1, 2, 3, [4, 5, 6]];
arr2.toString(); // "1,2,3,4,5,6"
join()
方法以指定参数作为分隔符,将所有数组成员连接为一个字符串返回。如果不提供参数,默认用逗号分隔。
var arr = [1,2,3,4];
var str = arr.join(' '); // 空格作为分隔符
// '1 2 3 4'
var str1 = arr.join('|'); // | 作为分割符
// '1|2|3|4'
var str2 = arr.join(); // 逗号作为分隔符
// '1,2,3,4'
- 数组成员是
undefined
或null
或空位,会被转成空字符串。
[undefined, null].join('#');
// '#'
['a',, 'b'].join('-');
// 'a--b'
- 通过
call
方法,这个方法也可以用于字符串或类似数组的对象。
Array.prototype.join.call('hello', '-');
// [].join.call('hello','-')
// "h-e-l-l-o"
var obj = {0: 'a',1: 'b',length: 2}; // 类对象 有 length 属性
// [].join.call('obj','-')
Array.prototype.join.call(obj,'-');
// 'a-b'
8.3.2 栈方法
-
栈(
stack
)是一种后进先出(LIFO,Last-In-First-Out
)的数据结构。ECMAScript
数组提供了push()
和pop()
方法,以实现类似栈的行为。 -
push
:在数组末尾添加添加一个或多个元素,返回添加新元素后的数组长度。注意,该方法会改变原数组。 -
pop
:从数组末尾删除元素,返回该元素。注意,该方法会改变原数组。
let arr = [];
arr.push(1); // 1
arr.push('a') // 2
arr.push(true, {}); // 4
console.log(arr); // [1, 'a', true, {}]
console.log(arr.pop());// {}
console.log(arr); // [1, 'a', true]
- 对空数组使用
pop
方法,不会报错,而是返回undefined
。
console.log([].pop());// undefined
8.3.3 队列方法
-
队列(
queue
)是一种先进先出(FIFO,First-In-First-Out
)的数据结构。 -
具体:队列在列表末尾添加数据(使用
push
),但从列表开头获取数据并删除(使用shift
),二者结合使用,就构成了“先进先出”的队列结构。 -
shift()
方法用于删除数组的第一个元素,并返回该元素。注意,该方法会改变原数组。 -
unshift()
方法用于在数组的第一个位置添加元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。
var a = ['a','b','c'];
console.log(a.shift()); // 'a'
console.log(a); // ['b','c']
shift()
方法可以遍历并清空一个数组。
// shift() 方法
var list = [1, 2, 3, 4];
var item;
// list.shift() 每次返回 list 数组中删除的【第一个】元素
// 缺点:数组元素不能是 0 或任何布尔值等于 false 的元素
while (item = list.shift()) {
console.log(item);
}
console.log(list); // []
// unshift() 方法 --> 在数组的第一个位置添加元素
var a = ['a','b','c'];
a.unshift('x'); // 返回数组长度为 4
console.log(a); // ['x','a','b','c']
a.unshift('d','e'); // 6
console.log(a) // ['d','e','x','a','b','c']
8.3.4 操作方法
concat 方法
concat
方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变。
['hello'].concat(['world']);
// ["hello", "world"]
['hello'].concat(['world'], ['!']);
// ["hello", "world", "!"]
[].concat({a: 1}, {b: 2});
// [{ a: 1 }, { b: 2 }]
[2].concat({a: 1});
// [2, {a: 1}]
- 除了数组作为参数,
concat
也接受其他类型的值作为参数,添加到目标数组尾部。
[1, 2, 3].concat(4, 5, 6)
// [1, 2, 3, 4, 5, 6]
- 如果数组成员包括对象,
concat
方法返回当前数组的一个浅拷贝。 - 浅拷贝指的是新数组拷贝的是对象的引用。
var obj = { a: 1 };
var oldArray = [obj, true];
var newArray = oldArray.concat();
// [ { a: 1 }, true ];
obj.a = 2;
oldArray[0].a // 2
newArray[0].a // 2
- 上面代码中,原数组包含一个对象,
concat
方法生成的新数组包含这个对象的引用。改变原对象,新数组也会改变。
slice 方法
slice()
用于创建一个包含原数组中一个或多个元素的新数组。接收一个或两个参数:返回元素的开始索引和结束索引,该方法操作不影响原数组。
arr.slice(start,end);
- 如果只有一个参数,则会返回该索引到数组末尾的所有元素。
var arr = [1,2,3,4];
arr.slice(1);// [2,3,4]
- 如果有两个参数,则返回从开始索引到结束索引对应的所有元素,其中不包含结束索引对应的元素。
var arr = ["red","green","blue","pink","yellow"];
arr.slice(2,4);// ['blue', 'pink']
- 如果
slice()
方法的参数是负数,那么就以数组长度加上这个负值的结果确定位置。比如:在包含 5 个元素的数组上调用slice(-2,-1)
,就相当于调用slice(5+(-2),5+(-1)) 即 slice(3,4)
。
var a = ['a', 'b', 'c','d','e'];
a.slice(-2) // ["d", "e"]
a.slice(-2, -1) // ["d"]
- 如果第一个参数大于等于数组长度,或者结束位置小于开始位置,则返回空数组。
var a = ['a', 'b', 'c'];
a.slice(4) // []
a.slice(2, 1) // []
slice()
方法的一个重要应用,是将类似数组的对象转为真正的数组。
Array.prototype.slice.call({ 0: 'a', 1: 'b', length: 2 })
// ['a', 'b']
Array.prototype.slice.call(document.querySelectorAll("div"));
Array.prototype.slice.call(arguments);
splice 方法
splice()
方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。该方法会改变原数组。
arr.splice(start, count, addElement1, addElement2, ...);
splice
的第一个参数是删除的起始位置(从0开始),第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。
var arr = ['a','b','c','d','e','f'];
arr.splice(3,2);// 返回删除元素 ["d","e"]
console.log(arr);
// 从下标索引为 3 的位置 删除 2 个数组元素
var arr1 = ['a','b','c','x','y'];
// 删除从下标索引为 3 的位置 删除 2 个数组元素且添加两个元素 'd' 和 'e'
arr1.splice(3,2,'d','e');// ['x', 'y']
console.log(arr1);// ['a', 'b', 'c', 'd', 'f']
// 起始位置如果是负数,就表示从倒数位置开始删除。
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(-4, 2); // ["c", "d"]
- 如果只是单纯地插入元素,
splice
方法的第二个参数可以设为0
var arr = [1, 1, 1];
arr.splice(1, 0, 2) // []
arr // [1, 2, 1, 1]
- 如果只提供第一个参数,等同于将原数组在指定位置拆分成两个数组。
var arr = [1, 2, 3, 4];
arr.splice(2) // [3, 4]
arr // [1, 2]
8.3.5 排序方法
-
数组有两个方法可以用来对数组元素重新排序:
reverse()
和sort()
,两方法都返回它们数组的引用。 -
reverse()
方法将数组元素反向排列。
let values = [1, 2, 3, 4, 5];
values.reverse();
console.log(values); // 5,4,3,2,1
手写 reverse 方法
function newReverse(arr) {
for(let i = 0; i < (arr.length)/2; i++) {
let temp = arr[i];
arr[i] = arr[arr.length - i - 1];
arr[arr.length - i - 1] = temp;
}
return arr;
}
function newReverse_1(arr) {
let temp_arr = [];
for(let i = arr.length - 1; i >= 0; i--) {
temp_arr.push(arr[i]);
}
return temp_arr;
}
// 思考一下?其他实现方式?
sort
方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变。
['d', 'c', 'b', 'a'].sort();
// ['a', 'b', 'c', 'd']
[4, 3, 2, 1].sort();
// [1, 2, 3, 4]
[11, 101].sort();
// [101, 11]
[10111, 1101, 111].sort();
// [10111, 1101, 111]
-
上面代码的最后两个例子,需要特殊注意。
sort()
方法不是按照大小排序,而是按照字典顺序。 -
也就是说,数值会被先转成字符串,再按照字典顺序进行比较,所以
101
排在11
的前面。 -
如果想让
sort
方法按照自定义方式排序,可以传入一个函数作为参数。
[5,4,3].sort(function (a, b) {
return a - b;
});
// [3,4,5]
sort
的参数函数本身接受两个参数,表示进行比较的两个数组成员。如果该函数的返回值大于0
,表示第一个成员排在第二个成员后面(默认升序);其他情况下,都是第一个元素排在第二个元素前面。- 我们可以定义一个比较函数
compare
,用于判断哪个值应该排在前面。
// 比较函数 compare
// 比较函数接收两个参数,如果第一个参数应该排在第二个参数前面,就返回负值;如果两个参数相等,就返回 0;如果第一个参数应该排在第二个参数后面,就返回正值
// 下面代码注释的实现为降序
function compare(value1, value2) {
if(value1 < value2) {
return -1;
// return 1;
} else if(value1 > value2) {
return 1;
// return -1;
} else {
return 0;
}
}
var arr = [1,2,3];
arr.sort(compare);
console.log(arr);
- 应用
let arr = [
{ name: "张三", age: 30 },
{ name: "李四", age: 25 },
{ name: "王五", age: 46 },
{ name: "柳六", age: 45 },
{ name: "琪七", age: 20 },
];
arr.sort(function compare(o1,o2){
return o1.age - o2.age;
});
console.log(arr);
/*[
{ name: "琪七", age: 20 },
{ name: "李四", age: 25 },
{ name: "张三", age: 30 },
{ name: "柳六", age: 45 },
{ name: "王五", age: 46 },
];*/
8.3.6 搜索和位置方法(一)
-
ECMAScript
提供两类搜索数组的方法:按严格相等搜索和按断言函数搜索(es6)。 -
严格相等搜索
:indexOf() lastIndexOf()
-
indexOf
方法返回给定元素在数组中第一次出现的位置,如果没有出现则返回-1
。
var a = ['a', 'b', 'c'];
a.indexOf('b') // 1
a.indexOf('y') // -1
indexOf
方法还可以接受第二个参数,表示搜索的开始位置。
['a', 'b', 'c'].indexOf('a', 1) // -1
indexOf
应用
例1:数组去重
核心算法:遍历原数组,使用原数组的元素查询新数组,如果该元素在新数组里面没有出现过,就添加该元素,否则不添加。
let fn = function(arr) {
let newArr = [];
for(let i = 0; i < arr.length; i++) {
if(newArr.indexOf(arr[i]) === -1){
newArr.push(arr[i]);
}
}
return newArr;
}
var arr = ['a','b','x','a','c','b'];
var newArr = fn(arr);
console.log(newArr); // ['a','b','x','c']
例2:查找字符串中某个字符出现次数
利用
indexOf
的第二个参数
var str = 'abcoefoxyozzopp';
var index = str.indexOf('o');
var count = 0;
while(index !== -1) {
console.log(index);
count++;
index = str.indexOf('o',index+1);
}
console.log('o出现的次数是' + count);
console.log('-------------------');
// 变式:如果是统计某个字符串在字符串数组中出现的次数
var str1 = ['red', 'blue', 'red', 'green', 'pink', 'red'];
var count1 = 0;
// var index1 = str1.indexOf('blue'); // 1
for(var index1 = str1.indexOf('red'); index1 !== -1; index1 = str1.indexOf('red',index1 + 1)) {
console.log(index1);
count1++;
}
// 封装一个函数 ==> 统计某个字符或字符串在字符串或字符串数组中出现的次数
function newIndexOf(str,ch) {
// str 字符串或字符串数组 ch 查找字符或字符串
var index = str.indexOf(ch); // 第一次出现位置
var count = 0;
while(index !== -1) {
// 继续查找
console.log(index);
count++;
index = str.indexOf(ch,index + 1);
}
return count;
/*
其他做法:while()部分改变
for(var index = str.indexOf(ch); index !== -1; index = str.indexOf(ch,index+1)) {
console.log(index);
count++;
}
*/
}
console.log('-------------------');
newIndexOf(str1,'red');
console.log('-------------------');
newIndexOf(str,'o');
例3:统计字符串中出现最多的字符和其次数
核心算法:利用
charAt()
遍历字符串str
,把每个字符都存储给对象obj
,如果对象没有该属性就设置值为1 ,如果存在就原值加1,然后遍历对象,得到最大值和该字符。
let fn1 = function(str) {
let obj = {};
let chars;
for(let i = 0; i < str.length; i++) {
chars = str.charAt(i);
if(obj[chars]) {
obj[chars]++;
} else {
obj[chars] = 1;
}
}
return obj;
}
let max = 0;
let ch = '';
for (let k in obj) {
if(obj[k] > max) {
max = obj[k];
ch = k;
}
}
console.log("字符串 " + ch + " 出现次数最多且为 " + max);
lastIndexOf
方法返回给定元素在数组中最后一次出现的位置,如果没有出现则返回-1
。
var a = [2, 5, 9, 2];
a.lastIndexOf(2) // 3
a.lastIndexOf(7) // -1
- 注意,这两个方法不能用来搜索
NaN
的位置,即它们无法确定数组成员是否包含NaN
。
[NaN].indexOf(NaN) // -1
[NaN].lastIndexOf(NaN) // -1
- 这是因为这两个方法内部,使用严格相等运算符(
===
)进行比较,而NaN
是唯一一个不等于自身的值。
8.3.7 迭代方法
ECMAScript
为数组定义了 5 个迭代方法。every some map forEach filter
- 每个方法接收两个参数:以每一项为参数运行的函数, 以及可选的作为函数运行上下文的作用域对象(影响函数中
this
的值)。 - 传给每个方法的函数接收 3 个参数:数组元素、元素索引和数组本身。因具体方法而异,这个函数的执行结果可能会也可能不会影响方法的返回值。
挨个学习!<参数于下述不多赘述>
every()
:对数组每一项都运行传入的函数,如果对每一项函数都返回true
,则这个方法返回true
。
// Array.prototype.every(Function,this);
let arr = [1,2,3,4,5,4,3,2,1];
let everyResult = arr.every((item,index,array) => item > 2); // false
some()
:跟every
方法类似,对数组每一项都运行传入的函数,只要有一项函数返回true
,则这个方法返回true
。
var arr = [1, 2, 3, 4, 5];
arr.some(function (elem, index, arr) {
return elem >= 3;
});
// true
- 注意:
some
方法从数组的第 1 项开始执行回调函数,直到回调函数返回true
,意味着如果其中一项回调函数返回true
,后续元素不再继续检查。
- 注意,对于空数组,
some
方法返回false
,every
方法返回true
,回调函数都不会执行。
function isEven(x) { return x % 2 === 0 }
[].some(isEven); // false
[].every(isEven); // true
map
方法 :将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回。
var arr = [1, 2, 3];
var arr1 = arr.map(function (n) {
return n + 1;
});
console.log(arr,arr1);
var arr2 = arr.map(function(elem, index, arr) {
console.log(elem,index,arr);
return elem * index;
});
console.log(arr2);
// [0,2,6]
const ninjas = [
{name: "Yagyu", weapon: "shuriken"},
{name: "Yoshi", weapon: "katana"},
{name: "Kuma", weapon: "wakizashi"}
];
- 下述示例通过
map()
方法的第二个参数,将回调函数内部的this
对象,指向arr
数组。
var arr = ['a', 'b', 'c'];
[1, 2].map(function (item) {
return this[item];
}, arr);
// ['b','c']
- 如果数组有空位,
map()
方法的回调函数在这个位置不会执行,会跳过数组的空位。
var f = function (n) { return 'a' };
[1, undefined, 2].map(f) // ["a", "a", "a"]
[1, null, 2].map(f) // ["a", "a", "a"]
[1, , 2].map(f) // ["a", , "a"]
// map 方法 不会跳过 undefined 和 null,但会跳过空位
-
forEach
方法:跟map
方法类似。对数组每一项都运行传入的函数,没有返回值。 -
所以,如果数组遍历的目的是为了得到返回值,那么使用
map()
方法,否则使用forEach()
方法。
function log(element, index, array) {
console.log('[' + index + '] = ' + element);
}
[2, 5, 9].forEach(log);
// [0] = 2
// [1] = 5
// [2] = 9
- 下述代码中,空数组
out
是forEach()
方法的第二个参数,意味着回调函数内部的this
就指向out
var out = [];
[1, 2, 3].forEach(function(elem) {
this.push(elem * elem);
}, out);
out // [1, 4, 9]
- 注意,
forEach()
方法使用break
是无法中断执行,总是会将所有成员遍历完。如果想中断执行,可以用try/catch
中throw new Error
来停止。
- 如果希望符合某种条件时,就中断遍历,请要使用
for
循环。 forEach()
方法也会跳过数组的空位。
var log = function (n) {
console.log(n + 1);
};
[1, undefined, 2].forEach(log);
// 2
// NaN
// 3
[1, null, 2].forEach(log);
// 2
// 1
// 3
[1, , 2].forEach(log);
// 2
// 3
- forEach 同/异步问题
filter
方法用于过滤数组成员,满足条件的成员组成一个新数组返回。
[1, 2, 3, 4, 5].filter(function (elem) {
return (elem > 3);
})
// [4, 5]
// 将大于3的数组元素作为新数组返回
var arr = [0,1,'a',true,undefined,NaN,false,null];
var arr1 = arr.filter(Boolean);
console.log(arr1);
// [1,'a',true]
// 返回偶数位置的成员组成的新数组
[1, 2, 3, 4, 5].filter(function (elem, index, arr) {
return index % 2 === 0;
});
// [1, 3, 5]
- 下面代码中,过滤器
myFilter()
内部有this
变量,它可以被filter()
方法的第二个参数obj
绑定,返回大于3
的成员。
var obj = { MAX: 3 };
var myFilter = function (item) {
if (item > this.MAX) return true;
};
var arr = [2, 8, 3, 4, 1, 3, 2, 9];
arr.filter(myFilter, obj) // [8, 4, 9]
8.3.8 归并方法
ECMAScript
为数组提供了两个归并方法:reduce()
和reduceRight()
。这两个方法都会迭代数组的所有项,并在此基础上构建一个最终返回值。reduce()
方法从数组第一项开始遍历到最后一项。 而reduceRight()
从最后一项开始遍历至第一项。- 这两个方法都接收两个参数:对每一项都会运行的归并函数,以及可选的以之为归并起点的初始值。
- 传给
reduce()
和reduceRight()
的函数接收 4 个参数:上一个归并值、当前项、当前项的索引和数组本身。- 注意:
- 上一个归并值:第一次执行时,默认为数组的第一个成员;以后每次执行时,都是上一轮的返回值。
- 当前项:第一次执行时,默认为数组的第二个成员;以后每次执行时,都是下一个成员。
- 注意:
// 语法:
[/*...*/].reduce(function (
a, // 上一个归并值,必须
b, // 当前项,必须
i, // 当前索引,可选
arr // 原数组,可选
) {
// ... ...
来点栗子 !
[1, 2, 3, 4, 5].reduce(function (a, b) {
console.log(a, b);
return a + b;
})
// 1 2
// 3 3
// 6 4
// 10 5
//最后结果:15
/*
执行流程:
第一次执行:a 是数组的第一个成员且值为 1,b 是数组的第二个成员且值为 2。
第二次执行:a 为上一轮的返回值 3,b为第三个成员且值为 3。
第三次执行:a 为上一轮的返回值 6,b为第四个成员且值为 4。
第四次执行:a 为上一轮返回值 10,b 为第五个成员且值为 5。至此所有成员遍历完成,整个方法的返回值就是最后一轮的返回值 15。
*/
- 如果要对累积变量指定初值,可以把它放在
reduce()
方法和reduceRight()
方法的第二个参数。
[1, 2, 3, 4, 5].reduce(function (a, b) {
return a + b;
}, 10);
// 25
- 建议总是加上第二个参数,这样比较符合直觉,每个数组成员都会依次执行
reduce()
方法的参数函数。另外,第二个参数可以防止空数组报错。
function add(prev, cur) {
return prev + cur;
}
[].reduce(add);
// TypeError: Reduce of empty array with no initial value
[].reduce(add, 1);
// 1
- 上面代码中,由于空数组取不到累积变量的初始值,
reduce()
方法会报错。这时,加上第二个参数,就能保证总是会返回一个值。
reduceRight()
方法与reduce
区别只是方向相反。
function subtract(prev, cur) {
return prev - cur;
}
[3, 2, 1].reduce(subtract); // 0
[3, 2, 1].reduceRight(subtract); // -4
使用 reduce 还是 reduceRight 取决于遍历数组元素的方向。
应用:找出字符长度最长的数组成员
function findLongest(strArr) {
return strArr.reduce(function (longest, item) {
return item.length > longest.length ? item : longest;
}, '');
}
var strArr = ['aaa','bb','cccc','ddd'];
console.log(findLongest(strArr)); // 'cccc'
第八期学习内容就这么多啦,如果您觉得内容不错的话,望您能关注🤞点赞👍收藏❤️一键三连!