【问题标题】:How can I perform operations in JavaScript just like we do pipeline of operations in Java streams?我如何在 JavaScript 中执行操作,就像我们在 Java 流中执行操作管道一样?
【发布时间】:2019-07-01 06:11:18
【问题描述】:

在使用流的 Java 8 中,当我一个接一个地链接方法时,操作的执行是以流水线方式执行的。

例子:

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 程序中一样的输出?

This question ask only how to collect like in JavaScript but not how internal working changes for same type of methods.

【问题讨论】:

  • 你想要一个来自 JS 的原生解决方案还是寻找一些库/帮助类?
  • 查看Lazy.js 遗憾的是我没有在 CDN 上找到它来在这里创建 sn-p
  • @AdityaGupta 这两个问题在概念上是不同的。
  • 从概念上讲,您要求的是stream 实现。

标签: javascript java


【解决方案1】:

也许以后(或永远不会)您可以使用实际的experimental pipeline operator |&gt;,其语法如下:

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);

管道作为函数 (function composition enabling piping) 与所需函数的闭包相同。

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);

【讨论】:

  • 所有 F# 开发人员都称赞管道运算符 :D
【解决方案2】:

如果您将每个函数操作放入一个数组中,您可以使用 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)。

【讨论】:

    【解决方案3】:

    等效于 Java 的流是 JavaScript 的 iterators。不幸的是,迭代器对象没有map 方法(yet),但您可以轻松地自己编写一个(如果您需要方法语法,甚至可以使用install it on the prototype)。

    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,则不需要依赖执行顺序。

    【讨论】:

    • 不要改变你不是开发者的任何类型的原型。如果每个人都在改变原型,任何库都可能破坏任何其他库,因为它们会覆盖彼此的原型突变。 stackoverflow.com/q/14034180/4146602
    • @Suppen 同意了。不过,您可以使用迭代器方法提案中的 polyfill。
    【解决方案4】:

    既然我们有解决方案,为什么还要从头开始重新发明。此功能存在于 lodash/RxJS/stream.js 中。

    来自 lodash 的示例 sn-p:

    _.flow(
     _.assign(rows[0]),
     _.omit('blah')
    )(foundUser);
    
    // >> {"charData":[],"ok": 1}
    

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

    【讨论】:

    • 但是,如果我不想只为这种类型的场景再加载一个库来加载我的页面怎么办?
    • 有效点!但是,添加一个库比编写臃肿的代码要好。您可以使用库摆脱样板代码。
    • 我们也可以反对这样一个事实,即与 Java 不同,默认运行时 Javascript 没有那么多东西,这就是为什么我们需要在 JS 中如此快速的库。
    • 你在Java案例中仍然使用库,不同的是它们是Oracle提供的
    • @Caleth 不,不同之处在于 Java 中的库不会使您的应用程序膨胀。
    【解决方案5】:

    您在这里使用的 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() 函数(来自What is the JavaScript version of 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
    

    【讨论】:

      【解决方案6】:

      我建议使用 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代码示例从不调用.paralel(),因此从不在多个线程上执行。线程与该问题没有任何关系。 Java 中的流与迭代器的工作方式几乎相同,一次从开始到源的一项。 OP 还要求提供与他的 Java 代码相同的 JavaScript 代码,而不是与他的 JavaScript 代码相同的 Java 代码
      • 好的,那么我可能对 Java 内部有错误的期望,对此感到抱歉。但是你能解释一下你的第二点吗?我确实提供了 Javascript 代码而不是 Java 代码,所以我不明白你的意思。
      • 我想我在说第二点时犯了一个错误,我想我错过了上面段落中的“喜欢”这个词(根据我发表评论的时间,我好像做了当我乘坐地铁旅行并使用 SE 应用程序时发表评论)。很抱歉在第二点上浪费了您的时间。我做了一些研究,您推荐的“Rx.Observable”库似乎与简单情况下的 java 流几乎相同,所以如果您可以编辑您的帖子以删除它,我实际上可以收回我的反对意见
      • 好的,谢谢您的解释。我删除了关于 Java 的部分和误导性的“喜欢”部分。
      【解决方案7】:

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

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

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

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

      如您所见,这些方法在功能上是等效的,但具有不同的机制。

      【讨论】:

      • 但是在java中它也是单线程运行的,类似的javascript操作是由一个线程执行的。
      • 他从未使用过 .parallel (),因此他的流仅在 1 个线程上运行。只是解释无效
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-06-03
      • 2023-01-17
      • 1970-01-01
      • 2013-09-04
      • 2010-10-30
      • 1970-01-01
      • 2015-11-13
      相关资源
      最近更新 更多