Cheating Lexical
If lexical scope is defined only by where a function is declared, which is entirely an author-time decision, how could there possibly be a way to “modify” (aka, cheat) lexical scope at run-time?
JavaScript has two such mechanisms. Both of them are equally frowned-upon in the wider community as bad practices to use in your code. But the typical arguments against them are often missing the most important point: cheating lexical scope leads to poorer performance.
Before I explain the performance issue, though, let’s look at how these two mechanisms work.
eval
The eval(..)
function in JavaScript takes a string as an argument, and treats the contents of the string as if it had actually been authored code at that point in the program. In other words, you can programmatically generate code inside of your authored code, and run the generated code as if it had been there at author time.
Evaluating eval(..)
(pun intended) in that light, it should be clear how eval(..)
allows you to modify the lexical scope environment by cheating and pretending that author-time (aka, lexical) code was there all along.
On subsequent lines of code after an eval(..)
has executed, the Engine will not “know” or “care” that the previous code in question was dynamically interpreted and thus modified the lexical scope environment. The Engine will simply perform its lexical scope look-ups as it always does.
Consider the following code:
function foo(str, a) {
eval( str ); // cheating!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1 3
The string "var b = 3;"
is treated, at the point of the eval(..)
call, as code that was there all along. Because that code happens to declare a new variable b
, it modifies the existing lexical scope of foo(..)
. In fact, as mentioned above, this code actually creates variable b
inside of foo(..)
that shadows the b
that was declared in the outer (global) scope.
When the console.log(..)
call occurs, it finds both a
and b
in the scope of foo(..)
, and never finds the outer b
. Thus, we print out “1 3” instead of “1 2” as would have normally been the case.
Note: In this example, for simplicity’s sake, the string of “code” we pass in was a fixed literal. But it could easily have been programmatically created by adding characters together based on your program’s logic. eval(..)
is usually used to execute dynamically created code, as dynamically evaluating essentially static code from a string literal would provide no real benefit to just authoring the code directly.
By default, if a string of code that eval(..)
executes contains one or more declarations (either variables or functions), this action modifies the existing lexical scope in which the eval(..)
resides. Technically, eval(..)
can be invoked “indirectly”, through various tricks (beyond our discussion here), which causes it to instead execute in the context of the global scope, thus modifying it. But in either case, eval(..)
can at runtime modify an author-time lexical scope.
Note: eval(..)
when used in a strict-mode program operates in its own lexical scope, which means declarations made inside of the eval()
do not actually modify the enclosing scope.
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );
There are other facilities in JavaScript which amount to a very similar effect to eval(..)
. setTimeout(..)
and setInterval(..)
can take a string for their respective first argument, the contents of which are eval
uated as the code of a dynamically-generated function. This is old, legacy behavior and long-since deprecated. Don’t do it!
The new Function(..)
function constructor similarly takes a string of code in its last argument to turn into a dynamically-generated function (the first argument(s), if any, are the named parameters for the new function). This function-constructor syntax is slightly safer than eval(..)
, but it should still be avoided in your code.
The use-cases for dynamically generating code inside your program are incredibly rare, as the performance degradations are almost never worth the capability.
with
The other frowned-upon (and now deprecated!) feature in JavaScript which cheats lexical scope is the with
keyword. There are multiple valid ways that with
can be explained, but I will choose here to explain it from the perspective of how it interacts with and affects lexical scope.
with
is typically explained as a short-hand for making multiple property references against an object without repeating the object reference itself each time.
For example:
var obj = {
a: 1,
b: 2,
c: 3
};
// more "tedious" to repeat "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// "easier" short-hand
with (obj) {
a = 3;
b = 4;
c = 5;
}
However, there’s much more going on here than just a convenient short-hand for object property access. Consider:
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 -- Oops, leaked global!
In this code example, two objects o1
and o2
are created. One has an a
property, and the other does not. The foo(..)
function takes an object reference obj
as an argument, and calls with (obj) { .. }
on the reference. Inside the with
block, we make what appears to be a normal lexical reference to a variable a
, an LHS reference in fact (see Chapter 1), to assign to it the value of 2
.
When we pass in o1
, the a = 2
assignment finds the property o1.a
and assigns it the value 2
, as reflected in the subsequent console.log(o1.a)
statement. However, when we pass in o2
, since it does not have an a
property, no such property is created, and o2.a
remains undefined
.
But then we note a peculiar side-effect, the fact that a global variable a
was created by the a = 2
assignment. How can this be?
The with
statement takes an object, one which has zero or more properties, and treats that object as if it is a wholly separate lexical scope, and thus the object’s properties are treated as lexically defined identifiers in that “scope”.
Note: Even though a with
block treats an object like a lexical scope, a normal var
declaration inside that with
block will not be scoped to that with
block, but instead the containing function scope.
While the eval(..)
function can modify existing lexical scope if it takes a string of code with one or more declarations in it, the with
statement actually creates a whole new lexical scope out of thin air, from the object you pass to it.
Understood in this way, the “scope” declared by the with
statement when we passed in o1
was o1
, and that “scope” had an “identifier” in it which corresponds to the o1.a
property. But when we used o2
as the “scope”, it had no such a
“identifier” in it, and so the normal rules of LHS identifier look-up (see Chapter 1) occurred.
Neither the “scope” of o2
, nor the scope of foo(..)
, nor the global scope even, has an a
identifier to be found, so when a = 2
is executed, it results in the automatic-global being created (since we’re in non-strict mode).
It is a strange sort of mind-bending thought to see with
turning, at runtime, an object and its properties into a “scope” with “identifiers”. But that is the clearest explanation I can give for the results we see.
Note: In addition to being a bad idea to use, both eval(..)
and with
are affected (restricted) by Strict Mode. with
is outright disallowed, whereas various forms of indirect or unsafe eval(..)
are disallowed while retaining the core functionality.
Performance
Both eval(..)
and with
cheat the otherwise author-time defined lexical scope by modifying or creating new lexical scope at runtime.
So, what’s the big deal, you ask? If they offer more sophisticated functionality and coding flexibility, aren’t these good features? No.
The JavaScript Engine has a number of performance optimizations that it performs during the compilation phase. Some of these boil down to being able to essentially statically analyze the code as it lexes, and pre-determine where all the variable and function declarations are, so that it takes less effort to resolve identifiers during execution.
But if the Engine finds an eval(..)
or with
in the code, it essentially has to assume that all its awareness of identifier location may be invalid, because it cannot know at lexing time exactly what code you may pass to eval(..)
to modify the lexical scope, or the contents of the object you may pass to with
to create a new lexical scope to be consulted.
In other words, in the pessimistic sense, most of those optimizations it would make are pointless if eval(..)
or with
are present, so it simply doesn’t perform the optimizations at all.
Your code will almost certainly tend to run slower simply by the fact that you include an eval(..)
or with
anywhere in the code. No matter how smart the Engine may be about trying to limit the side-effects of these pessimistic assumptions, there’s no getting around the fact that without the optimizations, code runs slower.