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
- Review the code below
- Identify how the for ... of loop is used to iterate over
all Enemys
- 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
- Start with the code from example #1
- In the for ... of loop, move each enemy up two pixels
- After the for ... of loop, pause for 1000 ms
- After the pause, create another for ... of
over all sprites of kind Enemy
- In the second for ... of loop, move every
Enemy down two pixels
- 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!
- Review the code below
- Identify how filter is used to identify only the
Enemys on the right side of the screen
- 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
- Start with the code from example #2
- Add a new event for when the player presses the right button
- 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)
- 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?
- In your own words, explain how for ... of loops can be easier to
use than for loops.
- 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:
- No parameters:
() => game.splash("I'm here!")
will
splash “I’m here!” when it is called
- Two parameters:
(a, b) => a + b
will return the sum
of a and b