admin管理员组

文章数量:1406063

What is the best way to implement a simulation of a loop's break statement, when one does iterate through a user/engine defined function?

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

I've thought of implementing forEach in a way that would break when the function returns false. But I would like to hear thoughts on how that is normally done.

What is the best way to implement a simulation of a loop's break statement, when one does iterate through a user/engine defined function?

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

I've thought of implementing forEach in a way that would break when the function returns false. But I would like to hear thoughts on how that is normally done.

Share Improve this question edited Feb 6, 2024 at 8:22 Peter Seliger 13.5k3 gold badges30 silver badges44 bronze badges asked Sep 2, 2012 at 17:32 MaiaVictorMaiaVictor 53.1k47 gold badges158 silver badges302 bronze badges
Add a ment  | 

2 Answers 2

Reset to default 7

returning false is the most mon way to do it. That's what jQuery's iterator function .each() does:

We can break the $.each() loop at a particular iteration by making the callback function return false. Returning non-false is the same as a continue statement in a for loop; it will skip immediately to the next iteration.

And its very simplified implementation:

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

Since the OP explicitly did ask for "simulating ... break ... inside ... forEach", and since the language core now has much more features than 11½ years ago, one actually could implement quite easily a prototypal array method which not only enables a break but also a continue mand, similar to both statements break and continue.

In order to achieve the implementation of the iterating array method, one first needs to write some abstractions which do borrow the basic idea from AbortController and its related AbortSignal.

Thus, one would implement e.g. a 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;
  }
}

... which is going to be used by its PauseController ...

class PauseController {
  #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;
  }
}

... where PausedStateSignal has to extend EventTarget in order to be able of signaling state-changes via dispatchEvent, and where PauseController features the two main methods break and continue.

Both implementations are relying on class syntax, private properties, get syntax and a private, protected state object which gets shared by reference in between a controller and a signal instance. The latter gets achieved by a connecting callback function which is passed at a signal's instantiation time.

Having covered that part, one can continue with the actual implementation of an array method which, in addition of the standard forEach functionality, is capable of three things ...

  • allowing to pause/halt the callback function's execution via break,
  • and via continue allowing ...
    • either to continue a paused/halted loop,
    • or to skip the loop's next iteration step.

The implementation could be named e.g. forEachAsyncBreakAndContinue; it does make use of the above described signal and controller abstractions, might look like follows ...

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

  const controller = new PauseController;
  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-mand before.
      continueLooping();
    } else {
      // - continue-mand while already running which
      //   is equal to skipping  the next occurring cycle.
      ++idx;
    }
  });
  continueLooping();

  return promise;
}

... and finally gets assigned for demonstration purposes via Reflect.defineProperty as forEachAsyncBC to Array.prototype ...

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

The now prototypal forEachAsyncBC method is always going to return a promise. This Promise instance either rejects or resolves; the former in case the provided callback function does raise an error at any time it gets invoked, and the latter in case the iteration cycle has been fully pleted.

Thanks to all the abstractions an executable example code which does test all of the mentioned features can be written as easy as that ...

(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 PauseController {
  #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 `PauseController` 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 PauseController;
  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-mand before.
      continueLooping();
    } else {
      // - continue-mand while already running which
      //   is equal to skipping  the next occurring cycle.
      ++idx;
    }
  });
  continueLooping();

  return promise;
}

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

</script>

本文标签: