map和reduce的主要区别
Main difference between map and reduce
这两种方法我都用过,但我对这两种方法的用法很困惑。
有什么是map
可以做而reduce
不能做的,反之亦然吗?
注意:我知道如何使用这两种方法我想知道这些方法之间的主要区别以及我们何时需要使用。
通常"map"意味着将一系列输入转换为等长输出系列,而"reduce"意味着将一系列输入转换为更小 输出数量。
人们所说的 "map-reduce" 通常被解释为 "transform, possibly in parallel, combine serially"。
当您 "map" 时,您正在编写一个将 x
和 f(x)
转换为某个新值 x1
的函数。当您 "reduce" 时,您正在编写一些函数 g(y)
,它接受数组 y
并发出数组 y1
。他们处理不同类型的数据并产生不同的结果。
map()
函数 returns 通过对输入数组中的每个元素传递一个函数来创建一个新数组。
这与 reduce()
不同,后者以相同的方式接受一个数组和一个函数,但该函数接受 2
个输入 - 一个累加器和一个当前值。
所以 reduce()
可以像 map()
一样使用,如果你总是 .concat
到累加器上的函数的下一个输出。然而,它更常用于减少数组的维度,因此要么采用一维并返回单个值,要么展平二维数组等。
让我们一一来看看这两个。
地图
Map 接受回调,运行 它针对数组中的每个元素,但什么是
它的独特之处在于 根据您现有的数组生成一个新数组.
var arr = [1, 2, 3];
var mapped = arr.map(function(elem) {
return elem * 10;
})
console.log(mapped); // it genrate new array
减少
数组对象的reduce方法用于将数组缩减为一个值.
var arr = [1, 2, 3];
var sum = arr.reduce(function(sum, elem){
return sum + elem;
})
console.log(sum) // reduce the array to one single value
map
和 reduce
都将数组和您定义的函数作为输入。它们在某种程度上是互补的:map
不能 return 单个元素用于多个元素的数组,而 reduce
将始终 return 您最终更改的累加器。
map
使用 map
迭代元素,并为每个元素 return 一个你想要的元素。
例如,如果你有一个数字数组,想得到它们的平方,你可以这样做:
// A function which calculates the square
const square = x => x * x
// Use `map` to get the square of each number
console.log([1, 2, 3, 4, 5].map(square))
reduce
使用数组作为输入,您可以根据获得 accumulator
和 current_element
参数:
const numbers = [1, 2, 3, 4, 5]
// Calculate the sum
console.log(numbers.reduce(function (acc, current) {
return acc + current
}, 0)) // < Start with 0
// Calculate the product
console.log(numbers.reduce(function (acc, current) {
return acc * current
}, 1)) // < Start with 1
当你可以用两者做同样的事情时,你应该选择哪一个?试着想象代码的样子。对于提供的示例,您可以使用 reduce
:
像您提到的那样计算 squares 数组
// Using reduce
[1, 2, 3, 4, 5].reduce(function (acc, current) {
acc.push(current*current);
return acc;
}, [])
// Using map
[1, 2, 3, 4, 5].map(x => x * x)
现在,看看这些,显然第二个实现看起来更好而且更短。通常您会选择更清洁的解决方案,在本例中为 map
。当然,你可以用reduce
来做,但简而言之,想想哪个更短,最终那个会更好。
要了解 map、filter 和 reduce 之间的区别,请记住:
- 所有这三种方法都应用于数组所以任何时候你想对数组进行任何操作,你都将使用这些方法。
- 这三个都遵循函数式方法,因此原始数组保持不变。原始数组不会更改,而是返回一个新的 array/value。
Map
returns 一个新数组,编号相等。元素 与原始数组中一样。因此,如果原始数组有 5 个元素,则返回的数组也将有 5 个元素。每当我们想要对数组的每个单独元素进行一些更改时,都会使用此方法。你可以记住,ann 数组的每个元素都被映射到输出数组中的某个新值,因此名称 map
例如,
var originalArr = [1,2,3,4]
//[1,2,3,4]
var squaredArr = originalArr.map(function(elem){
return Math.pow(elem,2);
});
//[1,4,9,16]
Filter
returns 一个比原始数组 equal/less 个元素 个元素的新数组。它 returns 数组中已通过某些条件的那些元素。当我们想对原始数组应用过滤器时使用此方法,因此名称为 filter
。例如,
var originalArr = [1,2,3,4]
//[1,2,3,4]
var evenArr = originalArr.filter(function(elem){
return elem%2==0;
})
//[2,4]
Reduce
returns 单个值,不同于 map/filter。因此,每当我们想要对数组的所有元素进行 运行 操作但希望使用所有元素的单个输出时,我们使用 reduce
。您可能还记得数组的输出被缩减为单个值,因此名称为 reduce
。例如,
var originalArr = [1,2,3,4]
//[1,2,3,4]
var sum = originalArr.reduce(function(total,elem){
return total+elem;
},0)
//10
我想这张图会回答你这些高阶函数
之间的区别
我认为这个问题是一个很好的问题,我不能不同意这些答案,但我觉得我们完全没有抓住要点。
更抽象地思考 map
和 reduce
可以为我们提供很多非常好的见解。
这个答案分为三个部分:
- 在 map 和 reduce 之间定义和决定(7 分钟)
- 有意使用 reduce(8 分钟)
- 使用传感器桥接 map 和 reduce(5 分钟)
映射或减少
共同特征
map
和 reduce
以有意义且一致的方式在范围广泛的不一定是集合的对象上实现。
他们return一个对周围算法有用的值,他们只关心这个值。
它们的主要作用是传达关于改造或保存结构的意图。
结构
我所说的“结构”是指一组描述抽象对象的概念属性,例如无序列表或二维矩阵,以及它们在数据结构中的具体化。
请注意,两者之间可以断开连接:
- 无序列表可以存储为数组,具有索引键携带排序的概念;
- 二维矩阵可以存储为TypedArray,缺少维度(或嵌套)的概念。
地图
map
is a strict structure-preserving transformation.
在其他类型的对象上实现它以掌握它的语义值是有用的:
class A {
constructor (value) {
this.value = value
}
map (f) {
return new A(f(this.value))
}
}
new A(5).map(x => x * 2); // A { value: 10 }
实现 map
的对象可以有各种行为,但它们始终 return 与您在使用提供的回调转换值时开始使用的对象类型相同。
Array.map
returns 与原始数组长度和顺序相同的数组。
关于回调参数
因为它保留了结构,map
被视为安全操作,但并非每个回调都是平等的。
使用一元回调:map(x => f(x))
,数组的每个值对其他值的存在完全无动于衷。
另一方面,使用其他两个参数会引入耦合,这可能与原始结构不符。
想象一下删除或重新排序以下数组中的第二项:在映射之前或之后执行此操作不会产生相同的结果。
与数组大小耦合:
[6, 3, 12].map((x, _, a) => x/a.length);
// [2, 1, 4]
与排序耦合:
['foo', 'bar', 'baz'].map((x, i) => [i, x]);
// [[0, 'foo'], [1, 'bar'], [2, 'baz']]
与一个特定值耦合:
[1, 5, 3].map((x, _, a) => x/Math.max(...a));
//[ 0.2, 1, 0.6]
与邻居结缘:
const smooth = (x, i, a) => {
const prev = a[i - 1] ?? x;
const next = a[i + 1] ?? x;
const average = (prev + x + next) / 3;
return Math.round((x + average) / 2);
};
[1, 10, 50, 35, 40, 1].map(smoothh);
// [ 3, 15, 41, 38, 33, 8 ]
我建议在调用站点上明确说明是否使用这些参数。
const transfrom = (x, i) => x * i;
❌ array.map(transfrom);
⭕ array.map((x, i) => transfrom(x, i));
当您将可变参数函数与 map
一起使用时,这还有其他好处。
❌ ["1", "2", "3"].map(parseInt);
// [1, NaN, NaN]
⭕ ["1", "2", "3"].map(x => parseInt(x));
// [1, 2, 3]
减少
reduce
sets a value free from its surrounding structure.
同样,让我们在一个更简单的对象上实现它:
class A {
constructor (value) {
this.value = value
}
reduce (f, init) {
return init !== undefined
? f(init, this.value)
: this.value
}
}
new A(5).reduce(); // 5
const concat = (a, b) => a.concat(b);
new A(5).reduce(concat, []); // [ 5 ]
无论您保留该值还是将其放回其他值,reduce
的输出都可以是任何形状。它与 map
.
的字面意思相反
对数组的影响
数组可以包含多个或零个值,这会产生两个有时相互冲突的要求。
需要结合
How can we return multiple values with no structure around them?
这是不可能的。为了return只有一个值,我们有两个选择:
- 将值汇总为一个值;
- 将值移动到不同的结构中。
现在是不是更有意义了?
需要初始化
What if there is no value to return?
如果 reduce
return 编辑了一个虚假值,将无法知道源数组是否为空或是否包含该虚假值,因此除非我们提供初始值, reduce
必须投。
reducer 的真正目的
您应该能够猜出减速器 f
在以下代码段中的作用:
[a].reduce(f);
[].reduce(f, a);
没有。不叫。
这是微不足道的情况:a
是我们想要 return 的单个值,因此不需要 f
。
顺便说一句,这就是为什么我们之前没有在 class A
中强制使用 reducer 的原因:因为它只包含一个值。它在数组上是强制性的,因为数组可以包含多个值。
由于只有当你有 2 个或更多值时才会调用 reducer,说它的唯一目的是将它们组合起来只是一箭之遥。
关于转换值
在可变长度的数组上,期望 reducer 转换值是危险的,因为正如我们发现的那样,它可能不会被调用。
我鼓励你在 reduce
之前 map
当你需要转换值和改变形状时。
为了可读性,最好将这两个问题分开。
何时不使用 reduce
因为reduce
是t是实现结构转换的 general-purpose 工具,如果存在另一个更集中的方法可以满足您的需求,我建议您在想要返回数组时避免使用它。
具体来说,如果您在 map
中遇到嵌套数组问题,请在使用 reduce
.
之前考虑 flatMap
或 flat
减少的核心
递归二元运算
在数组上实现 reduce
引入了这个反馈循环,其中 reducer 的第一个参数是前一次迭代的 return 值。
不用说,它看起来一点也不像 map
的回调。
我们可以像这样递归地实现Array.reduce
:
const reduce = (f, acc, [current, ...rest]) =>
rest.length == 0
? f(acc, current)
: reduce(f, f(acc, current), rest)
这突出了 reducer f
的二进制性质以及它的 return 值如何在下一次迭代中变成新的 acc
。
我让你自己相信以下是真实的:
reduce(f, a, [b, c, d])
// is equivalent to
f(f(f(a, b), c), d)
// or if you squint a little
((a ❋ b) ❋ c) ❋ d
这看起来应该很熟悉:您知道算术运算遵循“关联性”或“交换性”等规则。我想在这里传达的是同样的规则适用。
reduce
可能会去掉周围的结构,在转换时值仍然以代数结构绑定在一起。
减速器的代数
代数结构超出了这个答案的范围,所以我只会谈及它们的相关性。
((a ❋ b) ❋ c) ❋ d
查看上面的表达式,self-evident 有一个约束将所有值绑定在一起:❋
必须知道如何以相同的方式组合它们 +
必须知道如何结合 1 + 2
和同样重要的 (1 + 2) + 3
.
最弱的安全结构
确保这一点的一种方法是强制这些值属于同一个集合,在该集合上减速器是“内部”或“封闭”二元操作,也就是说:将这个集合中的任意两个值与reducer 产生一个属于同一个集合的值。
在抽象代数中,这称为 magma。您还可以查找 semi-groups ,它们被更多地讨论并且与关联性相同(不需要大括号),尽管 reduce
不关心。
不太安全
生活在岩浆中并不是绝对必要的:我们可以想象这样一种情况,其中 ❋
可以结合 a
和 b
但不能结合 c
和 b
.
这方面的一个例子是函数组合。以下函数之一 return 是一个字符串,它限制了您可以组合它们的顺序:
const a = x => x * 2;
const b = x => x ** 2;
const c = x => x + ' !';
// (a ∘ b) ∘ c
const abc = x => c(b(a(x)));
abc(5); // "100 !"
// (a ∘ c) ∘ b
const acb = x => b(c(a(x)));
acb(5); // NaN
像许多二元运算一样,函数组合可以用作reducer。
了解我们是否处于重新排序或从数组中删除元素可能导致 reduce
中断的情况,这很有价值。
所以,岩浆:不是绝对必要,但非常重要。
初始值如何
假设我们想通过引入一个初始值来防止在数组为空时抛出异常:
array.reduce(f, init)
// which is really the same as doing
[init, ...array].reduce(f)
// or
((init ❋ a) ❋ b) ❋ c...
我们现在有了额外的价值。没问题。
“没问题”!?我们说reducer的目的是合并数组值,但是init
不是true值:是我们自己强行引入的,应该不会影响结果reduce
.
问题是:
What init
should we pick so that f(init, a)
or init ❋ a
returns a
?
我们想要一个初始值,就像它不存在一样。我们想要一个中性元素(或“身份”)。
你可以查一下unital magmas或者monoids(结合律也一样)这些都是含有中性元素的magmas的脏话.
一些中性元素
你已经知道了一堆中性元素
numbers.reduce((a, b) => a + b, 0)
numbers.reduce((a, b) => a * b, 1)
booleans.reduce((a, b) => a && b, true)
strings.reduce((a, b) => a.concat(b), "")
arrays.reduce((a, b) => a.concat(b), [])
vec2s.reduce(([u,v], [x,y]) => [u+x,v+y], [0,0])
mat2s.reduce(dot, [[1,0],[0,1]])
您可以为多种抽象重复此模式。请注意,中性元素和计算不需要如此简单 (extreme example)。
中性元素的艰辛
We have to accept the fact that some reductions are only possible for non-empty arrays and that adding poor initialisers don't fix the problem.
一些减少错误的例子:
仅部分中立
numbers.reduce((a, b) => b - a, 0)
// does not work
numbers.reduce((a, b) => a - b, 0)
减去0
形成b
returnsb
,但是从0
returns-b
。
我们说只有“right-identity”为真。
并非每个 non-commutative 操作都缺少对称的中性元素,但这是一个好兆头。
超出范围
const min = (a, b) => a < b ? a : b;
// Do you really want to return Infinity?
numbers.reduce(min, Infinity)
Infinity
是唯一不会改变 non-empty 数组的 reduce
输出的初始值,但我们不太可能希望它实际出现在我们的程序中.
中性元素不是我们为了方便而添加的一些小丑值。它必须是一个允许的值,否则它不会完成任何事情。
荒谬
下面的缩减依赖于位置,但是添加初始化器自然会将第一个元素移动到第二个位置,这需要弄乱中的索引educer 来维持行为。
const first = (a, b, i) => !i ? b : a;
things.reduce(first, null);
const camelCase = (a, b, i) => a + (
!i ? b : b[0].toUpperCase() + b.slice(1)
);
words.reduce(camelCase, '');
接受数组不能为空的事实并简化 reducer 的定义会更清晰。
此外,初始值退化:
null
不是空数组的第一个元素。
空字符串绝不是有效的标识符。
没有办法用初始值来保留“第一”的概念。
结论
代数结构可以帮助我们更系统地思考我们的程序。知道我们正在处理的是哪一个可以准确预测我们对 reduce
的期望,所以我只能建议您查看它们。
更进一步
我们已经看到 map
和 reduce
有多么不同 structure-wise,但它们并不是两个孤立的东西。
我们可以用 reduce
表示 map
,因为总是可以重建我们开始的相同结构。
const map = f => (acc, x) =>
acc.concat(f(x))
;
const double = x => x * 2;
[1, 2, 3].reduce(map(double), []) // [2, 4, 6]
进一步推动它已经产生了巧妙的技巧,例如换能器。
我不会详细介绍它们,但我希望您注意一些与我们之前所说的相呼应的事情。
换能器
首先让我们看看我们要解决什么问题
[1, 2, 3, 4].filter(x => x % 2 == 0)
.map(x => x ** 2)
.reduce((a, b) => a + b)
// 20
我们正在迭代 3 次并创建 2 个中间数据结构。此代码是声明性的,但效率不高。传感器试图调和两者。
首先是一个使用 reduce
组合函数的小工具,因为我们不打算使用方法链:
const composition = (f, g) => x => f(g(x));
const identity = x => x;
const compose = (...functions) =>
functions.reduce(composition, identity)
;
// compose(a, b, c) is the same as x => a(b(c(x)))
现在注意下面map
和filter
的实现。我们正在传递这个 reducer
函数而不是直接连接。
const map = f => reducer => (acc, x) =>
reducer(acc, f(x))
;
const filter = f => reducer => (acc, x) =>
f(x) ? reducer(acc, x) : acc
;
具体看这个:
reducer => (acc, x) => [...]
应用回调函数 f
后,我们剩下一个函数,它以一个 reducer 作为输入,return 是一个 reducer。
这些对称函数就是我们传递给compose
:
const pipeline = compose(
filter(x => x % 2 == 0),
map(x => x ** 2)
);
记住compose
是用reduce
实现的:我们前面定义的composition
函数结合了我们的对称函数。
这个操作的输出是一个相同形状的函数:需要一个 reducer 而 returns 是一个 reducer,这意味着
- 我们有岩浆。只要它们具有这种形状,我们就可以继续组合变换。
- 我们可以通过将结果函数与 reducer 一起使用来使用此链,这将 return 一个我们可以与
reduce
一起使用的 reducer
如果你需要说服力,我让你展开整个事情。如果你这样做,你会注意到转换将很方便地从左到右应用,这是 compose
.
的相反方向
好吧,让我们使用这个怪人:
const add = (a, b) => a + b;
const reducer = pipeline(add);
const identity = 0;
[1, 2, 3, 4].reduce(reducer, identity); // 20
我们已将 map
、filter
和 reduce
等多种操作组合成一个 reduce
,仅迭代一次,没有中介 data-structure。
这是一个不小的成就!也不是仅仅根据语法的简洁性在map
和reduce
之间取舍就能想出的方案。
另请注意,我们可以完全控制初始值和最终减速器。我们使用 0
和 add
,但我们可以使用 []
和 concat
(更实际的是 push
performance-wise)或任何其他 data-structure 我们可以为此实现 concat-like 操作。
map 函数对每个元素执行一个给定的函数,而 reduce 执行一个将数组缩减为单个值的函数。我将举一个例子:
// map function
var arr = [1, 2, 3, 4];
var mappedArr = arr.map((element) => { // [10, 20, 30, 40]
return element * 10;
})
// reduce function
var arr2 = [1, 2, 3, 4]
var sumOfArr2 = arr2.reduce((total, element) => { // 10
return total + element;
})
确实 reduce
将数组缩减为单个值,但由于我们可以将对象作为 initialValue
传递,因此我们可以在此基础上进行构建并最终得到比我们从什么开始,比如这个例子,我们按照一些标准对项目进行分组。因此,术语 'reduce' 可能会稍微误导 reduce
的功能,并且认为它必然会减少信息可能是错误的,因为它也可以添加信息。
let a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
let b = a.reduce((prev, curr) => {
if (!prev["divisibleBy2"]) {
prev["divisibleBy2"] = []
}
if (curr % 2 === 0) {
prev["divisibleBy2"].push(curr)
}
if (!prev["divisibleBy3"]) {
prev["divisibleBy3"] = []
}
if (curr % 3 === 0) {
prev["divisibleBy3"].push(curr)
}
if (!prev["divisibleBy5"]) {
prev["divisibleBy5"] = []
}
if (curr % 5 === 0) {
prev["divisibleBy5"].push(curr)
}
return prev
}, {})
console.log(b)
这两种方法我都用过,但我对这两种方法的用法很困惑。
有什么是map
可以做而reduce
不能做的,反之亦然吗?
注意:我知道如何使用这两种方法我想知道这些方法之间的主要区别以及我们何时需要使用。
通常"map"意味着将一系列输入转换为等长输出系列,而"reduce"意味着将一系列输入转换为更小 输出数量。
人们所说的 "map-reduce" 通常被解释为 "transform, possibly in parallel, combine serially"。
当您 "map" 时,您正在编写一个将 x
和 f(x)
转换为某个新值 x1
的函数。当您 "reduce" 时,您正在编写一些函数 g(y)
,它接受数组 y
并发出数组 y1
。他们处理不同类型的数据并产生不同的结果。
map()
函数 returns 通过对输入数组中的每个元素传递一个函数来创建一个新数组。
这与 reduce()
不同,后者以相同的方式接受一个数组和一个函数,但该函数接受 2
个输入 - 一个累加器和一个当前值。
所以 reduce()
可以像 map()
一样使用,如果你总是 .concat
到累加器上的函数的下一个输出。然而,它更常用于减少数组的维度,因此要么采用一维并返回单个值,要么展平二维数组等。
让我们一一来看看这两个。
地图
Map 接受回调,运行 它针对数组中的每个元素,但什么是 它的独特之处在于 根据您现有的数组生成一个新数组.
var arr = [1, 2, 3];
var mapped = arr.map(function(elem) {
return elem * 10;
})
console.log(mapped); // it genrate new array
减少
数组对象的reduce方法用于将数组缩减为一个值.
var arr = [1, 2, 3];
var sum = arr.reduce(function(sum, elem){
return sum + elem;
})
console.log(sum) // reduce the array to one single value
map
和 reduce
都将数组和您定义的函数作为输入。它们在某种程度上是互补的:map
不能 return 单个元素用于多个元素的数组,而 reduce
将始终 return 您最终更改的累加器。
map
使用 map
迭代元素,并为每个元素 return 一个你想要的元素。
例如,如果你有一个数字数组,想得到它们的平方,你可以这样做:
// A function which calculates the square
const square = x => x * x
// Use `map` to get the square of each number
console.log([1, 2, 3, 4, 5].map(square))
reduce
使用数组作为输入,您可以根据获得 accumulator
和 current_element
参数:
const numbers = [1, 2, 3, 4, 5]
// Calculate the sum
console.log(numbers.reduce(function (acc, current) {
return acc + current
}, 0)) // < Start with 0
// Calculate the product
console.log(numbers.reduce(function (acc, current) {
return acc * current
}, 1)) // < Start with 1
当你可以用两者做同样的事情时,你应该选择哪一个?试着想象代码的样子。对于提供的示例,您可以使用 reduce
:
// Using reduce
[1, 2, 3, 4, 5].reduce(function (acc, current) {
acc.push(current*current);
return acc;
}, [])
// Using map
[1, 2, 3, 4, 5].map(x => x * x)
现在,看看这些,显然第二个实现看起来更好而且更短。通常您会选择更清洁的解决方案,在本例中为 map
。当然,你可以用reduce
来做,但简而言之,想想哪个更短,最终那个会更好。
要了解 map、filter 和 reduce 之间的区别,请记住:
- 所有这三种方法都应用于数组所以任何时候你想对数组进行任何操作,你都将使用这些方法。
- 这三个都遵循函数式方法,因此原始数组保持不变。原始数组不会更改,而是返回一个新的 array/value。
Map
returns 一个新数组,编号相等。元素 与原始数组中一样。因此,如果原始数组有 5 个元素,则返回的数组也将有 5 个元素。每当我们想要对数组的每个单独元素进行一些更改时,都会使用此方法。你可以记住,ann 数组的每个元素都被映射到输出数组中的某个新值,因此名称map
例如,
var originalArr = [1,2,3,4]
//[1,2,3,4]
var squaredArr = originalArr.map(function(elem){
return Math.pow(elem,2);
});
//[1,4,9,16]
Filter
returns 一个比原始数组 equal/less 个元素 个元素的新数组。它 returns 数组中已通过某些条件的那些元素。当我们想对原始数组应用过滤器时使用此方法,因此名称为filter
。例如,
var originalArr = [1,2,3,4]
//[1,2,3,4]
var evenArr = originalArr.filter(function(elem){
return elem%2==0;
})
//[2,4]
Reduce
returns 单个值,不同于 map/filter。因此,每当我们想要对数组的所有元素进行 运行 操作但希望使用所有元素的单个输出时,我们使用reduce
。您可能还记得数组的输出被缩减为单个值,因此名称为reduce
。例如,
var originalArr = [1,2,3,4]
//[1,2,3,4]
var sum = originalArr.reduce(function(total,elem){
return total+elem;
},0)
//10
我想这张图会回答你这些高阶函数
之间的区别我认为这个问题是一个很好的问题,我不能不同意这些答案,但我觉得我们完全没有抓住要点。
更抽象地思考 map
和 reduce
可以为我们提供很多非常好的见解。
这个答案分为三个部分:
- 在 map 和 reduce 之间定义和决定(7 分钟)
- 有意使用 reduce(8 分钟)
- 使用传感器桥接 map 和 reduce(5 分钟)
映射或减少
共同特征
map
和 reduce
以有意义且一致的方式在范围广泛的不一定是集合的对象上实现。
他们return一个对周围算法有用的值,他们只关心这个值。
它们的主要作用是传达关于改造或保存结构的意图。
结构
我所说的“结构”是指一组描述抽象对象的概念属性,例如无序列表或二维矩阵,以及它们在数据结构中的具体化。
请注意,两者之间可以断开连接:
- 无序列表可以存储为数组,具有索引键携带排序的概念;
- 二维矩阵可以存储为TypedArray,缺少维度(或嵌套)的概念。
地图
map
is a strict structure-preserving transformation.
在其他类型的对象上实现它以掌握它的语义值是有用的:
class A {
constructor (value) {
this.value = value
}
map (f) {
return new A(f(this.value))
}
}
new A(5).map(x => x * 2); // A { value: 10 }
实现 map
的对象可以有各种行为,但它们始终 return 与您在使用提供的回调转换值时开始使用的对象类型相同。
Array.map
returns 与原始数组长度和顺序相同的数组。
关于回调参数
因为它保留了结构,map
被视为安全操作,但并非每个回调都是平等的。
使用一元回调:map(x => f(x))
,数组的每个值对其他值的存在完全无动于衷。
另一方面,使用其他两个参数会引入耦合,这可能与原始结构不符。
想象一下删除或重新排序以下数组中的第二项:在映射之前或之后执行此操作不会产生相同的结果。
与数组大小耦合:
[6, 3, 12].map((x, _, a) => x/a.length);
// [2, 1, 4]
与排序耦合:
['foo', 'bar', 'baz'].map((x, i) => [i, x]);
// [[0, 'foo'], [1, 'bar'], [2, 'baz']]
与一个特定值耦合:
[1, 5, 3].map((x, _, a) => x/Math.max(...a));
//[ 0.2, 1, 0.6]
与邻居结缘:
const smooth = (x, i, a) => {
const prev = a[i - 1] ?? x;
const next = a[i + 1] ?? x;
const average = (prev + x + next) / 3;
return Math.round((x + average) / 2);
};
[1, 10, 50, 35, 40, 1].map(smoothh);
// [ 3, 15, 41, 38, 33, 8 ]
我建议在调用站点上明确说明是否使用这些参数。
const transfrom = (x, i) => x * i;
❌ array.map(transfrom);
⭕ array.map((x, i) => transfrom(x, i));
当您将可变参数函数与 map
一起使用时,这还有其他好处。
❌ ["1", "2", "3"].map(parseInt);
// [1, NaN, NaN]
⭕ ["1", "2", "3"].map(x => parseInt(x));
// [1, 2, 3]
减少
reduce
sets a value free from its surrounding structure.
同样,让我们在一个更简单的对象上实现它:
class A {
constructor (value) {
this.value = value
}
reduce (f, init) {
return init !== undefined
? f(init, this.value)
: this.value
}
}
new A(5).reduce(); // 5
const concat = (a, b) => a.concat(b);
new A(5).reduce(concat, []); // [ 5 ]
无论您保留该值还是将其放回其他值,reduce
的输出都可以是任何形状。它与 map
.
对数组的影响
数组可以包含多个或零个值,这会产生两个有时相互冲突的要求。
需要结合How can we return multiple values with no structure around them?
这是不可能的。为了return只有一个值,我们有两个选择:
- 将值汇总为一个值;
- 将值移动到不同的结构中。
现在是不是更有意义了?
需要初始化What if there is no value to return?
如果 reduce
return 编辑了一个虚假值,将无法知道源数组是否为空或是否包含该虚假值,因此除非我们提供初始值, reduce
必须投。
reducer 的真正目的
您应该能够猜出减速器 f
在以下代码段中的作用:
[a].reduce(f);
[].reduce(f, a);
没有。不叫。
这是微不足道的情况:a
是我们想要 return 的单个值,因此不需要 f
。
顺便说一句,这就是为什么我们之前没有在 class A
中强制使用 reducer 的原因:因为它只包含一个值。它在数组上是强制性的,因为数组可以包含多个值。
由于只有当你有 2 个或更多值时才会调用 reducer,说它的唯一目的是将它们组合起来只是一箭之遥。
关于转换值
在可变长度的数组上,期望 reducer 转换值是危险的,因为正如我们发现的那样,它可能不会被调用。
我鼓励你在 reduce
之前 map
当你需要转换值和改变形状时。
为了可读性,最好将这两个问题分开。
何时不使用 reduce
因为reduce
是t是实现结构转换的 general-purpose 工具,如果存在另一个更集中的方法可以满足您的需求,我建议您在想要返回数组时避免使用它。
具体来说,如果您在 map
中遇到嵌套数组问题,请在使用 reduce
.
flatMap
或 flat
减少的核心
递归二元运算
在数组上实现 reduce
引入了这个反馈循环,其中 reducer 的第一个参数是前一次迭代的 return 值。
不用说,它看起来一点也不像 map
的回调。
我们可以像这样递归地实现Array.reduce
:
const reduce = (f, acc, [current, ...rest]) =>
rest.length == 0
? f(acc, current)
: reduce(f, f(acc, current), rest)
这突出了 reducer f
的二进制性质以及它的 return 值如何在下一次迭代中变成新的 acc
。
我让你自己相信以下是真实的:
reduce(f, a, [b, c, d])
// is equivalent to
f(f(f(a, b), c), d)
// or if you squint a little
((a ❋ b) ❋ c) ❋ d
这看起来应该很熟悉:您知道算术运算遵循“关联性”或“交换性”等规则。我想在这里传达的是同样的规则适用。
reduce
可能会去掉周围的结构,在转换时值仍然以代数结构绑定在一起。
减速器的代数
代数结构超出了这个答案的范围,所以我只会谈及它们的相关性。
((a ❋ b) ❋ c) ❋ d
查看上面的表达式,self-evident 有一个约束将所有值绑定在一起:❋
必须知道如何以相同的方式组合它们 +
必须知道如何结合 1 + 2
和同样重要的 (1 + 2) + 3
.
最弱的安全结构
确保这一点的一种方法是强制这些值属于同一个集合,在该集合上减速器是“内部”或“封闭”二元操作,也就是说:将这个集合中的任意两个值与reducer 产生一个属于同一个集合的值。
在抽象代数中,这称为 magma。您还可以查找 semi-groups ,它们被更多地讨论并且与关联性相同(不需要大括号),尽管 reduce
不关心。
不太安全
生活在岩浆中并不是绝对必要的:我们可以想象这样一种情况,其中 ❋
可以结合 a
和 b
但不能结合 c
和 b
.
这方面的一个例子是函数组合。以下函数之一 return 是一个字符串,它限制了您可以组合它们的顺序:
const a = x => x * 2;
const b = x => x ** 2;
const c = x => x + ' !';
// (a ∘ b) ∘ c
const abc = x => c(b(a(x)));
abc(5); // "100 !"
// (a ∘ c) ∘ b
const acb = x => b(c(a(x)));
acb(5); // NaN
像许多二元运算一样,函数组合可以用作reducer。
了解我们是否处于重新排序或从数组中删除元素可能导致 reduce
中断的情况,这很有价值。
所以,岩浆:不是绝对必要,但非常重要。
初始值如何
假设我们想通过引入一个初始值来防止在数组为空时抛出异常:
array.reduce(f, init)
// which is really the same as doing
[init, ...array].reduce(f)
// or
((init ❋ a) ❋ b) ❋ c...
我们现在有了额外的价值。没问题。
“没问题”!?我们说reducer的目的是合并数组值,但是init
不是true值:是我们自己强行引入的,应该不会影响结果reduce
.
问题是:
What
init
should we pick so thatf(init, a)
orinit ❋ a
returnsa
?
我们想要一个初始值,就像它不存在一样。我们想要一个中性元素(或“身份”)。
你可以查一下unital magmas或者monoids(结合律也一样)这些都是含有中性元素的magmas的脏话.
一些中性元素
你已经知道了一堆中性元素
numbers.reduce((a, b) => a + b, 0)
numbers.reduce((a, b) => a * b, 1)
booleans.reduce((a, b) => a && b, true)
strings.reduce((a, b) => a.concat(b), "")
arrays.reduce((a, b) => a.concat(b), [])
vec2s.reduce(([u,v], [x,y]) => [u+x,v+y], [0,0])
mat2s.reduce(dot, [[1,0],[0,1]])
您可以为多种抽象重复此模式。请注意,中性元素和计算不需要如此简单 (extreme example)。
中性元素的艰辛
We have to accept the fact that some reductions are only possible for non-empty arrays and that adding poor initialisers don't fix the problem.
一些减少错误的例子:
仅部分中立numbers.reduce((a, b) => b - a, 0)
// does not work
numbers.reduce((a, b) => a - b, 0)
减去0
形成b
returnsb
,但是从0
returns-b
。
我们说只有“right-identity”为真。
并非每个 non-commutative 操作都缺少对称的中性元素,但这是一个好兆头。
超出范围const min = (a, b) => a < b ? a : b;
// Do you really want to return Infinity?
numbers.reduce(min, Infinity)
Infinity
是唯一不会改变 non-empty 数组的 reduce
输出的初始值,但我们不太可能希望它实际出现在我们的程序中.
中性元素不是我们为了方便而添加的一些小丑值。它必须是一个允许的值,否则它不会完成任何事情。
荒谬下面的缩减依赖于位置,但是添加初始化器自然会将第一个元素移动到第二个位置,这需要弄乱中的索引educer 来维持行为。
const first = (a, b, i) => !i ? b : a;
things.reduce(first, null);
const camelCase = (a, b, i) => a + (
!i ? b : b[0].toUpperCase() + b.slice(1)
);
words.reduce(camelCase, '');
接受数组不能为空的事实并简化 reducer 的定义会更清晰。
此外,初始值退化:
null
不是空数组的第一个元素。空字符串绝不是有效的标识符。
没有办法用初始值来保留“第一”的概念。
结论
代数结构可以帮助我们更系统地思考我们的程序。知道我们正在处理的是哪一个可以准确预测我们对 reduce
的期望,所以我只能建议您查看它们。
更进一步
我们已经看到 map
和 reduce
有多么不同 structure-wise,但它们并不是两个孤立的东西。
我们可以用 reduce
表示 map
,因为总是可以重建我们开始的相同结构。
const map = f => (acc, x) =>
acc.concat(f(x))
;
const double = x => x * 2;
[1, 2, 3].reduce(map(double), []) // [2, 4, 6]
进一步推动它已经产生了巧妙的技巧,例如换能器。
我不会详细介绍它们,但我希望您注意一些与我们之前所说的相呼应的事情。
换能器
首先让我们看看我们要解决什么问题
[1, 2, 3, 4].filter(x => x % 2 == 0)
.map(x => x ** 2)
.reduce((a, b) => a + b)
// 20
我们正在迭代 3 次并创建 2 个中间数据结构。此代码是声明性的,但效率不高。传感器试图调和两者。
首先是一个使用 reduce
组合函数的小工具,因为我们不打算使用方法链:
const composition = (f, g) => x => f(g(x));
const identity = x => x;
const compose = (...functions) =>
functions.reduce(composition, identity)
;
// compose(a, b, c) is the same as x => a(b(c(x)))
现在注意下面map
和filter
的实现。我们正在传递这个 reducer
函数而不是直接连接。
const map = f => reducer => (acc, x) =>
reducer(acc, f(x))
;
const filter = f => reducer => (acc, x) =>
f(x) ? reducer(acc, x) : acc
;
具体看这个:
reducer => (acc, x) => [...]
应用回调函数 f
后,我们剩下一个函数,它以一个 reducer 作为输入,return 是一个 reducer。
这些对称函数就是我们传递给compose
:
const pipeline = compose(
filter(x => x % 2 == 0),
map(x => x ** 2)
);
记住compose
是用reduce
实现的:我们前面定义的composition
函数结合了我们的对称函数。
这个操作的输出是一个相同形状的函数:需要一个 reducer 而 returns 是一个 reducer,这意味着
- 我们有岩浆。只要它们具有这种形状,我们就可以继续组合变换。
- 我们可以通过将结果函数与 reducer 一起使用来使用此链,这将 return 一个我们可以与
reduce
一起使用的 reducer
如果你需要说服力,我让你展开整个事情。如果你这样做,你会注意到转换将很方便地从左到右应用,这是 compose
.
好吧,让我们使用这个怪人:
const add = (a, b) => a + b;
const reducer = pipeline(add);
const identity = 0;
[1, 2, 3, 4].reduce(reducer, identity); // 20
我们已将 map
、filter
和 reduce
等多种操作组合成一个 reduce
,仅迭代一次,没有中介 data-structure。
这是一个不小的成就!也不是仅仅根据语法的简洁性在map
和reduce
之间取舍就能想出的方案。
另请注意,我们可以完全控制初始值和最终减速器。我们使用 0
和 add
,但我们可以使用 []
和 concat
(更实际的是 push
performance-wise)或任何其他 data-structure 我们可以为此实现 concat-like 操作。
map 函数对每个元素执行一个给定的函数,而 reduce 执行一个将数组缩减为单个值的函数。我将举一个例子:
// map function
var arr = [1, 2, 3, 4];
var mappedArr = arr.map((element) => { // [10, 20, 30, 40]
return element * 10;
})
// reduce function
var arr2 = [1, 2, 3, 4]
var sumOfArr2 = arr2.reduce((total, element) => { // 10
return total + element;
})
确实 reduce
将数组缩减为单个值,但由于我们可以将对象作为 initialValue
传递,因此我们可以在此基础上进行构建并最终得到比我们从什么开始,比如这个例子,我们按照一些标准对项目进行分组。因此,术语 'reduce' 可能会稍微误导 reduce
的功能,并且认为它必然会减少信息可能是错误的,因为它也可以添加信息。
let a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
let b = a.reduce((prev, curr) => {
if (!prev["divisibleBy2"]) {
prev["divisibleBy2"] = []
}
if (curr % 2 === 0) {
prev["divisibleBy2"].push(curr)
}
if (!prev["divisibleBy3"]) {
prev["divisibleBy3"] = []
}
if (curr % 3 === 0) {
prev["divisibleBy3"].push(curr)
}
if (!prev["divisibleBy5"]) {
prev["divisibleBy5"] = []
}
if (curr % 5 === 0) {
prev["divisibleBy5"].push(curr)
}
return prev
}, {})
console.log(b)