JS中多方式数组复制知识扩展
- 前言
- 浅拷贝
- JavaScript 展开操作符
- for() 循环
- 其他:
- array.forEach
- forEach方法详解
- array.map
- map()方法详解
- array.filter
- filter()方法详解
- array.reduce
- reduce()方法详解
- array.slice
- slice()方法详解
- Array.from
- Array.from()方法详解
- 深拷贝
前言
在这篇文章中,我们将探讨多种复制JavaScript数组的方法,并详细解释这些方法的具体应用。这不仅是对我自身知识的一个总结和备忘,同时也希望能够为读者提供一些有用的参考与启发。希望本文能在您解决数组复制问题时提供帮助,并拓展您对JavaScript的理解。
首先明确一个点:数组/对象值是按 引用 而不是按 值 复制的。
接下来我们将分浅拷贝和深拷贝来写几个js数组的复制
关于深拷贝和浅拷贝可以看我的另一篇文章:JS中对象的浅拷贝,深拷贝和引用-CSDN博客
浅拷贝
JavaScript 展开操作符
自从 ES6 发布以来,这一直是最受欢迎的方法。这是一个简短的语法。
numbers = [1, 2, 3];
numbersCopy = [...numbers];
**注意:**这不能安全地复制多维数组。数组/对象值是按 引用 而不是按 值 复制的。
numbersCopy.push(4);
console.log(numbers, numbersCopy);
// [1, 2, 3] and [1, 2, 3, 4]
由于是浅拷贝,像下面这样想复制多维数组就不符合要求了:
nestedNumbers = [[1], [2]];
numbersCopy = [...nestedNumbers];numbersCopy[0].push(300);
console.log(nestedNumbers, numbersCopy);
// [[1, 300], [2]]
// [[1, 300], [2]]
// They've both been changed because they share references
for() 循环
考虑到函数式编程在业界的流行程度,我认为这种方法 最不受欢迎。
numbers = [1, 2, 3];
numbersCopy = [];for (i = 0; i < numbers.length; i++) {numbersCopy[i] = numbers[i];
}
注意: 这不能安全地复制多维数组。由于你使用的是 =
运算符,因此它将按 引用 而不是按 值 分配对象/数组。
其他:
for循环时同步的,这意味着在代码的执行过程中,for
循环会逐步执行每一次迭代,直到循环结束,然后再继续执行循环后的代码。然而,如果你在for
循环中使用了异步操作(例如setTimeout
或Promise
),那么这些异步操作的执行顺序将取决于事件循环机制,比如当循环中包含异步函数时(例如setTimeout
或Promise
),这些异步操作会被推送到事件队列中,而主线程会继续执行后续代码。
具体来说,setTimeout
会将其回调函数推送到事件队列中,并在指定时间到达后将其放入执行队列。这意味着主线程不会在setTimeout
执行时阻塞,而是会继续执行后续代码。
写一个示例:
for (let i = 0; i < 3; i++) {setTimeout(() => {console.log(i);}, 1000);
}
console.log('Done1');
for (let i = 0; i < 3; i++) {setTimeout(() => {console.log(100 - i);}, 500);
}
console.log('Done2');
for (let i = 0; i < 3; i++) {setTimeout(() => {console.log(50 - i);}, 500);
}
console.log('Done3');
执行步骤如下:
- 执行第一个
for
循环:依次将 3 个setTimeout
回调函数推送到事件队列中,这些回调函数将在 1000 毫秒(1秒)后执行。- 回调函数1:
console.log(i)
在 1 秒后输出0
- 回调函数2:
console.log(i)
在 1 秒后输出1
- 回调函数3:
console.log(i)
在 1 秒后输出2
- 回调函数1:
- 打印
Done
:立即执行。 - 执行第二个
for
循环:依次将 3 个setTimeout
回调函数推送到事件队列中,这些回调函数将在 500 毫秒(0.5秒)后执行。- 回调函数1:
console.log(100 - i)
在 0.5 秒后输出100
- 回调函数2:
console.log(100 - i)
在 0.5 秒后输出99
- 回调函数3:
console.log(100 - i)
在 0.5 秒后输出98
- 回调函数1:
- 打印
Done2
:立即执行。 - 执行第三个
for
循环:依次将 3 个setTimeout
回调函数推送到事件队列中,这些回调函数将在 500 毫秒(0.5秒)后执行。- 回调函数1:
console.log(50 - i)
在 0.5 秒后输出50
- 回调函数2:
console.log(50 - i)
在 0.5 秒后输出49
- 回调函数3:
console.log(50 - i)
在 0.5 秒后输出48
- 回调函数1:
- 打印第二个
Done2
:立即执行。 - 定时器到达,按照设定的时间顺序执行
setTimeout
回调函数。
所以,实际的打印顺序如下:
Done1
Done2
Done3
100
99
98
50
49
48
0
1
2
Done
和Done2
会在任何setTimeout
回调函数之前立即打印,因为它们是同步的。- 第二个
for
循环的setTimeout
回调函数将在 0.5 秒后执行,因此它们的输出先于第一个for
循环的输出。 - 第三个
for
循环的setTimeout
回调函数也将在 0.5 秒后执行,因此它们与第二个for
循环几乎同时输出。 - 第一个
for
循环的setTimeout
回调函数将在 1 秒后执行,因此它们的输出最后。
array.forEach
forEach
是 JavaScript 数组的一个原型方法,用于对数组中的每个元素依次执行给定的函数。与传统的 for
循环不同,forEach
通过回调函数的形式来处理数组元素,简化了数组遍历的代码书写,并提高了代码的可读性和可维护性。
numbers = [1, 2, 3];
numbersCopy = []
numbers.forEach(item => {numbersCopy.push(item)
})
forEach方法详解
-
参数详解:
array.forEach(callback(currentvalue,index,arr) ,thisValue)callback为数组中每个元素执行的函数,该函数可接受1-3个参数:currentvalue参数表示数组的当前元素项,必须的参数index参数表示的当前元素下标,可选参数arr参数表示当前元素所属的数组,可选参数thisValue表示执行回调函数callback()时的this指向。可选参数。当不写时,则默认是指向window全局2
-
示例:
注意点:
-
forEach()是没有返回值的
-
forEach()会跳过空值
-
forEach() 方法无法提前结束:
forEach
方法无法通过break
或return
提前终止循环。如果需要中途退出循环,应该考虑使用for
循环或some
、every
等方法。
var arr = [1, 3, 5, 13, 2]; var res = arr.forEach(function(item,index) {console.log(`数组第${index+1}个元素是${item}`); }) console.log(res);//forEach的返回值为undefined,
用forEach()方法修改数组元素不能使用item直接修改,item相当于从arr2中复制过来的值,并不是真正指向原数组arr里面的元素。所以我们想要修改原数组的每个元素必须通过拿到它的索引值index去进行修改:
arr[index] = 2;
(当然是浅拷贝,对象中的数据还是可以改变的) -
-
是同步的,处理机制和上面的for循环相同,就不重复叙述了
array.map
map
函数是映射。映射起源于数学,map
是在保留结构的同时将集合转换为另一种类型的集合的概念,这意味着 Array.map
每次都会返回相同长度的数组。
numbers = [1, 2, 3];
numbersCopy = numbers.map((x) => x);
如果你想更加数学化,(x) => x
被称为恒等,它返回给定的任何参数。
map(identity)
拷贝一个列表。
identity = (x) => x;
numbers.map(identity);
// [1, 2, 3]
注意: 这也是通过 引用 而不是 值 来分配对象/数组,下面几种也是这个意思,就不一一举例说明了。
当然,map是对数据的每一项进行操作,你可以进行复制,当然也可以进行其他的操作,比如每个数据乘以2等等等等,用它做复制可能有一点点功能浪费,但是确实能做,简要扩展:
map()方法详解
-
map() 的返回值是一个新的数组,新数组中的元素为 “原数组调用函数处理过后的值”
-
map()函数的参数详解
一般参数是一个回调函数 array.map((item,index,arr)=>{//item是操作的当前元素//index是操作元素的下标//arr是需要被操作的元素//具体需要哪些参数 就传入那个 })
-
示例
const array = [2, 3, 4, 4, 5, 6] const map2=array.map((item,index,arr)=>{console.log("操作的当前元素",item)console.log("当前元素下标",index)console.log("被操作的元素",arr)//对元素乘以2return item*2 }) console.log("处理之后先产生的数组map",map2)以第一次返回结果为例,打印结果为: 操作的当前元素,2 当前元素下标,0 被操作的元素,[2, 3, 4, 4, 5, 6] 第二次为: 操作的当前元素,3 当前元素下标,1 被操作的元素,[2, 3, 4, 4, 5, 6] 最后的map数据为: 处理之后先产生的数组map,[4, 6, 8, 8, 10, 12]
-
总结:map()方法经常拿来遍历数组,但是不改变原数组,但是会返回一个新的数组
-
注意:有时候会出现这种现象,出现几个undefined
const array = [2, 3, 4, 4, 5, 6]console.log("原数组array为",array)const map = array.map(x => {if (x == 4) {return x * 2}})结果为: [undefined, undefined, 8, 8, undefined, undefined]
-
是同步方法,处理机制和上面的for循环相同,就不重复叙述了
array.filter
此函数会返回一个数组,就像 map
一样,但是不能保证长度相同,因为它是为了过滤,filter
函数对数组中的每个元素都会执行一次回调函数,如果回调函数返回 true
,则将该元素添加到新数组中;如果返回 false
,则不添加。这使得 filter
成为一种方便的方法来创建满足特定条件的新数组。
这里我们将让filter
始终返回 true
,则将获得重复项:
numbers = [1, 2, 3];
numbersCopy = numbers.filter(() => true);
filter()方法详解
-
参数详解
Array.filter(function(element, indedx, array), thisArg)element: 当前被处理的元素。 index(可选): 当前被处理的元素的索引。 array(可选): 调用 filter 的数组。 thisArg(可选): 执行函数时,用于设置 this 的值。
-
官方写法
const people = [{ name: "Alice", age: 25 },{ name: "Bob", age: 30 },{ name: "Charlie", age: 20 }, ];const adults = people.filter(function (person) {return person.age >= 25; });console.log(adults); // 输出: [{ name: "Alice", age: 25 }, { name: "Bob", age: 30 }]
-
使用箭头函数(更常用):
const numbers = [10, 20, 30, 40, 50]; const greaterThan30 = numbers.filter((num) => num > 30);console.log(greaterThan30); // 输出: [40, 50]
-
自定义过滤函数:
// 自定义过滤函数,筛选出年龄大于等于 25 的人 function filterAdult(person) {return person.age >= 25; }const people = [{ name: "Alice", age: 25 },{ name: "Bob", age: 30 },{ name: "Charlie", age: 20 }, ];// 使用自定义过滤函数 const adults = people.filter(filterAdult);console.log(adults); // 输出: [{ name: "Alice", age: 25 }, { name: "Bob", age: 30 }]
-
是同步方法,处理机制和上面的for循环相同,就不重复叙述了
array.reduce
reduce() 是数组的归并方法,与forEach()、map()、filter()等迭代方法一样都会对数组每一项进行遍历,但是reduce() 可同时将前面数组项遍历产生的结果与当前遍历项进行运算,这一点是其他迭代方法无法企及的。
我觉得使用 reduce
拷贝数组很糟糕,因为它的功能远不止于此。但是也能做:
numbers = [1, 2, 3];numbersCopy = numbers.reduce((newArray, element) => {newArray.push(element);return newArray;
}, []);
当 reduce
循环遍历列表时,转换初始值。
这里的初始值是一个空数组,我们将使用每个元素填充它。该数组必须从函数中返回,以在下一次迭代中使用。
reduce()方法详解
-
参数详解:
arr.reduce(function(prev,cur,index,arr){ ... }, init);回调函数参数: prev 表示上一次调用回调返回的值,或者是提供的初始值(init) 必须; cur 表示当前正在处理的数组元素 必须; index 表示当前正在处理的数组元素的索引,若提供 init 值,则索引为0,否则索引为1 可选; arr 表示原数组 可选;init 表示初始值 可选。常用的参数只有两个:prev 和 cur
-
示例说明:
先提供一个原始数组: var arr = [3,9,4,3,6,0,9];1. 求数组项之和 var sum = arr.reduce(function (prev, cur) {return prev + cur; },0);由于传入了初始值0,所以开始时prev的值为0,cur的值为数组第一项3,相加之后返回值为3作为下一轮回调的prev值,然后再继续与下一个数组项相加,以此类推,直至完成所有数组项的和并返回。2. 求数组项最大值 var max = arr.reduce(function (prev, cur) {return Math.max(prev,cur); });// 数组最大值 const max = arr.reduce(function(pre, cur) {return pre>cur?pre:cur; }); 由于未传入初始值,所以开始时prev的值为数组第一项3,cur的值为数组第二项9,取两值最大值后继续进入下一轮回调。
-
箭头函数:
// 数组求和 const arr = [12, 34, 23]; const sum = arr.reduce((total, num) => total + num); <!-- 设定初始值求和 --> const arr = [12, 34, 23]; const sum = arr.reduce((total, num) => total + num, 10); // 以10为初始值求和 <!-- 对象数组求和 --> var result = [{ subject: 'math', score: 88 },{ subject: 'chinese', score: 95 },{ subject: 'english', score: 80 } ]; const sum = result.reduce((accumulator, cur) => accumulator + cur.score, 0); const sum = result.reduce((accumulator, cur) => accumulator + cur.score, -10); // 总分扣除10分 // 记录元素出现次数 let arr5 = ['name','age','long','short','long','name','name'] let arrResult1 = arr.reduce((pre,cur) =>{console.log(pre,cur)if(cur in pre){pre[cur]++}else{pre[cur] = 1}return pre },{})console.log(arrResult1)//结果:{name: 3, age: 1, long: 2, short: 1}
-
简要总结
使用arr.reduce(function(prev,cur,index,arr){ ... }, init); 如果没有init,初始状态这个prev就是arr的第一个值,cur就是arr的第二个值,一次计算之后,prev变成了计算的结果,cur遍历到第三个值,一直结束,这种结果可想而知,结果的数据类型就是arr里面值的类型,比如是数字数组,那结果大概率数字,字符串数组,结果大概率是字符串(当然你也可以计算字符串长度和,当然也行),总之最后的结果是一个值; 那如果你想得到一个数组呢?你可以将init设置为[],这样初始状态prev就是[],cur就是arr的第一个值,你可以对这个值处理如何添加到prev中去,再return prev就是一个数组,如上面的示例,复制数组;当然你也可以将初始值设置为字典{},如上面记录元素出现次数。
-
是同步方法,处理机制和上面的for循环相同,就不重复叙述了
array.slice
**slice()**方法提取数组的一部分元素,并返回一个新的数组。
**slice()**方法提取的元素开始在给定的start参数,并在给定的端部end参数(end不包括)。原始数组不被会更改。
如果我们想得到所有元素,不提供任何参数:
numbers = [1, 2, 3, 4, 5];
numbersCopy = numbers.slice();
// [1, 2, 3, 4, 5]
slice()方法详解
-
参数详解
array.slice(start, end)start (可选)从零开始的索引,从该索引开始提取,使用负数从数组的末尾进行选择。如果省略,则类似于0。 end (可选)从零开始的索引,终止提取之前,如果省略,将选择从开始位置到数组末尾的所有元素。使用负数从数组末尾进行选择。注释:slice() 方法不会改变原始数组。
-
示例
如果我们想得到前 3 个元素: [1, 2, 3, 4, 5].slice(0, 3); // [1, 2, 3] 如果我们想得到后 3 个元素: [1, 2, 3, 4, 5].slice(3); // [3, 4, 5] 使用负数(从后面选取数据): [1, 2, 3, 4, 5].slice(-3,-1); // [3, 4]
-
是同步方法,处理机制和上面的for循环相同,就不重复叙述了
Array.from
Array.from()方法就是将一个类数组对象或者可迭代的对象转换成一个真正的数组,也是ES6的新增方法。
这可以将任何可迭代对象转换为数组。输入数组将返回浅拷贝。
numbers = [1, 2, 3];
numbersCopy = Array.from(numbers);
// [1, 2, 3]
Array.from()方法详解
-
参数详解
Array.from(object, mapFunction, thisValue)object 必需,要转换为数组的对象。 mapFunction 可选,数组中每个元素要调用的函数。 thisValue 可选,映射函数(mapFunction)中的 this 对象。
-
将类数组对象转换为真正数组
什么是类数组对象呢?所谓类数组对象,最基本的要求就是具有length属性的对象
let arrayLike = {0: 'tom', 1: '65',2: '男',3: ['jane','john','Mary'],'length': 4 } let arr = Array.from(arrayLike) console.log(arr) // ['tom','65','男',['jane','john','Mary']]
将上面代码中length属性去掉呢?实践证明,答案会是一个长度为0的空数组。
这里将代码再改一下,就是具有length属性,但是对象的属性名不再是数字类型的,而是其他字符串型的,代码如下:
let arrayLike = {'name': 'tom', 'age': '65','sex': '男','friends': ['jane','john','Mary'],length: 4 } let arr = Array.from(arrayLike) console.log(arr) // [ undefined, undefined, undefined, undefined ]
会发现结果是长度为4,元素均为undefined的数组
由此可见,要将一个类数组对象转换为一个真正的数组,必须具备以下条件:
1、该类数组对象必须具有length属性,用于指定数组的长度。如果没有length属性,那么转换后的数组是一个空数组。
2、该类数组对象的属性名必须为数值型或字符串型的数字
ps: 该类数组对象的属性名可以加引号,也可以不加引号
-
将Set结构的数据转换为真正的数组(常用于数组去重):
let arr = Array.from(new Set([1, 2, 1, 2])) console.log(arr) //[1, 2]
Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。如下:
let arr = [12,45,97,9797,564,134,45642] let set = new Set(arr) console.log(Array.from(set, item => item + 1)) // [ 13, 46, 98, 9798, 565, 135, 45643 ]
-
将字符串转换为数组:
let str = 'hello world!'; console.log(Array.from(str)) // ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d", "!"]
-
Array.from参数是一个真正的数组:
console.log(Array.from([12,45,47,56,213,4654,154]))
-
是同步方法,处理机制和上面的for循环相同,就不重复叙述了
深拷贝
这里就先提供一种方式:
JSON.parse(JSON.stringify(array));
JSON.stringify
将一个对象转换成一个字符串。
JSON.parse
将一个字符串转换成一个对象。
将它们组合在一起可以将一个对象变成一个字符串,然后反过来可以创建一个全新的数据结构。
注意:这个方法可以安全地复制深度嵌套的对象/数组!
nestedNumbers = [[1], [2]];
numbersCopy = JSON.parse(JSON.stringify(nestedNumbers));numbersCopy[0].push(300);
console.log(nestedNumbers, numbersCopy);// [[1], [2]]
// [[1, 300], [2]]
// These two arrays are completely separate!