我该如何在JavaScript中执行操作,就像我们在Java流中执行一系列操作一样?

17

在Java 8中使用流(streams)时,当我将一个方法连接到另一个方法后,操作的执行会以管道方式进行。

例如:

List<Integer> nums = Arrays.asList(1,2,3,4,5,6);    
nums.stream().map(x->{
    x = x * x;
    System.out.println("map1="+x);
    return x;
}).map(x->{
    x = x * 3;
    System.out.println("map2="+x);
    return x;
}).forEach(x-> System.out.println("forEach="+x));

输出:-

map1=1
map2=3
forEach=3
map1=4
map2=12
forEach=12
map1=9
map2=27
forEach=27
map1=16
map2=48
forEach=48
map1=25
map2=75
forEach=75
map1=36
map2=108
forEach=108

但是当我尝试在 JavaScript 中进行相似的操作时,结果不同。因为在 JavaScript 中,先完成第一个操作,然后才执行第二个操作。例如:

var nums = [1,2,3,4,5,6 ];
nums.map(x => {
  x = (x * x);
  console.log('map1='+x);
  return x;})
  .map(x => {
  x = x * 3;
  console.log('map2='+x);
  return x;})
  .forEach(x=> console.log('forEach='+x));

输出:

 map1=1
 map1=4
 map1=9
 map1=16
 map1=25
 map1=36
 map2=3
 map2=12
 map2=27
 map2=48
 map2=75
 map2=108
 forEach=3
 forEach=12
 forEach=27
 forEach=48
 forEach=75
 forEach=108

在JavaScript中有没有一种方法可以使其以管道方式执行操作,并且我可以获得与Java程序相同的输出?

此问题仅询问如何在JavaScript中进行收集,但不涉及相同类型方法的内部工作更改


你想要一个来自JS的本地解决方案还是寻找一些库/助手类? - s-f
检查 Lazy.js,可惜我没有在CDN上找到它以在此处创建代码片段。 - Thomas
可能是 Java Streams API 的 Javascript 等效方法 的重复问题。 - YetAnotherBot
@AdityaGupta,这两个问题在概念上是不同的。 - Jaspreet Jolly
从概念上讲,你所要求的是一个“流”实现。 - YetAnotherBot
7个回答

18

也许以后(或永远)您可以使用实际的试验性 管道运算符|>,其语法如下:

expression |> function

通过将函数视为单独的函数并针对每个管道迭代流数组,可以实现您想要的结果。

这仅适用于FF。从版本58开始:此功能位于--enable-pipeline-operator编译标志后面。

const
    a = x => { x = x * x; console.log("map1=" + x); return x; },
    b = x => { x = x * 3; console.log("map2=" + x); return x; },
    c = x => console.log("forEach=" + x)

var nums = [1, 2, 3, 4, 5, 6];

nums.forEach(v => v |> a |> b |> c);

通过使用管道(函数组合实现管道)与对所需函数的闭包相同。

const
    pipe = (...functions) => input => functions.reduce((acc, fn) => fn(acc), input),
    a = x => { x = x * x; console.log("map1=" + x); return x; },
    b = x => { x = x * 3; console.log("map2=" + x); return x; },
    c = x => console.log("forEach=" + x)

var nums = [1, 2, 3, 4, 5, 6],
    pipeline = pipe(a, b, c);

nums.forEach(pipeline);


1
所有的 F# 开发者都赞美管道运算符 :D - aloisdg

12

如果将每个函数操作放入一个数组中,您可以使用reduce遍历该数组,并在累加器中传递上一个计算出的值,直到到达函数数组的末尾:

如果您将每个函数操作放入数组中,则可以使用reduce遍历该数组,并在累加器中传递上一个计算出的值,直到到达函数数组的末尾:

var nums = [1,2,3,4,5,6 ];
var fns = [
  (x) => {
    x = x * x;
    console.log('map1=' + x);
    return x;
  },
  (x) => {
    x *= 3;
    console.log('map2=' + x);
    return x;
  },
  (x) => {
    console.log(x);
    return x;
  }
];

nums.forEach((num) => {
  fns.reduce((lastResult, fn) => fn(lastResult), num);
  // pass "num" as the initial value for "lastResult",
  // before the first function has been called
});

不能使用 nums.map,因为 .map 会在解析映射后的输出数组之前必须遍历整个输入数组(之后再对该映射输出数组调用另一个 .map)。


2
Java中的流相当于JavaScript中的迭代器。不幸的是,迭代器对象目前还没有map方法(但是尚未),但是您可以轻松地自己编写一个(甚至可以将其安装在原型上以获得方法语法)。

function* map(iterable, f) {
    for (var x of iterable)
        yield f(x);
}

var nums = [1,2,3,4,5,6];
function square(x) {
  x = (x * x);
  console.log('map1='+x);
  return x;
}
function triple(x) {
  x = x * 3;
  console.log('map2='+x);
  return x;
}
for (const x of map(map(nums.values(), square), triple)) {
  console.log('forEach='+x);
}

还需要注意的是,在函数式编程中,对于纯操作,顺序并不重要——如果你正在使用map,就不应该依赖于执行顺序。

2
请不要改变您不是开发者的任何类型的原型。 如果每个人都在改变原型,任何库都可能破坏其他库,因为它们会覆盖彼此的原型变异。https://dev59.com/T2Yr5IYBdhLWcg3wAFg0 - Suppen
1
@Suppen同意。不过你可以使用迭代器方法提案中的polyfill。 - Bergi

