Array.prototype.map()

map() 方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。(每个元素都是回调函数的返回值)

1、语法

map(callbackFn)

map(callbackFn,thisArg)

参数:

​ callbackFn: 为数组中的每个元素执行的函数。它的返回值作为一个元素被添加为新数组中。该函数被调用时将传入以下参数:(1)element:数组中当前正在处理的元素(2)index:正在处理的元素在数组中的索引(3)array:调用 map()的数组本身。

​ thisArg: 执行 callbackFn 时用作 this 的值

map()是一个迭代方法。为数组中每一个元素调用一次提供的 callbackFn 函数,并用结果构建一个新数组。

callbackFn 仅在已分配值的数组索引处被调用。它不会在稀疏数组中的空槽处被调用。

map()不会改变 this。作为 callbackFn 提供的函数可以更改数组。

由于 map 创建一个新数组,在没有使用返回的数组的情况下调用它是不恰当的;应当使用 forEach 或者 for of 作为代替。

示例 1:求数组中每个元素的平方根

1
2
3
4
const numbers = [1, 4, 9]
const roots = numbers.map((num) => Math.sqrt(num))
// roots 现在是 [1, 2, 3]
// numbers 依旧是 [1, 4, 9]

示例 2:map 格式化数组中的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const kvArray = [
{ key: 1, value: 10 },
{ key: 2, value: 20 },
{ key: 3, value: 30 }
]

const reformattedArray = kvArray.map(({ key, value }) => ({ [key]: value }))

console.log(reformattedArray) // [{ 1: 10 }, { 2: 20 }, { 3: 30 }]
console.log(kvArray)
// [
// { key: 1, value: 10 },
// { key: 2, value: 20 },
// { key: 3, value: 30 }
// ]

示例 3:使用单个参数的函数来映射一个数字数组

1
2
3
4
5
6
// 当 map 循环遍历原始数组时,这个参数会自动被分配成数组中对应的每个元素。
const numbers = [1, 4, 9]
const doubles = numbers.map((num) => num * 2)

console.log(doubles) // [2, 8, 18]
console.log(numbers) // [1, 4, 9]

示例 4:非数组对象上调用 map()

1
2
3
4
5
6
7
8
9
// map() 方法读取 this 的 length 属性,然后访问每个整数索引。
const arrayLike = {
length: 3,
0: 2,
1: 3,
2: 4
}
console.log(Array.prototype.map.call(arrayLike, (x) => x ** 2))
// [ 4, 9, 16 ]

示例 5:NodeList 使用 map()

如何去遍历通过 querySelectorAll 得到的对象集合。这是因为 querySelectorAll 返回的是一个对象集合 NodeList

1
2
3
4
5
6
7
const elems = document.querySelectorAll('select option:checked')
const values = Array.prototype.map.call(elems, ({ value }) => value)
// 一种更简单的方式用Array.from()
// 根据 DOM 元素的属性创建一个数组
const images = document.querySelectorAll('img')
const sources = Array.from(images, (image) => image.src)
const insecureSources = sources.filter((link) => link.startsWith('http://'))

示例 6:稀疏数组使用 map()

1
2
3
4
5
6
7
8
9
10
// 稀疏数组在使用 map() 方法后仍然是稀疏的。空槽的索引在返回的数组中仍然为空,并且回调函数不会对它们进行调用。
console.log(
[1, , 3].map((x, index) => {
console.log(`Visit ${index}`)
return x * 2
})
)
// Visit 0
// Visit 2
// [2, empty, 6]

Array.prototype.forEach()

forEach()方法对数组的每个元素执行一次给定的函数。不会改变其调用的数组

语法同 map()

forEach() 方法是一个迭代方法。它按索引升序地为数组中的每个元素调用一次提供的 callbackFn 函数。与 map() 不同,forEach() 总是返回 undefined,而且不能继续链式调用。其典型的用法是在链式调用的末尾执行某些操作。

除非抛出异常,否则没有办法停止或中断 forEach() 循环。如果有这样的需求,则不应该使用 forEach() 方法。

可以通过像 forfor...offor...in 这样的循环语句来实现提前终止。当不需要进一步迭代时,诸如 every()some()find()findIndex() 等数组方法也会立即停止迭代。

注意:跳出循环和跳出本次循环是有区别的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 // 使用 forEach 跳出当前循环
const array1 = [1, 2, 3, 4, 5];
let sum1 = 0;

array1.forEach((element) => {
if (element === 3) {
return; // 使用 return 跳出循环
}
console.log(element); //1,2,4,5
sum1 += element;
});

console.log(sum1); // 输出: 12
// 使用 break 无法中断 forEach 循环
const array = [1, 2, 3, 4, 5];
let sum = 0;

array.forEach((element) => {
if (element === 3) {
break; // 使用 break 无法中断 forEach 循环
}
sum += element;
});

console.log(sum); // 此行代码不会执行,因为上面的代码会抛出错误

forEach() 期望的是一个同步函数,它不会等待 Promise 兑现。在使用 Promise(或异步函数)作为 forEach 回调时,请确保你意识到这一点可能带来的影响。

1
2
3
4
5
6
7
8
9
10
11
12
const ratings = [5, 4, 5]
let sum = 0

