Cheating Scope: Eval, Try-Catch, Let, and Const
February 01, 2018
Introduction
In the last two articles, we talked about the processes by which JavaScript creates scopes. We learned that scopes are created during compilation phase, more accurately during lexing. A global scope is created when runtime begins and each function that the compiler encounters is given its own lexical scope. We also learned about execution contexts, and how the way they are created explains the behavior of scopes.
Right now, we have only talked about two types of scopes: global scopes and functional local scopes. On the surface, this is all there is. But if we start to dig deeper into JavaScript, we will find some very interesting behaviors that bend the rules that we have learned so far. In a nutshell, they give us the possibility of “cheating” JavaScript’s scopes. We will talk about them and we will discuss if it is a good idea to use them or not. Some of them are useful, some of them are not recommended. But even the ones that are considered “bad practices” are interesting to understand. So, let’s begin!
The Eval function
eval()
is a built-in function that you can call with a string as an argument. The string that you pass to it should be a string representing JavaScript code: an expression, a statement, or a series of statements. The string can contain variables and properties of objects that exist in other parts of your script. eval()
takes the string, converts it into code, and executes it. For example:
var _a = 1;
function example(a) {
var b = 2;
eval('a = 3; var c = a + b;');
console.log(c); //5
console.log(a); //3
console.log(_a); //1
}
example(_a);
console.log(a); //ReferenceError
console.log(c); //ReferenceError (You can't have two errors,
//as JavaScript would stop executing
//after the first error is thrown;
//this is only for our learning purposes)
In this example we have a global variable a
and a function called example()
. Inside of the function, a local variable b
is declared and assigned. Then, inside eval()
, we pass a string with the following statements: a = 3
, and var c = a + b
. eval()
assigns to a
a value of 3
, declares a new variable c
, and assigns to it the sum of a
and b
. As c
is created inside of eval()
, when we call console.log(c)
, it prints 5
(3 + 2) in the console.
Pay attention to the variable handling. c
exists in example()
’s scope, and so it can’t be accessed from outside of the function. a
is the parameter’s identifier, and so it is not REALLY variable _a
, but just a copy that “lives” inside example()
’s scope. And so, it is also not available from outside of the function. As you can see, _a
is left untouched, as eval
is reassigning the function’s argument, not the global variable.
Pretty cool, right? Well, perhaps you will be disappointed. eval()
looks amazing on the surface. You could “construct” code programmatically, character by character, and then pass it to eval()
to execute:
function example_2() {
var _string;
_string = 'var';
_string = _string + ' ';
_string = _string + 'a';
_string = _string + ' ';
_string = _string + '=';
_string = _string + ' ';
_string = _string + '1';
eval(_string); //var a = 1
console.log(a); //1
}
example_2();
But it has its downside, and it’s pretty bad. For starters, how can eval()
perform such an action? It is a function, isn’t it? This means that it is ignored until execution (as it is a function property of the global object, it is not even declared at runtime). So, how can the interpreter run the code INSIDE eval()
if it doesn’t know what’s in there until the execution phase?
What happens is the following:
- In compilation phase (creation stage for the execution context), the compiler establishes which variables are declared and where they are declared. By doing this, lexical scopes are created.
- When the interpreter starts executing the code, it has the confidence that the compiler has already declared the variables and constructed the scopes. And so, the interpreter doesn’t need to bother with that.
- The problem begins when the interpreter encounters
eval()
. It says to itself (this only happens in our imagination, of course, the interpreter doesn’t SAY anything to itself): “Oh, my, an eval() function! Within the string that this function has as argument, I could find variable or function declarations! How can I trust what the compiler has done if it had no access to this information beforehand?”. - So, just in case, the interpreter calls his buddy the compiler and tells it to repeat the whole process of creating lexical scopes. And so,
eval()
modifies the scope in which it resides DURING execution stage.
Why is this a problem? Well, because JavaScript performs several optimizations during compilation to achieve better performance. When Javascript’s engine encounters an eval()
, it knows that the optimizations won’t matter, because it will have to repeat the process again. So, not only does it not perform the optimizations, but it has to modify and re-create the lexical scopes for a second time.
Besides, if someone with malevolent intentions affected the string that eval()
would use, you could run malicious code on the user’s machine with the permission of your service.
So, yes, eval()
IS pretty cool. But taking into account the decrease in performance and the security risks, it is better to avoid it completely. Even by using Strict Mode, which solves some of the problems that eval()
causes, most programmers agree that this function is bad practice and should be avoided.
Try…Catch
In our first tutorial, we talked about how JavaScript doesn’t create scopes for block statements. Well, this was only partially true. In fact, even since ES3, JavaScript has had a little “hack” that allows you to simulate block scopes. It is called try/catch
. Why do we need scopes for block statements? Well, let’s look at the following example:
var a = 1;
if (true) {
var a = 2;
console.log(a); //2
}
console.log(a); //2
Thanks to the enclosing curly braces, this code appears to create a variable just for the block statement. But, as you can remember, the compiler binds in-block variable declarations to the scope in which the block statement exists. And so: in compilation time, the second variable declaration would be ignored; and in execution, both console.log(a)
s would print 2
, because this number is assigned after the 1
.
The first trick that we are going to learn to simulate block scopes uses the try/catch
structure, which by the way we are going to explore in depth in the tutorial about error handling. Since this is an old practice (starting with ES6, JavaScript offers a much better solution), we are just going to learn it for the sake of JavaScript mastery.
So, why do mean when we say that try/catch
uses block scope? We can understand this concept with the following example:
var a;
try {
a();
}
catch(err) {
console.log(err); //TypeError: a is not a function
}
console.log(err); //ReferenceError: err is not defined
Here, we define a variable a
, and then we try to run it as a function inside of the try
statement. Because it is not a variable, JavaScript catches a TypeError
and passes it to the catch
statement so that we can handle it. The first console.log(err)
works well, because the variable err
exists in that block; the second console.log(err)
does not work because the variable err
does not exist outside of the block statement. Voilá! We have created a block scope!
How would we use it in a more flexible way?
var a = 1;
try {
throw 2;
}
catch(b) {
console.log(a + b); //3
}
console.log(a + b); //ReferenceError: b is not defined
Here we have a variable a
with a 1
assigned to it. Then, inside of the try
statement, we are throwing a 2
that will be caught by the catch
statement as the b
parameter. In catch
’s block scope, we can print the sum of variables a
and b
. By contrast, outside of the block scope, variable b
does not exist, and JavaScript throws a ReferenceError
.
Ok, so this sounds pretty cool, but isn’t there a better way to do it? Does JavaScript offer a better option? Well, yes indeed. And we are going to learn about it in the next section.
Let & Const Keywords
Ok, so, starting from ES6, JavaScript offered a couple of keywords that are kind of cousins of var
. They are also used to declare variables (and constants, as we will see), but they have different properties than the variables created with var
.
The main difference is that variables created with the let
and const
keywords are lexically bound to the enclosing block statement in which they are declared in. And so, they cannot be used outside of the block statement. When the compiler encounters let
or const
, it treats the block statement as if it has its own lexical scope.
Cool, right? Let’s start by working with let
. One of the best uses of block scoping is modifying for
loops to make them block scoped:
for (let i = 0; i < 5; i++) {
console.log(i); //1, 2, 3, 4
}
var i;
console.log(i); //undefined
Usually, when we use for
loops, we are creating dummy variables that are not really that important but that we need to make some computation inside of the loop. The problem arises when we start naming all of these dummy variables with the same name: i, j, k, etc. Because these variables don’t have a name that explains what they do, it is easy to forget that we already used them and use them again.
In the last example, we have a for
loop that uses a let
variable, and so the variable only exists inside of the for
loop. The first console.log(i)
prints 1
, 2
, 3
, and 4
(one number per iteration, of course), and i++
makes sure to sum an extra 1 at the end, so that i == 5
and the for
loop stops.
We declare another variable i
outside of the block statement. If the first i
would have been declared with var
, the second one would have not been declared at all. And so, the second console.log(i)
would have printed 5
. But because the first variable stops existing when the for
loop ends, then the second console.log(i)
prints undefined
, because the second variable i
is a different one from the first variable i
.
Amazing, right?
Implicit vs. Explicit
It is a good practice to make our block scopes more explicit by surrounding with curly braces the statements that make use of let and const keywords:
var a = 1;
if (true) {
console.log(a);
{
let b = 2;
console.log(b);
}
console.log(b);
}
Even inside the if
clause, the second console.log(b)
does not have access to the b
variable, because b
was created inside a second pair of curly braces. This is useful for two reasons:
- It enforces the Principle of Least Privilege: We are creating more restricted snippets of code that really ONLY have available what is useful to them.
- We facilitate the refactoring of code: It is easier to avoid mistakes while refactoring if block statements are explicit.
We can even make block scopes more explicit by adding a little comment before the block statement:
var a = 1;
if (true) {
console.log(a);
/*let*/{
let b = 2;
console.log(b);
}
console.log(b);
}
Memory Recycling
Enclosing let
and const
variables into smaller and smaller snippets of code is also very useful for memory recycling. Let’s say we have this code:
var a = 1;
var b = 2;
var bigArray = ["This is a very, very big array"]
//Some code that makes computations with the array
console.log(a); //1
console.log(b); //2
console.log(a + b); //3
console.log(a - b); //-1
After doing the computations that need the very big array, we continue with some other processes that need variables a
and b
, but don’t need bigArray
. But JavaScript has to keep bigArray
in memory “just in case”, for it is possible that one of the console.log()
statements could use bigArray
for some reason.
JavaScript doesn’t know that bigArray
won’t be used after line 5, but YOU do. And so, you can help JavaScript by rewriting this code like this:
var a = 1;
var b = 2;
/*let*/{
let bigArray = ["This is a very, very big array"]
//Some code that makes computations with the array
}
console.log(a); //1
console.log(b); //2
console.log(a + b); //3
console.log(a - b); //-1
In this way, bigArray
only exists inside of the curly braces, and after the computations that use it are done, it can be wiped off memory to recycle computational resources.
The const keyword
Ok, so, we’ve talked about let
. But what about const
?
const
also creates variables in a block scope, but it creates what are called constants. A constant is not really a variable. A variable should have the property that its value can “vary”, right? Well, a constant is an identifier with a value assigned to it, but the value that we assign to a constant CANNOT be changed afterwards.
A constant is assigned a value when declared, and that value remains intact throughout all the block statement. If we try to reassign it, we get an error:
if (true) {
/*const*/{
const a = 1;
console.log(a); //1
a = 2; //TypeError: Assignment to constant variable
}
}
Just as it can’t be reassigned, a constant cannot be redeclared:
if (true) {
/*const*/{
const a = 1;
const a = 2; //SyntaxError: Identifier 'a' has already been declared
}
}
In this regard, let
variables are the same. They too cannot be redeclared, lest we want to throw a SyntaxError
(which we don’t want, of course):
if (true) {
/*let*/{
let a = 1;
let a = 2; //SyntaxError: Identifier 'a' has already been declared
}
}
Another interesting things about constants is that they cannot be reassigned, BUT that doesn’t mean that their content is immutable. For example:
const a = [1, 2, 3];
a[0] = 4;
console.log(a[0]); //4
a = "Reassignment"; //TypeError: Assignment to constant variable
As you can imagine, one rule about constants is that they MUST be assigned at the same time that they are declared:
const a = 1;
console.log(a); //1
const b; //SyntaxError: Missing initializer in const declaration
b = 2;
console.log(b);
Hoisting with let/const
There is also a funny behavior with let/const
regarding hoisting. var
variable declarations are hoisted in compilation phase, and they are assigned an undefined
value when declared. This behavior is what allows this:
var a = true
while (a) {
console.log(b); //undefined
var b = 1;
a = false;
}
console.log(b)
prints undefined
because the variable declaration was hoisted to the top and assigned a undefined
value.
By contrast, let
and const
declarations cannot be accessed before the actual declaration. According to the ECMAScript Language Specification:
let and const declarations define variables that are scoped to the running execution context’s LexicalEnvironment. The variables are created when their containing LexicalEnvironment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.
So, let
variables (the ones that may be assigned after declaration) are assigned an undefined
value, but until execution. And so:
let a;
console.log(a); //undefined
console.log(b); //ReferenceError: b is not defined
const b = "Not hoisted"
Transpilers
One last thing: Even though ES6 was launched in June 2015, not all browsers are completely compatible with it. Specifically, Internet Explorer 11 (their last version) is only 11% compatible with ES6. So, what can we do? Do we avoid using ES6 because we could encounter some incompatibilities in the user’s browser? Not at all. That’s why transpilers exist.
A transpiler converts code in a programming language to code in a different programming language. There are some transpilers for JavaScript that convert ES6 to ES5, or even to ES3. So, if your script finds out that the user is running your code in a non-compatible browser, it uses the transpiler to convert ES6 into ES5 or ES3 and allow the user to read your webpage or use your extension.
We are not going to explore transpilers deeply, but if you want to use them, you can start by exploring Babel, one of the most used and best maintained.
Conclusion
And so: eval()
modifies lexical scopes to run the code that was passed to it as a string. But it is not advised to use it because it can lead to performance and security problems.
try/catch
structures are viable options; so viable that Google’s Traceur, a transpiler for ES6 similar to Babel, used at first try/catch
structures to convert ES6’s block scopes into ES5 compatible code. The problem is that this option is a little bit outdated.
It is not very practical to use try/catch
because we now have let
and const
declarations, which declare variables and constants in a block scope, thus allowing us to use _if_
and _switch_
conditions and _for_
and _while_
loops with their own lexical scope.
Always remember to add a transpiler, or your script could be incompatible with the user’s browser!
In the next tutorial, we will finish our exploration of scopes with two very advanced uses of functions (closures and IIFEs), that take advantage of scopes to do fascinating things. Read on and continue your journey to become a JavaScript Grand Master!