深入理解JavaScript数组:玩转数据结构
- 前言
- 数组的基础知识
- 什么是数组
- 如何创建一个数组
- 如何访问数组元素
- 数组的常用操作
- 数组的长度 length
- 更改数组项
- 数组的遍历
- 数组类型的检测
- 数组的添加和删除元素(头尾操作 push()、pop()、unshift()、shift())
- 数组常用方法 splice()、slice()
- 数组常用方法 join()和split()方法
- 数组的合并方法 concat()
- 数组的置反方法 reverse()
- indexof()和includes()方法
- 数组的排序 sort()
- 关于数组在ES6中的增强
- 遍历相关算法
- 数组去重和随机样本
- 冒泡排序
- 二维数组
- 基本类型值和引用类型值
- 浅克隆和深克隆
- 浅克隆
- 深克隆
- 全篇重要知识点
前言
在现代Web开发中,JavaScript数组是不可或缺的一部分。它们是存储和操作数据的强大工具,可以应用于各种场景,从简单的列表到复杂的数据结构。然而,尽管它们是如此常见,但很多开发人员对于JavaScript数组的真正潜力却知之甚少。本文旨在帮助读者深入了解JavaScript数组,解锁它们的全部潜力,并学习如何以更有效的方式利用它们。
数组的基础知识
什么是数组
数组(Array),顾名思义,用来存储一组相关的值,从而方便进行求和、计算平均数、逐项遍历等操作。
每种高级编程语言中都有数组,它是非常重要的一种数据结构。
如何创建一个数组
方法1、定义一个数组非常的简单,只需要使用方括号[]即可
var arr1 = ['A','B','C','D']; //打印信息: ['A', 'B', 'C', 'D']
// 方法一最常见最简单
方法2、数组还可以这样定义:
var arr2 = new Array('A','B','C','D'); //打印信息: ['A', 'B', 'C', 'D']
方法3:
var arr3 = new Array(4); //打印信息:[empty × 4]
如何访问数组元素
JavaScript规定,访问数组中不存在的项会返回undefined,不会报错
数组的常用操作
数组的长度 length
数组的length属性表示它的长度
var arr = ['A','B','C','D'];
console.log(arr.length); // 打印信息: 4
数组最后一项的下标是数组的长度减1 arr[arr.length - 1]
更改数组项
数组并不是只读的,我们可以修改它其中任何项的值
var arr = [2, 6, 7, 3];arr[1]++;arr[2] = 0;console.log(arr); //[2, 7, 0, 3]
如果更改的数组项超过了length - 1 ,则会创造这项
var arr = [2, 6, 7, 3];arr[6] = 4;console.log(arr); //[2, 6, 7, 3, empty × 2, 4]
数组的遍历
数组的最大的优点就是方便遍历
var arr = [2, 6, 7, 3];for (var i = 0; i < arr.length; i++) {console.log(arr[i]);}
数组类型的检测
- 数组用typeof 检测结果是object
- Array.isArray()方法(兼容到IE8)可以用来检测数组
var arr = [2, 6, 7, 3];console.log(typeof (arr)); //objectconsole.log(Array.isArray(arr)); //true
- 鸭式辨型 检测方法
JavaScript 中的鸭式辨型(Duck Typing)是一种动态类型检查方法,它不关心对象的实际类型,而是关注对象是否具有特定的属性和方法。这种方法的核心思想是,如果某个对象具有和“鸭子”相似的行为(比如像鸭子一样会走、游泳、嘎嘎叫),那么它就可以被视为“鸭子”。
在 JavaScript 中,进行鸭式辨型的方法主要有两种:
-
检查对象的属性或方法是否存在:
function canQuackAndFly(obj) {return typeof obj.quack === 'function' && typeof obj.fly === 'function'; }const duck = {quack() {console.log('Quack!');},fly() {console.log('Flying!');} };const notADuck = {quack: 'I can quack, but not really',fly: 'I can fly, but just a little' };console.log(canQuackAndFly(duck)); // true console.log(canQuackAndFly(notADuck)); // false
-
使用 instanceof 运算符:
function Duck() {this.quack = function() {console.log('Quack!');};this.fly = function() {console.log('Flying!');}; }const duck = new Duck(); const notADuck = {};console.log(duck instanceof Duck); // true console.log(notADuck instanceof Duck); // false
这两种方法都是基于对象的行为来判断对象的类型。鸭式辨型在 JavaScript 中很常见,因为 JavaScript 是一种动态类型语言,对象的类型可以在运行时改变,因此更关注对象的行为而不是它们的类型。
数组的添加和删除元素(头尾操作 push()、pop()、unshift()、shift())
数组常用方法 splice()、slice()
splice() 方法用于替换数组中的指定项
slice()
- slice() 方法用于得到子数组,类似于字符串的slice()方法
- slice(a,b)截取的子数组从下标为a的项开始,到下标为b(但不包括下标为b的项)结束
- slice(a,b)方法不会更改原有数组
- slice()如果不提供第二个参数,则表示从指定项开始,提取所有后续所有项作为子数组
- slice()方法的参数允许为负数,表示数组的倒数第几项
数组常用方法 join()和split()方法
数组的join()方法可以使数组转为字符串;字符串的split()方法可以使字符串转为数组。
join()
join()的参数表示以什么字符作为连接符,如果留空则默认以逗号分隔,如同调用tostring()方法
splict() 注意:splict是字符串的方法
split()的参数表示以什么字符拆分字符串,一般不能留空
字符串和数组更多相关性
字符串也可以使用方括号内写下标的形式,访问某个字符等价于charAt()方法
字符串的一些算法问题有时候会转换为数组解决
数组的合并方法 concat()
concat()方法,可以合并连结多个数组
concat()方法 不会改变原数组
数组的置反方法 reverse()
reverse()方法用来将一个数组中的全部项顺序置反
题目:如何把 'ABCDEFG’置反
var s = 'ABCDEFG'
console.log(s.split('').reverse().join(''));
indexof()和includes()方法
index0f()方法的功能是搜索数组中的元素,并返回它所在的位置,如果元素不存在,则返回-1
const beasts = ['ant', 'bison', 'camel', 'duck', 'bison'];
console.log(beasts.indexOf('bison'));
// Expected output: 1
includes()方法的功能是判断一个数组是否包含一个指定的值,返回布尔值
const array1 = [1, 2, 3];
console.log(array1.includes(2));
// Expected output: true
数组的排序 sort()
数组有sort()方法可以用于数组排序,但是涉及到函数的相关知识,在函数进行介绍。
快速了解:sort()方法
除内置排序方法外,还有一些排序算法:冒泡排序稍后介绍,快速排序在函数介绍。
关于数组在ES6中的增强
数组在ES6中新增了较多新的方法,快速了解:
ES6(ECMAScript 2015)为 JavaScript 带来了许多强大的新特性,其中也包括对数组的增强。以下是一些 ES6 中数组增强的主要特性:
-
扩展运算符(Spread Operator):扩展运算符用于展开数组,可以在函数调用/数组字面量中方便地展开数组元素。例如:
const arr1 = [1, 2, 3]; const arr2 = [4, 5, 6]; const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
-
解构赋值(Destructuring Assignment):解构赋值允许将数组(或对象)的元素赋值给多个变量。例如:
const [a, b, c] = [1, 2, 3]; console.log(a); // 1 console.log(b); // 2 console.log(c); // 3
-
Array.from() 方法:Array.from() 方法用于将类似数组或可迭代对象转换为真正的数组。例如:
const str = 'hello'; const chars = Array.from(str); // ['h', 'e', 'l', 'l', 'o']
-
Array.of() 方法:Array.of() 方法用于创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。例如:
const arr = Array.of(1, 'two', 3.14, [4, 5]); console.log(arr); // [1, 'two', 3.14, [4, 5]]
-
find() 和 findIndex() 方法:这两个方法用于在数组中查找元素,find() 返回第一个满足条件的元素,而 findIndex() 返回第一个满足条件的元素的索引。例如:
const numbers = [1, 2, 3, 4, 5]; const evenNumber = numbers.find(num => num % 2 === 0); // 2 const evenIndex = numbers.findIndex(num => num % 2 === 0); // 1
-
includes() 方法:includes() 方法用于判断数组是否包含某个元素,返回布尔值。例如:
const array = [1, 2, 3, 4, 5]; console.log(array.includes(3)); // true console.log(array.includes(6)); // false
这些特性的引入极大地提升了 JavaScript 中数组的处理和操作的灵活性和便利性。
遍历相关算法
题目:求数组中每一项的总和
var scoreArr = [1, 2, 3]for (var i = 0, sum = 0; i < scoreArr.length; i++) {sum += scoreArr[i]}console.log(sum);
题目:求数组项的最大值和最小值
var scoreArr = [1, 2, 3]var max = scoreArr[0]var min = scoreArr[0]for (var i = 0; i < scoreArr.length; i++) {if (max > scoreArr[i]) {max = scoreArr[i]}if (min < scoreArr[i]) {min = scoreArr[i]}}console.log(max, min);
数组去重和随机样本
题目:去掉数组中的重复项
var scoreArr = [1, 2, 3, 3, 2, 2, 4];//思路:准备一个空结果数组,遍历原数组,如果遍历到的项不在结果数组中,则推入结果数组var arr = [];for (var i = 0; i < scoreArr.length; i++) {if (!arr.includes(scoreArr[i])) {arr.push(scoreArr[i])}}console.log(arr);
题目:请随机从原数组中取3项
思路:准备一个空结果数组,遍历原数组,随机选择一项,推入结果数组,并且将这项删除
var arr = [1, 2, 3, 4, 5];var result = [];//遍历数组for (var i = 0; i < 3; i++) {//随机选择一项的下标,数组的下标 0~ arr.length -1 ;//之前学习过一个random的公式,[a,b]区间的随机数是parseInt(Math.random()*(b-a+1))+a;var n = parseInt(Math.random() * arr.length);//把这项推入结果数组result.push(arr[n]);//删除这项,防止重复被随机到arr.splice(n, 1);}console.log(result);
冒泡排序
- 冒泡排序是一个著名的排序算法,也是在面试时非常爱考察的算法
- 冒泡排序的核心思路是一趟一趟地进行多次项的两两比较每次都会将最小的元素排好位置,如同水中的气泡上浮一样
- 4个数字,共需要比较3趟,比较次数为3+2+1=6次
- n个数字,共需要比较n-1趟,比较次数为n(n-1)/2次
var arr = [6, 9, 2, 1, 8, 5, 4];//一趟一趟比较,趟数序号就是ifor (var i = 1; i < arr.length; i++) {//内层循环负责两两比较for (var j = arr.length - 1; j >= i; j--) {//判断项的大小if (arr[j] < arr[j - 1]) {var temp = arr[j];arr[j] = arr[j - 1];arr[j - 1] = temp;}}}console.log(arr); //[1, 2, 4, 5, 6, 8, 9]
//方法二得到同样的结果:
function compareNumbers(a, b) {return a - b;
}
console.log(arr.sort(compareNumbers)); //[1, 2, 4, 5, 6, 8, 9]
二维数组
在JavaScript中,二维数组是指一个数组中包含了其他数组作为其元素的数组。换句话说,它是一个数组的数组,每个元素都是一个数组。二维数组在 JavaScript 中是很常见的数据结构,用于表示二维表格或矩阵等数据结构。
例如,一个简单的二维数组可以表示一个包含行和列的矩阵:
var matrix = [[1, 2, 3],[4, 5, 6],[7, 8, 9]
];
这个二维数组 matrix
包含了3个子数组,每个子数组代表矩阵中的一行。可以通过索引访问其中的元素,例如 matrix[0][0]
表示矩阵的第一行第一列的元素,其值为 1。
二维数组在 JavaScript 中通常用于表示二维的数据结构,比如游戏地图、表格数据、图像数据等。对于多维数据的操作,通常需要嵌套循环来遍历访问。
基本类型值和引用类型值
在JavaScript中,基本类型值和引用类型值是两种不同的数据类型,它们在内存中的存储方式和操作行为有所不同。
- 基本类型值:
- JavaScript中的基本类型值包括字符串string、数字number、布尔值boolean、null和undefined。
- 基本类型值存储在栈内存中,每个变量都有自己的内存空间,它们的值直接存储在变量访问的位置。
- 当你将一个基本类型值赋给另一个变量时,会创建一个新的独立的副本,修改其中一个变量的值不会影响另一个变量。
let a = 5;
let b = a; // 基本类型值的复制
b = 10;
console.log(a); // 输出: 5
console.log(b); // 输出: 10
- 引用类型值:
- 引用类型值包括对象object、数组array和函数function。
- 引用类型值存储在堆内存中,变量中存储的是对它们的引用(即内存地址)。
- 当你将一个引用类型值赋给另一个变量时,实际上是将相同的引用复制给了另一个变量,它们指向同一个对象,因此修改其中一个变量会影响到另一个变量。
let obj1 = { name: 'Alice' };
let obj2 = obj1; // 引用类型值的复制
obj2.name = 'Bob';
console.log(obj1.name); // 输出: 'Bob'
console.log(obj2.name); // 输出: 'Bob'
因此,在JavaScript中,了解基本类型值和引用类型值的区别对于编写高效的代码和避免意外行为非常重要。
浅克隆和深克隆
浅克隆
浅克隆是指创建一个新对象,该对象具有与原始对象相同的属性和值,但是对于对象中的嵌套对象(引用类型值),只复制了它们的引用而不是深层复制它们的内容。在JavaScript中,可以使用几种方法来进行浅克隆。
- 使用对象展开运算符(Spread Operator):
const originalObj = { a: 1, b: { c: 2 } };
const clonedObj = { ...originalObj };clonedObj.b.c = 3; // 修改克隆后的对象的嵌套对象属性
console.log(originalObj.b.c); // 输出: 3,因为只是引用的复制
- 使用Object.assign()方法:
//OObject.assign(target, ...sources):如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。
const originalObj = { a: 1, b: { c: 2 } };
const clonedObj = Object.assign({}, originalObj);clonedObj.b.c = 3; // 修改克隆后的对象的嵌套对象属性
console.log(originalObj.b.c); // 输出: 3,因为只是引用的复制
- 使用Array.prototype.slice()方法(仅适用于数组):
const originalArr = [1, 2, { a: 3 }];
const clonedArr = originalArr.slice();clonedArr[2].a = 4; // 修改克隆后的数组的嵌套对象属性
console.log(originalArr[2].a); // 输出: 4,因为只是引用的复制
这些方法都可以实现浅克隆,但需要注意的是,嵌套对象的修改会影响到原始对象和克隆对象,因为它们共享相同的引用。如果需要深层复制对象,即使嵌套对象也被复制而不是共享引用,需要使用深层克隆技术。
深克隆
深克隆是指创建一个新对象,该对象具有与原始对象相同的属性和值,包括原始对象中的所有嵌套对象,而不是共享引用。在JavaScript中,实现深克隆可以使用递归或其他方法来遍历对象的所有属性并进行复制。
以下是一种实现深克隆的简单方法:
方法一:
// 深拷贝
const obj3 = { a: 0, b: { c: 0 } };
const obj4 = JSON.parse(JSON.stringify(obj3));
//JSON.parse()
//解析 JSON 字符串并返回对应的值,可以额外传入一个转换函数,用来将生成的值和其属性,在返回之前进行某些修改。
//JSON.stringify()
//返回与指定值对应的 JSON 字符串,可以通过额外的参数,控制仅包含某些属性,或者以自定义方法来替换某些属性值。
obj3.a = 4;
obj3.b.c = 4;
console.log(obj4); // { a: 0, b: { c: 0 } }
方法二:
function deepClone(obj) {// 检查是否为对象,如果不是,则直接返回if (obj === null || typeof obj !== 'object') {return obj;}// 创建一个新的对象或数组来存储克隆后的数据const clone = Array.isArray(obj) ? [] : {};// 遍历原始对象的属性,并递归调用deepClone函数进行深层复制for (let key in obj) {if (Object.prototype.hasOwnProperty.call(obj, key)) {clone[key] = deepClone(obj[key]);}}return clone;
}
使用示例:
const originalObj = {a: 1,b: {c: 2,d: [3, 4]}
};const clonedObj = deepClone(originalObj);clonedObj.b.c = 5;
clonedObj.b.d.push(6);console.log(originalObj); // 输出: { a: 1, b: { c: 2, d: [3, 4] } }
console.log(clonedObj); // 输出: { a: 1, b: { c: 5, d: [3, 4, 6] } }
这个实现通过递归地遍历对象的所有属性和嵌套对象,并创建它们的副本,从而实现了深度复制。需要注意的是,这种方法对于循环引用的情况可能会导致栈溢出,因此在处理可能包含循环引用的对象时需要添加相应的处理逻辑。
全篇重要知识点
- 数组是什么?应该如何定义?
- 如何检测数组类型?
- 数组有哪些常用方法?
- 数组的遍历相关算法、去重和随机样本、冒泡排序
- 基本类型值和引用类型的区别
- 实现浅克隆和深克隆