const sumFunction = async (a, b) => a + b

ratings.forEach(async (rating) => {
sum = await sumFunction(sum, rating)
})

console.log(sum)
// 期望的输出:14
// 实际的输出:0

如果希望按照顺序或者并发的执行一系列操作

1
2
3
4
5
;[func1, func2, func3]
.reduce((p, f) => p.then(f), Promise.resolve())
.then((result3) => {
/* 使用 result3 */
})

使用 reduce 把一个异步函数数组变为一个 Promise 链。上面代码等于:

1
2
3
4
5
6
7
8
9
10
11
12
13
Promise.resolve()
.then(func1)
.then(func2)
.then(func3)
.then((result3) => {
/* 使用 result3 */
})
// 也可以写成复用的函数形式
const applyAsync = (acc, val) => acc.then(val)
const composeAsync =
(...funcs) =>
(x) =>
funcs.reduce(applyAsync, Promise.resolve(x))

composeAsync 函数将会接受任意数量的函数作为其参数,并返回一个新的函数,而该函数又接受一个初始值,该组合的参数传递管线如下所示:

1
2
const transformData = composeAsync(func1, func2, func3)
const result3 = transformData(data)

顺序组合还可以使用 async 和 await(考虑是否真的有必要——因为它们会阻塞彼此,除非一个 Promise 的执行依赖于另一个 Promise 的结果,否则最好并发运行 Promise。)

1
2
3
4
5
let result
for (const f of [func1, func2, func3]) {
result = await f(result)
}
/* 使用最后的结果(即 result3)*/

示例 1:稀疏数组使用 forEach()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arraySparse = [1, 3 /* empty */, , 7]
let numCallbackRuns = 0

arraySparse.forEach((element) => {
console.log({ element })
numCallbackRuns++
})

console.log({ numCallbackRuns })

// { element: 1 }
// { element: 3 }
// { element: 7 }
// { numCallbackRuns: 3 }

示例 2:打印数组内容

1
2
3
4
5
6
7
8
9
10
const logArrayElements = (element, index /*, array */) => {
console.log(`a[${index}] = ${element}`)
}

// 注意,索引 2 被跳过,因为数组中这个位置没有内容
;[2, 5, , 9].forEach(logArrayElements)
// logs:
// a[0] = 2
// a[1] = 5
// a[3] = 9

示例 3:对象复制函数

创建对象副本

1
2
3
4
5
6
7
8
9
10
11
12
const copy = (obj) => {
const copy = Object.create(Object.getPrototypeOf(obj))
const propNames = Object.getOwnPropertyNames(obj)
propNames.forEach((name) => {
const desc = Object.getOwnPropertyDescriptor(obj, name)
Object.defineProperty(copy, name, desc)
})
return copy
}

const obj1 = { a: 1, b: 2 }
const obj2 = copy(obj1) // 现在 obj2 看起来和 obj1 一模一样了

示例 4:迭代期间修改数组

1
2
3
4
5
6
7
8
9
const words = ['one', 'two', 'three', 'four']
words.forEach((word) => {
console.log(word)
if (word === 'two') {
words.shift() //'one' 将从数组中删除
}
}) // one // two // four

console.log(words) // ['two', 'three', 'four']

示例 5:非数组对象上调用 forEach

1
2
3
4
5
6
7
8
9
10
const arrayLike = {
length: 3,
0: 2,
1: 3,
2: 4
}
Array.prototype.forEach.call(arrayLike, (x) => console.log(x))
// 2
// 3
// 4

map 和 forEach 会不会改变原数组

(1)、基本数据类型

forEach

1
2
3
4
5
const array = [1, 2, 3, 4]
array.forEach((item) => {
item = item + 1
})
console.log(array) // [1,2,3,4]

map

1
2
3
4
5
const array = [1, 2, 3, 4]
array.map((item) => {
item = item + 1
})
console.log(array) // [1,2,3,4]

结论:数组中的元素为基本数据类型时,原数组不会改变

(2)、引用数据类型

forEach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const arr = [
{
name: 'shaka',
age: 23
},
{
name: 'virgo',
age: 18
}
]
arr.forEach((item) => {
if (item.name === 'shaka') {
item.age = 100
}
})
console.log(arr) //[{name: 'shaka', age: 100}, {name: 'virgo', age: 18}]

map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const arr = [
{
name: 'shaka',
age: 23
},
{
name: 'virgo',
age: 18
}
]
arr.map((item) => {
if (item.name === 'shaka') {
item.age = 100
}
})
console.log(arr) //[{name: 'shaka', age: 100}, {name: 'virgo', age: 18}]

结论:数组的元素为引用类型时,原数组会改变

数组的元素为引用类型时,原数组会改变的原因是什么?

这是因为在使用 forEachmap 方法时,对引用类型元素的修改会直接反映在原始数组中。这是因为引用类型的元素实际上存储的是引用(内存地址),而非值本身。因此,通过引用可以访问和修改原始数组中的元素。基本类型在栈内存中直接存储变量与值。

总结

forEachmap 的实现原理相似。它们都是通过遍历数组,对数组的每个元素执行特定的函数。区别主要在于它们处理函数返回值的方式不同。forEach 忽略函数的返回值,而 map 则将函数的返回值收集到一个新的数组中。