如何在数组迭代、自定义实现的“forEach”函数/方法中模拟循环的“break”语句?

问题描述 投票:0回答:2

当迭代用户/引擎定义的函数时,实现循环

break
语句模拟的最佳方法是什么?

forEach([0, 1, 2, 3, 4], function (n) {
  console.log(n);
  
  if (n === 2) {
    break;
  }
});

我想过以一种当函数返回

forEach
时会中断的方式实现
false
。但我想听听关于通常如何完成的想法。

javascript foreach iteration break continue
2个回答
7
投票

return
ing
false
是最常见的方法。这就是 jQuery 迭代器函数
.each()
的作用:

我们可以通过使 回调函数返回false。返回非 false 与 a 相同 for 循环中的 continue 语句;它会立即跳到下一个 迭代。

及其非常简化的实现:

each: function( object, callback ) {
  var i = 0, length = object.length,
  for ( var value = object[0]; 
        i < length && callback.call( value, i, value ) !== false; // break if false is returned by the callback 
        value = object[++i] ) {}
  return object;
}

-1
投票

由于 OP 明确要求 “在 [a]

break
[循环]”内模拟 [the]
forEach
[行为]”
,并且由于语言核心现在比 11.5 年前拥有更多功能,实际上可以很容易地实现一种原型数组方法,它不仅可以启用
break
,还可以启用
continue
命令,类似于
break
continue
这两个语句。

为了实现迭代数组方法,首先需要编写一些抽象,这些抽象借鉴了

AbortController
及其相关的
AbortSignal
的基本思想。

因此,人们可以实施例如一个

PausedStateSignal
...

class PausedStateSignal extends EventTarget {
  // shared protected/private state.
  #state;

  constructor(connect) {
    super();

    this.#state = {
      isPaused: false,
    };
    connect(this, this.#state);
  }
  get paused() {
    return this.#state.isPaused;
  }
}

...将由其

BreakAndContinueController
...

使用
class BreakAndContinueController {
  #signal;
  #signalState;

  constructor() {
    new PausedStateSignal((signal, signalState) => {

      this.#signal = signal;
      this.#signalState = signalState;
    });
    this.#signalState.isPaused = false;
  }
  get signal() {
    return this.#signal;
  }

  break() {
    const isPaused = this.#signalState.isPaused;

    if (!isPaused) {
      this.#signalState.isPaused = true;
    }
    this.#signal.dispatchEvent(
      new CustomEvent('break', { detail: { pausedBefore: isPaused } })
    );
    return !isPaused;
  }
  continue() {
    const isPaused = this.#signalState.isPaused;

    if (isPaused) {
      this.#signalState.isPaused = false;
    }
    this.#signal.dispatchEvent(
      new CustomEvent('continue', { detail: { pausedBefore: isPaused } })
    );
    return isPaused;
  }
}

...其中

PausedStateSignal
必须扩展
EventTarget
才能通过
dispatchEvent
发出状态变化信号,并且
BreakAndContinueController
具有两个主要方法
break
continue

两种实现都依赖于 class 语法、私有属性

get
语法以及私有、受保护的
state
对象,该对象通过在控制器和信号实例之间的引用共享。后者通过在信号实例化时传递的
connect
ing 回调函数来实现。

介绍完该部分后,我们可以继续实际实现数组方法,除了标准

forEach
功能之外,该方法还能够完成三件事......

  • 允许通过 break,
     
    暂停/停止回调函数的执行
  • 并通过
    continue
    允许...
    • 要么 继续暂停/停止的循环
    • 跳过循环的下一个迭代步骤

该实现可以命名为,例如

forEachAsyncBreakAndContinue
,利用上述信号和控制器抽象,可能如下所示...

function forEachAsyncBreakAndContinue(callback, context = null) {
  const { promise, reject, resolve } = Promise.withResolvers();

  const controller = new BreakAndContinueController;
  const { signal } = controller;

  const arr = this;
  const { length } = arr;

  let idx = -1;

  function continueLooping() {
    while(++idx < length) {

      if (signal.paused) {
        --idx;

        break;
      }
      try {
        callback.call(context, arr.at(idx), idx, arr, controller);

      } catch (exception) {

        reject(exception.message ?? String(exception));
      }
    }
    if (idx >= length) {

      resolve({ success: true });
    }
  }
  signal.addEventListener('continue', ({ detail: { pausedBefore } }) => {
    if (pausedBefore) {
      // - continue after already having
      //   encountered a break-command before.
      continueLooping();
    } else {
      // - continue-command while already running which
      //   is equal to skipping  the next occurring cycle.
      ++idx;
    }
  });
  continueLooping();

  return promise;
}

