In short
You are almost correct except for how it works when it goes out of scope.
More details
How are variables "captured" in JavaScript?
JavaScript uses lexical environments to determine which function uses which variable. Lexical environments are represented by environment records. In your case:
- there is a global environment;
- the function
f()
defines its lexical environment, in which x
is defined, even if it is after g()
;
- the inner function
g()
defines its lexical environment which is empty.
So g()
uses x
. Since there is no binding for x
there, JavaScript looks for x
in the enclosing environment. Since it is found therein, the x
in g()
will use the binding of x
in f()
. This looks like lexically scoped binding.
If later you define an x
in the environment where g()
is invoked, g()
would still be bound to the x
in f()
:
function f() {
function g() { console.log(x); }
let x = 0;
g(); // prints 0
x = 1;
g(); // prints 1
return g;
}
let x = 4;
let g = f();
g(); // prints 1 (the last known value in f before returning)
Online demo
This shows that the binding is static and will always refer to the x
known in the lexical scope where g()
was defined.
This excellent article explains in detail how this works, with very nice graphics. It is meant for closures (i.e. anonymous functions with their execution context) but is also applicable to normal functions.
How come that the value of a variable gone out of scope is preserved?
How to explain this very special behavior that JavaScript will always take the current value of x
as long as x
remains in scope (like a reference in C++) whereas it will take the last known value when x
is out of scope (when an out of scope reference in C++ would be UB)? Does JavaScript copies the value into the closure when the variable deceases? No, it is simpler than that!
This has to do with garbage collection: g()
is returned to an outer context. Since g()
uses the x
in f()
, the garbage collector will realize that this x
object of f()
is still in use. So, as long as g()
is accessible, the x
in f()
will be kept alive and remain accessible for its still active bindings. So no need to copy the value: the x
object will just stay (unmodified).
As a proof that it is not a copy, you can study the following code. It defines a second function in the context of f()
that is able to change the (same) x
:
let h;
function f() {
function g() { console.log(x); }
h = function () { x = 27; }
let x = 0;
g(); // prints 0
x = 1;
g(); // prints 1
x = 3;
return g;
}
let x = 4;
let g = f();
g(); // prints 3
h();
g(); // prints 27
Online demo
Edit: Additional bonus article that explains this phenomenon, in a slightly more complex context. Interestingly it explains that this situation can lead to memory leaks if no precaution is taken.