我读了很多关于
map
、reduce
和 filter
的文章,因为它们在 React 和 FP 中的使用量很大。如果我们写这样的东西:
let myArr = [1,2,3,4,5,6,7,8,9]
let sumOfDoubleOfOddNumbers = myArr.filter(num => num % 2)
.map(num => num * 2)
.reduce((acc, currVal) => acc + currVal, 0);
运行 3 个不同的循环。
我也阅读过有关 Java 8 流的内容,并且知道它们使用所谓的 monad,即首先存储计算。它们仅在一次迭代中执行一次。例如,
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A");
})
.forEach(s -> System.out.println("forEach: " + s));
// map: d2
// filter: D2
// map: a2
// filter: A2
// forEach: A2
// map: b1
// filter: B1
// map: b3
// filter: B3
// map: c
// filter: C
PS:Java代码取自:http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/
还有许多其他语言使用相同的方法。在 JS 中是否有同样的方法?
这是您的 Java 代码的精确克隆。与 Bergi 的解决方案不同,无需修改全局原型。
class Stream {
constructor(iter) {
this.iter = iter;
}
* [Symbol.iterator]() {
yield* this.iter;
}
static of(...args) {
return new this(function* () {
yield* args
}());
}
_chain(next) {
return new this.constructor(next.call(this));
}
map(fn) {
return this._chain(function* () {
for (let a of this)
yield fn(a);
});
}
filter(fn) {
return this._chain(function* () {
for (let a of this)
if (fn(a))
yield (a);
});
}
forEach(fn) {
for (let a of this)
fn(a)
}
}
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s => {
console.log("map: " + s);
return s.toUpperCase();
})
.filter(s => {
console.log("filter: " + s);
return s.startsWith("A");
})
.forEach(s => console.log('forEach', s));
实际上,链接功能可以与特定迭代器解耦以提供通用框架:
// polyfill, remove me later on
Array.prototype.values = Array.prototype.values || function* () { yield* this };
class Iter {
constructor(iter) { this.iter = iter }
* [Symbol.iterator]() { yield* this.iter }
static of(...args) { return this.from(args) }
static from(args) { return new this(args.values()) }
_(gen) { return new this.constructor(gen.call(this)) }
}
现在,您可以将任意生成器放入其中,包括预定义的和临时的,例如:
let map = fn => function* () {
for (let a of this)
yield fn(a);
};
let filter = fn => function* () {
for (let a of this)
if (fn(a))
yield (a);
};
it = Iter.of("d2", "a2", "b1", "b3", "c", "a000")
._(map(s => s.toUpperCase()))
._(filter(s => s.startsWith("A")))
._(function*() {
for (let x of [...this].sort())
yield x;
});
console.log([...it])
您可以使用管道来实现此目的,我不知道这是否会使它变得太复杂,但是通过使用管道,您可以在管道上调用
Array.reduce
,并且它在每次迭代中执行相同的行为。
const stream = ["d2", "a2", "b1", "b3", "c"];
const _pipe = (a, b) => (arg) => b(a(arg));
const pipe = (...ops) => ops.reduce(_pipe);
const _map = (value) => (console.log(`map: ${value}`), value.toUpperCase());
const _filter = (value) => (console.log(`filter: ${value}`),
value.startsWith("A") ? value : undefined);
const _forEach = (value) => value ? (console.log(`forEach: ${value}`), value) : undefined;
const mapFilterEach = pipe(_map,_filter,_forEach);
const result = stream.reduce((sum, element) => {
const value = mapFilterEach(element);
if(value) sum.push(value);
return sum;
}, []);
我从这里
获取了管道函数这里是管道缩减的填充,如果您想将其用于更动态的目的,还有一个示例
Array.prototype.pipeReduce = function(...pipes){
const _pipe = (a, b) => (arg) => b(a(arg));
const pipe = (...ops) => ops.reduce(_pipe);
const reducePipes = pipe(...pipes);
return this.reduce((sum, element) => {
const value = reducePipes(element);
if(value) sum.push(value);
return sum;
}, []);
};
const stream = ["d2", "a2", "b1", "b3", "c"];
const reduced = stream.pipeReduce((mapValue) => {
console.log(`map: ${mapValue}`);
return mapValue.toUpperCase();
}, (filterValue) => {
console.log(`filter: ${filterValue}`);
return filterValue.startsWith("A") ? filterValue : undefined;
}, (forEachValue) => {
if(forEachValue){
console.log(`forEach: ${forEachValue}`);
return forEachValue;
}
return undefined;
});
console.log(reduced); //["A2"]
什么叫monad,即首先存储计算结果
嗯,不,这不是 Monad 的意思。
有没有办法在 JS 中也能做到同样的事情?
是的,您可以使用迭代器。检查 this 实现或 that one (对于 monad 方法,here)。
const myArr = [1,2,3,4,5,6,7,8,9];
const sumOfDoubleOfOddNumbers = myArr.values() // get iterator
.filter(num => num % 2)
.map(num => num * 2)
.reduce((acc, currVal) => acc + currVal, 0);
console.log(sumOfDoubleOfOddNumbers);
["d2", "a2", "b1", "b3", "c"].values()
.map(s => {
console.log("map: " + s);
return s.toUpperCase();
})
.filter(s => {
console.log("filter: " + s);
return s.startsWith("A");
})
.forEach(s => console.log("forEach: " + s));
var IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));
IteratorPrototype.map = function*(f) {
for (var x of this)
yield f(x);
};
IteratorPrototype.filter = function*(p) {
for (var x of this)
if (p(x))
yield x;
};
IteratorPrototype.reduce = function(f, acc) {
for (var x of this)
acc = f(acc, x);
return acc;
};
IteratorPrototype.forEach = function(f) {
for (var x of this)
f(x);
};
Array.prototype.values = Array.prototype[Symbol.iterator];
const myArr = [1,2,3,4,5,6,7,8,9];
const sumOfDoubleOfOddNumbers = myArr.values() // get iterator
.filter(num => num % 2)
.map(num => num * 2)
.reduce((acc, currVal) => acc + currVal, 0);
console.log({sumOfDoubleOfOddNumbers});
["d2", "a2", "b1", "b3", "c"].values()
.map(s => {
console.log("map: " + s);
return s.toUpperCase();
})
.filter(s => {
console.log("filter: " + s);
return s.startsWith("A");
})
.forEach(s => console.log("forEach: " + s));
在生产代码中,您可能应该使用静态函数,而不是将自定义方法放在内置迭代器原型上。
Array.prototype.map 和 Array.prototype.filter 从前一个数组创建新数组。 Array.prototype.reduce 对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。
因此,它们都不允许惰性求值。
您可以通过将多个循环减少为一个来实现惰性:
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9]
const result = array.reduce((acc, x) => x % 2 ? acc += x * 2 : acc, 0);
console.log(result);
另一种方法可以是在自定义对象中自行处理惰性求值,如下所示。下一个片段是重新定义
filter
和 map
的示例:
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// convert to a lazy structure...
const results = toLazy(array)
.filter(x => {
console.log('filter', x);
return x % 2 !== 0;
})
.map(x => {
console.log('map', x);
return x * 2;
});
// check logs for `filter` and `map` callbacks
console.log(results.run()); // -> [2, 6, 10, 14, 18]
function toLazy(array) {
const lazy = {};
let callbacks = [];
function addCallback(type, callback) {
callbacks.push({ type, callback });
return lazy;
}
lazy.filter = addCallback.bind(null, 'filter');
lazy.map = addCallback.bind(null, 'map');
lazy.run = function () {
const results = [];
for (var i = 0; i < array.length; i += 1) {
const item = array[i];
for (var { callback, type } of callbacks) {
if (type === 'filter') {
if (!callback(item, i)) {
break;
}
} else if (type === 'map') {
results.push(callback(item, i));
}
}
}
return results;
};
return lazy;
}
自从 ECMAScript 2025 中引入 iterator helpers 以来,您可以将示例 Java 代码非常简单地转换为 JavaScript:
["d2", "a2", "b1", "b3", "c"].values()
.map(s => {
console.log("map: " + s);
return s.toUpperCase();
})
.filter(s => {
console.log("filter: " + s);
return s.startsWith("A");
})
.forEach(s => console.log("forEach: " + s));
// map: d2
// filter: D2
// map: a2
// filter: A2
// forEach: A2
// map: b1
// filter: B1
// map: b3
// filter: B3
// map: c
// filter: C
如您所见,输出是相同的,并且顺序相同(惰性)。方法
map
和 filter
是 iterators 上的方法(而不是数组上的方法)。除了初始数组之外,在此过程中不会创建其他数组。
Java 代码在参数数组上使用
Stream.of
时,此代码会在初始数组上创建迭代器(使用 .values()
方法)。