The illusion of magic relies on the magician knowing one more fact than the audience. As is the case with magic tricks, understanding why the snippets below don’t produce the same outcome relies on knowing how Javascript handles block scope. For the difference between block and function scope, have a look here.
Problem
As a Swift developer and UI / UX designer who occasionally dabbles with Javascript when no better alternative presents itself, I stumbled into this gotcha recently while experimenting with Framer’s code pane, which uses CoffeeScript as its Javascript pre-compiler. Consider the Swift snippet below:
class Spaceship {
let warpDrive: Bool
let numberOfTeleporters: Int
let evacuationProtocol: () -> ()
init (warpDrive: Bool, numberOfTeleporters: Int, evacuationProtocol: @escaping () -> ()) {
self.warpDrive = warpDrive
self.numberOfTeleporters = numberOfTeleporters
self.evacuationProtocol = evacuationProtocol
}
}
var fleetOfSpaceships = [Spaceship]()
for i in 0...2 {
let evacuationProtocol = {
print("Number of crew members to evacuate: \(i).")
}
let spaceship = Spaceship(
warpDrive: true,
numberOfTeleporters: i,
evacuationProtocol: evacuationProtocol
)
fleetOfSpaceships.append(spaceship)
}
print("Number of teleporters: \(fleetOfSpaceships[0].numberOfTeleporters).") // Number of teleporters: 0.
print("Number of teleporters: \(fleetOfSpaceships[1].numberOfTeleporters).") // Number of teleporters: 1.
print("Number of teleporters: \(fleetOfSpaceships[2].numberOfTeleporters).") // Number of teleporters: 2.
fleetOfSpaceships[0].evacuationProtocol() // Number of crew members to evacuate: 0.
fleetOfSpaceships[1].evacuationProtocol() // Number of crew members to evacuate: 1.
fleetOfSpaceships[2].evacuationProtocol() // Number of crew members to evacuate: 2.
I expected each line printed to the console to have a number that is one more than the number in the preceding line. So far, so good.
Now, let’s perform the same task in Javascript:
class Spaceship {
constructor(warpDrive, numberOfTeleporters, evacuationProtocol) {
this.warpDrive = warpDrive;
this.numberOfTeleporters = numberOfTeleporters;
this.evacuationProtocol = evacuationProtocol;
}
}
var fleetOfSpaceships = []
for (var i=0; i <= 2; i++) {
var evacuationProtocol = function() {
console.log("Number of crew members to evacuate: " + i +".");
};
var spaceship = new Spaceship(true, i, evacuationProtocol);
fleetOfSpaceships.push(spaceship);
}
console.log("Number of teleporters: " + fleetOfSpaceships[0].numberOfTeleporters + "."); // Number of teleporters: 0.
console.log("Number of teleporters: " + fleetOfSpaceships[1].numberOfTeleporters + "."); // Number of teleporters: 1.
console.log("Number of teleporters: " + fleetOfSpaceships[2].numberOfTeleporters + "."); // Number of teleporters: 2.
fleetOfSpaceships[0].evacuationProtocol(); // Number of crew members to evacuate: 3.
fleetOfSpaceships[1].evacuationProtocol(); // Number of crew members to evacuate: 3.
fleetOfSpaceships[2].evacuationProtocol(); // Number of crew members to evacuate: 3.
So what’s going on here? In languages like Swift and C, variables introduced with a block of code such as a
for
loop are scoped to that block. Therefore, in Swift,
for i in 0...2 {
print(i)
}
print(i) // Error: use of unresolved identifier 'i'
produces an error, because
i
does not exist outside of the
for
loop’s scope.
In Javascript, on the other hand, this snippet:
for (i = 0; i <=2; i++) {
console.log(i);
}
console.log(i); // 3
produces ‘3’.
According to
Mozilla’s developer documentation on the matter,
the reason is that “[v]ariables declared with
var
do not
have block scope. Variables introduced with a block are scoped to the containing function or script, and the effects of setting them persist beyond the block itself. In other words, block statements do not introduce a scope.”
So, in our spaceship example above, when we call
evacuationProtocol()
on a spaceship, the function references
i
, but not the value of
i
as it existed within the scope of the
for
loop. Instead, it references
i
as it exists
after
the
for
loop completed - that is, at the point in time when the
evacuationProtocol()
method is called.
In contrast, the value for
numberOfTeleporters
behaves as expected (to a Swift developer), because it is assigned at the time of each iteration of the
for
loop. To see this mechanism in action, add the following line to the end of the Javascript snippet’s
for
loop:
fleetOfSpaceships[i].evacuationProtocol()
Output:
// Number of crew members to evacuate: 0.
// Number of crew members to evacuate: 1.
// Number of crew members to evacuate: 2.
// Number of teleporters: 0.
// Number of teleporters: 1.
// Number of teleporters: 2.
// Number of crew members to evacuate: 3.
// Number of crew members to evacuate: 3.
// Number of crew members to evacuate: 3.
So how do you get around this feature (or pitfall, depending on your outlook ) of Javascript?
Solution
Instead of declaring
i
by using the
var
keyword, use the
let
keyword since variables declared with
let
(or
const
, for that matter) do have block scope.