1
为什么要从头开始重建,当我们已经有解决方案了呢?这个功能在lodash/RxJS/stream.js中已经存在。 lodash的示例片段:
_.flow(
 _.assign(rows[0]),
 _.omit('blah')
)(foundUser);

// >> {"charData":[],"ok": 1}

然而,JavaScript 运行在单线程上,这些库也是如此。Java 流可以受益于多核系统(在并行的情况下)。它们可以使用多个线程来利用所有可用的内核。


但是如果我不想为了仅此类型的情况而再加载一个库到我的页面中,该怎么办? - Jaspreet Jolly
有道理!但是,添加一个库比编写臃肿的代码更好。您可以使用这些库来摆脱样板代码。 - YetAnotherBot
1
我们也可以反对这个事实,即与Java不同,Javascript的默认运行时没有那么多的东西,这就是为什么我们在JS中需要如此快速地使用库的原因。 - Walfrat
2
你在Java案例中仍在使用库,不同之处在于它们是由Oracle提供的。 - Caleth
@Caleth 不,不同之处在于Java库不会使您的应用程序变得臃肿。 - xehpuk
总之,代码维护和代码抽象应该是开发者的重点。添加一个小型压缩脚本比带来伤害更有益。 - YetAnotherBot

0
我建议使用像RxJS这样的库。这可以让您更好地控制处理的类型,无论是顺序、并行还是其他。
以下是一个接近您期望的示例:
const source = Rx.Observable.from([{name: 'Joe', age: 30}, {name: 'Frank', age: 20},{name: 'Ryan', age: 50}]);
const example = source.map(person => {
  console.log("Mapping1" + person.name)
  return person.name
});
const subscribe = example.subscribe(val => console.log(val));

输出:

"Mapping1Joe"
"Joe"
"Mapping1Frank"
"Frank"
"Mapping1Ryan"
"Ryan"

问题中的Java代码示例从不调用.parallel(),因此永远不会在多个线程上执行。线程与此问题无关。 Java中的流与迭代器几乎相同,每次从源开始对1个项目进行处理。 OP还要求提供一个执行与其Java代码相同操作的JavaScript代码,而不是执行与其JavaScript代码相同操作的Java代码。 - Ferrybig
好的,那么我可能对Java内部有错误的期望,对此感到抱歉。但是你能解释一下你的第二个观点吗?我提供的是Javascript代码而不是Java代码,所以我不明白你的意思。 - Martin Seeler
我觉得在说我的第二点时犯了一个错误,我认为在上面的段落中错过了“like”这个词(根据我的评论时间,似乎是当我在地铁上旅行并使用SE应用程序时发表的评论)。对于第二点浪费您的时间,我感到非常抱歉。我做了一些研究,似乎您推荐的“Rx.Observable”库在简单情况下几乎与Java流相同,因此如果您可以编辑您的帖子以删除此内容,我实际上可以撤回我的反对票。 - Ferrybig
好的,谢谢解释。我已经删除了关于Java和误导性“like”部分的内容。 - Martin Seeler

0

你在这里使用的JS代码没有任何惰性求值的概念 - .map()方法的返回值已经是一个数组值,在它自己的.map()方法执行之前必须完全被评估。这不是解释器操作的实现细节,而是语言定义的一部分。(与此同时,Java代码的Collection.stream()方法返回一个尚未评估其内容的Stream对象。)

JavaScript确实具有你想要的异步/延迟评估功能,以Promise对象的形式呈现。

以下代码将执行类似于你想要的操作:

var nums = [1,2,3,4,5,6 ];
nums.map(async function(x) {
  x = (x * x);
  console.log('map1='+x);
  return x;
}).map(async function(x) {
  x = await x;
  x = x * 3;
  console.log('map2='+x);
  return x;
}).forEach(async function(x) {
  x = await x;
  console.log('forEach='+x);
});

现在,在实践中,这仍然会以与之前相同的顺序打印输出,因为 Promise 立即解决。但是,这次 map 函数的评估确实是“惰性”的,并且原则上可以以任何顺序发生。要实际测试此功能,我们可以使用异步 sleep() 函数(来自JavaScript 的 sleep() 版本是什么?)在计算中引入延迟:

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

var nums = [1,2,3,4,5,6 ];
nums.map(async function(x) {
  x = (x * x);
  await sleep(1); // 1ms delay
  console.log('map1='+x);
  return x;
}).map(async function(x) {
  x = await x;
  x = x * 3;
  console.log('map2='+x);
  return x;
}).forEach(async function(x) {
  x = await x;
  console.log('forEach='+x);
});

输出:

map1=1
map2=3
forEach=3
map1=4
map2=12
forEach=12
map1=9
map2=27
forEach=27
map1=16
map2=48
forEach=48
map1=25
map2=75
forEach=75
map1=36
map2=108
forEach=108

-2

你会得到相同的输出,因为map1map2forEach值序列的相对值是相同的。

你看到的顺序差异显示了JVM和JavaScript运行时引擎机器模型之间的根本差异。

JVM是线程化的。而JavaScript则不是。这意味着,在Java中,当关键数量的映射操作发生后,你的序列步骤可以立即运行。

在JavaScript中,下一步被放置在执行堆栈的底部,每个位于顶部的操作必须先执行才能到达下一个项目。

正如你所看到的,这些方法在功能上是等效的,但具有不同的机制。


但是在Java中,它也只能使用单个线程运行,同样,在JavaScript中执行的操作也是由一个线程完成的。 - Jaspreet Jolly
他从未使用过.parallel(),因此他的流只在一个线程上运行。仅仅解释是无效的。 - Ferrybig

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接