Advanced Topics: For Each Loops

Arrays can be traversed using a simple for loop, using each index from 0 to one less than the length of the array.

This can have a few downsides; for one, the index will often not be relevant outside of keeping track of the different elements of the array.

Additionally, the array might be created and intended to be used only within the loop, like destroying random Enemies using sprites.allOfKind; keeping the array around after the loop might result in bugs when the array is unintentionally used later on.

For each loops will iterate through each element in an array.

Concept: For Each loop syntax

In JavaScript, a for each loop is generally represented using the for ... of statement, shown in the snippet below.

let words: string[] = ["hello", "world"]; for (let element of words) { game.splash(element); }

In this snippet, element is the loop variable (like i in a normal for loop), and is changed to a different element from the array words on each iteration of the loop.

This is done in order from the first element (index 0) to the last element in the array.

Example #1: For Each Enemy

  1. Review the code below
  2. Identify how the for ... of loop is used to iterate over all Enemys
  3. Identify what is done to each Enemy in the for ... of loop
for (let i = 0; i < 15; i++) { let skeleton: Sprite = sprites.create(sprites.castle.skellyFront, SpriteKind.Enemy); skeleton.x = randint(0, screen.width); skeleton.y = randint(0, screen.height); } controller.A.onEvent(ControllerButtonEvent.Pressed, function () { for (let enemy of sprites.allOfKind(SpriteKind.Enemy)) { enemy.say("hi!", 1000); } });

Student Task #1: Spooky Skeleton

  1. Start with the code from example #1
  2. In the for ... of loop, move each enemy up two pixels
  3. After the for ... of loop, pause for 1000 ms
  4. After the pause, create another for ... of over all sprites of kind Enemy
  5. In the second for ... of loop, move every Enemy down two pixels
  6. Challenge: in your own words, explain why this behavior couldn’t be (easily) handled using only the first for ... of loop. It may help to temporarily put the pause inside that loop to test

The for ... of statement is sufficient in most all cases to handle this sort of behavior.

The rest of this appendix is quite a bit more complex, and potentially confusing: it is included mostly as a reference for use as needed. The topics introduced are commonly used in the functional programming paradigm.

Concept: Filter and For Each

In JavaScript, there is another common implementation of the for each loop: the array.forEach function. This applies the given function to each element in the array.

The function that is passed to array.forEach is allowed to have up to two parameters: the first, the element in the array, and the second, the index of that element in the array.

["Hello", "world"] .forEach(function (element: string, index: number) { game.splash(element); });

There are a number of other functions that can be used in this way. One of the most common is array.filter. Filter accepts a function with up to two parameters (the same allowed in forEach) that returns a boolean value.

Filter then returns a new array containing only the elements that the given function returned true for.

let myNumbers: number[] = [1, 2, 3, 5, 8, 4]; myNumbers.filter(function (element: number) { return element <= 4; }).forEach(function (element: number) { game.splash(element + " is not greater than 4!"); });

The snippet above will first filter out any elements from the array myNumbers that are greater than 4, and then iterate over any remaining elements in the array.

Example #2: Filter the Enemies!

  1. Review the code below
  2. Identify how filter is used to identify only the Enemys on the right side of the screen
  3. Identify how the forEach changes the Enemys that are on the right half of the screen
for (let i = 0; i < 15; i++) { let skeleton: Sprite = sprites.create(sprites.castle.skellyFront, SpriteKind.Enemy); skeleton.x = randint(0, screen.width); skeleton.y = randint(0, screen.height); } controller.left.onEvent(ControllerButtonEvent.Pressed, function () { let allEnemies: Sprite[] = sprites.allOfKind(SpriteKind.Enemy); allEnemies.filter(function (element: Sprite) { return element.x > screen.width / 2; }).forEach(function (element: Sprite) { element.x -= screen.width / 2; }); });

Student Task #2: Move to the Right

  1. Start with the code from example #2
  2. Add a new event for when the player presses the right button
  3. In this event, use filter to select all Enemys that are on the left half of the screen, and forEach to move any Enemys that are on the left to the right side of the screen (this will be effectively the opposite of the left button event, so duplicating that to start will likely be helpful)
  4. Challenge: recreate the same behavior for up and down, so that the enemies can be moved to the top or bottom half of the screen

What did we learn?

  1. In your own words, explain how for ... of loops can be easier to use than for loops.
  2. How do filter and forEach allow arrays to be used more easily?

Sidenote: Arrow Functions

The arrow function (=>) is very useful when modifying code in cases like this. This is an alternate form of a function that is not covered in detail in this course, but allows for clearer formatting when programming in this style.

for (let i = 0; i < 15; i++) { let skeleton: Sprite = sprites.create(sprites.castle.skellyFront, SpriteKind.Enemy); skeleton.x = randint(0, screen.width); skeleton.y = randint(0, screen.height); } controller.B.onEvent(ControllerButtonEvent.Pressed, function () { let allEnemies: Sprite[] = sprites.allOfKind(SpriteKind.Enemy); allEnemies .filter(element => element.x > screen.width / 2) .forEach(element => element.x -= screen.width / 2); });

The snippet above will behave exactly the same as example #2. The line element => element.x > screen.width / 2 says “take the first element, pass it as a parameter to the function on the right side of the =>, and return the result of the statement on the right.

Arrow Functions can also be created that are more than a single line using curly braces {}, but these will not automatically return the result of the statement and will behave similarly to other functions.

Additionally, if an arrow function requires more than a single parameter (or no parameters), parentheses need to be used to group the parameters:

  1. No parameters: () => game.splash("I'm here!") will splash “I’m here!” when it is called
  2. Two parameters: (a, b) => a + b will return the sum of a and b