Note: You will have a much easier time understanding this post if you already know how environments work in ECMAScript 5.
Let us start with a quick summary of environments and execution contexts in ECMAScript 5:
Lexical environments hold variables and parameters. The currently active environment is managed via a stack of execution contexts (which grows and shrinks in sync with the call stack). Nested scopes are handled by chaining environments: each environment points to its outer environment (whose scope surrounds its scope). In order to enable lexical scoping, functions remember the scope (=environment) they were defined in. When a function is invoked, a new environment is created for is arguments and local variables. That environment’s outer environment is the function’s scope.Another concept that is important to understand is hoisting: In JavaScript, any variable declaration var x=v; is split into two parts:
- The declaration is moved to the beginning of the surrounding function as var x;.
- The initializer becomes a simple assignment x=v; that replaces the declaration.
Data structures
A (lexical) environment is the following data structure [ES5, 10.2]:- A reference to the outer environment (null in the global environment).
- An environment record maps identifiers to values. There are two kinds of environment records:
- declarative environment records: store the effects of variable declarations, and function declarations.
- object environment records: are used by the with statement and for the global environment. They turn an object into an environment. For with, that is the argument of the statement. For the global environment, that is the global object.
- Environments: two references to environments.
- LexicalEnvironment (lookup and change existing): resolve identifiers.
- VariableEnvironment (add new): hold bindings made by variable declarations and function declarations.
- ThisBinding: the current value of this.
Handling temporary scopes via LexicalEnvironment and VariableEnvironment
LexicalEnvironment and VariableEnvironment are always the same, except in one case: When there is a dominant outer scope and one temporarily wants to enter an inner scope. In the inner scope, a few new bindings should be accessible, but all new bindings made inside of it should be added to the outer scope. This is done as follows:- LexicalEnvironment temporarily points to a new environment that has been put in front of the old LexicalEnvironment. The new environment holds the temporary bindings of the inner scope.
- VariableEnvironment does not change its value and is thus still the same as the old LexicalEnvironment, denoting the outer scope. New bindings are added here and will also be found when doing a lookup via LexicalEnvironment, because the latter comes before the former in the environment chain.
- After leaving the temporary scope, LexicalEnvironment’s old value is restored and it is again the same as VariableEnvironment.
- with statement [ES5, 12.10]: the object that is the argument of the statement becomes a temporary environment.
- catch clause [ES5, 12.14]: the exception that is the argument of this clause is made available via a temporary environment.
Functions and their scope: declarations versus expressions
Another area where the difference between LexicalEnvironment and VariableEnvironment can be observed are function definitions: function declarations use the VariableEnvironment as scope, while function expressions use the LexicalEnvironment. For function declarations, the motivation is to move the declaration to the (dominant) function scope. They should thus only see what exists at that level. The following code example illustrates how that works.
var foo = "abc";
with({ foo: "bar" }) {
function f() {
console.log(foo);
}
f();
}
Console output is “bar” on Firefox and Rhino, “abc” on V8 (Chrome, node.js). The latter more closely mirrors the specification, the former is probably what most programmers would expect.
If you instead use a function expression, the output is “bar” on all platforms.
var foo = "abc";
with({ foo: "bar" }) {
(function() { console.log(foo); }());
}
[Thanks to Allen Wirfs-Brock for helping me understand some of the finer points of LexicalEnvironment and VariableEnvironment.]
References:


4 comments:
Yep, good overview and everything's correct.
I have written a detailed articled on general theory of lexical environments, perhaps it will be interesting for you: http://dmitrysoshnikov.com/ecmascript/es5-chapter-3-1-lexical-environments-common-theory/
It's the first part and there will be chapter 3.2 devoted already for environments in ES where I also will describe all the subtle cases including the difference of the LE and VE in details.
Dmitry.
Thanks for the pointer. I’ve focused on making things understandable, so it’s nice to know that there is more thorough material out there.
Very informative
Regarding the different behavior in the with statement. FunctionDeclarations inside Blocks are not part of any version of ECMAScript. All engines do support it but with differences in their semantics. All engines except SpiderMonkey (and Rhino) hoist the function. SpiderMonkey only creates the name binding if the function declaration is ever reached.
if (false)
function f() {}
alert(f) // throws in SpiderMonkey
According to the ECMAScript standard, it is not legal to put a function declaration inside the body of a compound statement (the "with" statement in your example). I'm not sure that it's a good idea to use a non-standard example to explain how the standard works...
Post a Comment