...最后通过

Reflect.defineProperty
分配用于演示目的,如
forEachAsyncBC
Array.prototype
...

Reflect.defineProperty(Array.prototype, 'forEachAsyncBC', {
  value: forEachAsyncBreakAndContinue,
});

现在的原型

forEachAsyncBC
方法总是会返回一个承诺。这个承诺要么拒绝,要么解决;前者是指提供的回调函数在调用时确实会引发错误,后者是指迭代周期已完全完成的情况。

感谢所有抽象,可以像这样简单地编写测试所有提到的功能的可执行示例代码......

(async () => {

  const result =  await [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
    .forEachAsyncBC((value, idx, arr, controller) => {

      console.log({ value, idx });

      if (value === 9 || value === 3) {
        console.log(`... skip over next value => ${ arr[idx + 1] } ...`);

        // skip over.
        controller.continue();

      } else  if (value === 4 || value === 6) {

        console.log(`... break at value ${ value } ... continue after 5 seconds ...`);
        setTimeout(controller.continue.bind(controller), 5000);

        // break loop.
        controller.break();
      }
    });

  console.log({ result });

})();
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>

class PausedStateSignal extends EventTarget {
  // shared protected/private state.
  #state;

  constructor(connect) {
    super();

    this.#state = {
      isPaused: false,
    };
    connect(this, this.#state);
  }
  get paused() {
    return this.#state.isPaused;
  }
}

class BreakAndContinueController {
  #signal;
  #signalState;

  constructor() {
    new PausedStateSignal((signal, signalState) => {

      this.#signal = signal;
      this.#signalState = signalState;
    });
    this.#signalState.isPaused = false;
  }
  get signal() {
    return this.#signal;
  }

  break() {
    const isPaused = this.#signalState.isPaused;

    if (!isPaused) {
      this.#signalState.isPaused = true;
    }
    this.#signal.dispatchEvent(
      new CustomEvent('break', { detail: { pausedBefore: isPaused } })
    );
    return !isPaused;
  }
  continue() {
    const isPaused = this.#signalState.isPaused;

    if (isPaused) {
      this.#signalState.isPaused = false;
    }
    this.#signal.dispatchEvent(
      new CustomEvent('continue', { detail: { pausedBefore: isPaused } })
    );
    return isPaused;
  }
}

// - asynchronously implemented `forEach` array method which
//   provides a `BreakAndContinueController` instance as 4th
//   parameter to its callback function, where the latter's
//   two methods `break` and `continue` enable the following ...
//
//    - pause a `forEach` loop by invoking `break`.
//    - by invoking `continue` ...
//       - either continuing a paused `forEach` loop.
//       - or skipping the `forEach` loop's next iteration step.
//
function forEachAsyncBreakAndContinue(callback, context = null) {
  const { promise, reject, resolve } = Promise.withResolvers();

  const controller = new BreakAndContinueController;
  const { signal } = controller;

  const arr = this;
  const { length } = arr;

  let idx = -1;

  function continueLooping() {
    while(++idx < length) {

      if (signal.paused) {
        --idx;

        break;
      }
      try {
        callback.call(context, arr.at(idx), idx, arr, controller);

      } catch (exception) {

        reject(exception.message ?? String(exception));
      }
    }
    if (idx >= length) {

      resolve({ success: true });
    }
  }
  signal.addEventListener('continue', ({ detail: { pausedBefore } }) => {
    if (pausedBefore) {
      // - continue after already having
      //   encountered a break-command before.
      continueLooping();
    } else {
      // - continue-command while already running which
      //   is equal to skipping  the next occurring cycle.
      ++idx;
    }
  });
  continueLooping();

  return promise;
}

Reflect.defineProperty(Array.prototype, 'forEachAsyncBC', {
  value: forEachAsyncBreakAndContinue,
});

</script>

© www.soinside.com 2019 - 2024. All rights reserved.