Function and Block Scope in Javascript and Swift
Sep 17, 2018
Function and Block Scope in Javascript and Swift

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.

Pierre Liebenberg, Phase 2 UI/UX Designer and iOS Developer
Pierre Liebenberg Author
I’m a UI / UX Designer and sometime iOS Developer at Phase 2, a team of skilled, passionate, and engaged people who love crafting beautiful, usable software. You can see my most recent development work at lexico.app.