The difference between lexical vs dynamic scope

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the architecture category.

Last Updated: 2024-11-21

I found myself perenially confused about JS scoping of this and realized that one key in the puzzle is to understand lexical vs. dynamic scoping.

Generally

Lexical: the caller (where the function is defined) sets the scope of bindings (i.e. values assigned to variables)

Dynamic: the callee (where the function is called) sets the scope of bindings

See the perl code below for an example

x = 10;

sub print_x {
  # In lexically scoped code, this ($x) is always set to 10 since the free variable
  # was equal to 10 in the context where it was defined.  However, as we'll see,
  # this can vary when dynamic scoping is in use
  print $x; 
}

sub static {
  # NB: my creates a lexical variable.
  my $x = 20; # now this setting to 20 doesn't affect $x in the subsequent call
  # to print_x()
  print_x(); # 10, not 20
}

static();

sub dynamic {
  # the effect of local, technically, is to save the old value on a stack when
  # entering the scope with the local statement, and upon exiting, it will restore
  # the old value
  local $x = 20; # affects!
  print_x(); # 20, not 10! 
}

dynamic();

Applying to this in JavaScript:

JavaScript does not have dynamic scope (WRT to where names and variables are looked up). However the this keyword has a dynamic scope-like mechanism. It cares about where a function was called.

function produce() {
  console.log(this.x);
}

const alpha = {produce, x: 1};
const beta  = {produce, x: 2};
const gamma = {produce, x: 3};

console.log(
  alpha.produce(), // 1
  beta.produce(),  // 2
  gamma.produce(), // 3
);

As you can see, the produce function takes a different x value depending on which object it is part of. It is dynamically scoped to the object received. This makes sense actually and goes to the core of how instances of an object work (they use "generic" methods that have access to a localized state)

Compare to the following, which uses arrow functions to use lexical scope

// With an arrow function this within the function is the same as whatever this was
// outside the function when it was created. It is not bound to obj in your
// example, but instead to whatever it was already bound to where that obj is being
// created. - https://stackoverflow.com/questions/48295265/lexical-scope-in-javascript
const produce = () => {
  console.log(this.x);
}

const alpha = {produce, x: 1};
const beta  = {produce, x: 2};
const gamma = {produce, x: 3};

console.log(
  alpha.produce(), // undefined
  beta.produce(),  // undefined
  gamma.produce(), // undefined
);

Here, this is called at the top-level, despite the receiver objects (e.g. alpha.produce(). Since x isn't defined there, we get undefined.

Python makes this potential confusion super explicit by having a self argument as the first parameter when calling instance vars on objects.

References