JavaScript Variable Scope
by Nicklas EnvallIn essence, scope is about the accessibility of declared identifiers, like variables or functions. For example, the scope of a variable is where it's visible, in our program, in other words, where we can use or see it.
The scope has a set of rules which determines how and where in our program, we can access our variables. Knowing the scope rules for your programming language is therefore important. Understanding what scope is and how it works will make you more comfortable with variables when writing or reading programs.
Types of scope in Javascript
There are two main scopes, which are global and local scope. Creating a local scope entails creating a new scope in the global scope. We can create a local scope in two different ways, the first being function scope, and now after ES6, we have block scope.
Now we will look at the following scopes:
- Global scope
- Function scope
- Block scope
- Nested scope
- Lexical scope
Then at the end, we will look at hoisting and briefly at closure.
1. Global Scope
The global scope contains all scopes. Global variables are accessible in all scopes. Variables are global if they are declared outside any function or block scope.
// Global scope function fn() { // Local scope }
var a = "hello"; // global variable function fn() { console.log(a); // "hello" } console.log(a); // "hello"
As we can see, we can access the variable a
inside the function's scope. In the next example, we see that we cannot access b
in the global scope.
function fn() { var b = "greetings"; // local variable console.log(b) // "greetings" } console.log(b) // ReferenceError: b is not defined
2. Function Scope
Functions have their own scope, as we just saw. Variables that we declare inside a function are only visible within that function. Variables that are declared in a function are known as local variables.
For example, see how we can access the variables in each scope:
function printX() { var x = 2; console.log(x); } printX(); // 2 console.log(x) // ReferenceError: x is not defined
Just remember to declare the variable in the function, because undeclared variables become global or change an already declared global variable, which can make unexpected result occur:
function printX() { x = 2; console.log(x); } printX(); // 2 console.log(x) // 2
Let's say you, for some reason, just wanted to create a scope and you knew you only would use the function once. Then, you could call it straight away. This is known as IIFE (immediately invoked function expression). Here we create a named function expression:
var name = "Steve"; (function hello() { var name = "Jessica" console.log("Hello " + name); // Hello Jessica })(); console.log("Hello " + name); // Hello Steve
See how we use parentheses and then call it after })
. We achieve the same thing with an anonymous function expression:
(function() { var name = "Jessica" console.log("Hello " + name); // Hello Jessica })();
You can also replace })()
with }())
if you prefer, it does the same thing. I will not cover why you would want to use a named or anonymous function expression. But there are arguments, for both of them, I suggest looking them up if you are interested.
3. Block Scope
Block Scope is generally the area between two curly braces {...}
. For example, for and while loops or if statements.
Creating functions each time we want a scope might be not appropriate. Luckily with ES6, we can utilize the keywords, let
and const
. These can be used to declare variables inside a curly braces {}
scope.
if (true) { let personName = "Bill"; console.log(personName); // Bill } console.log(personName); // ReferenceError: personName is not defined
This does not work with var
, let me prove it:
if (true) { var personName = "Bill"; console.log(personName); // Bill } console.log(personName); // Bill
As you see, the variable personName
became global. If you just want a create block scope, you could also:
{ let number = 10; console.log(number); // 10 } console.log(number); // ReferenceError: number is not defined
4. Nested Scope
Now, let's look more deeply at how nested scopes work.
Scopes can be nested, which means that if a variable cannot be found in scope, the engine will check the first outer scope and then do this repeatedly. It stops when it either finds it or reaches the outermost scope.
What will happen when we call greetB()
with the code below?
var b = "Bob"; function greetB() { var a = "Hello "; console.log(a + b); } greetB();
It will console.log, Hello Bob
. This is because when the function is called, the engine asks the scope of greetB
if it has the variable b
, the scope replies no, so the engine goes up one level and asks the global scope, which says yes.
But, what would happen if we declared b
inside the function's scope?
var b = "Bob"; function greetB() { var a = "Hello "; var b = "Robert"; console.log(a + b); } greetB();
The answer is that it will console.log, Hello Robert
. This is because this time when the engine asked the scope for variable b
, the scope replied yes I have it in the scope of greetB
, so the engine stopped searching for it because it got it right away.
The example below might seem complex, but in reality, it just simply follows the steps we just covered. There are three scopes in this example, the global, outer()
, and inner()
:
var a = 1; function outer() { var b = 2; function inner() { var c = 3; console.log(a, b, c); // 1 2 3 } console.log(a, b); // 1 2 console.log(c); // ReferenceError: c is not defined } console.log(a); // 1 console.log(b); // ReferenceError: b is not defined console.log(c); // ReferenceError: c is not defined
5. Lexical Scope
Lexical Scope means that during compile time the scope of a variable in Javascript is decided. So, lexical scoping entails looking directly at the source code to decide the scope of a variable. In other words, a variable's scope can be determined statically, before execution. A contrast to lexical scope is dynamic scope.
There are exceptions to this if you use eval
or with
, but you really should not be using them anyway. If you think it sounds strange that Javascript compiles, then please read the article regarding if Javascript is interpreted or compiled.
Hoisting
Hoisting means that declarations of function variables are hoisted (moved) to the top of their scope. It's important to know that the assignment still stays on the same line. This means that declarations occur before assignments.
Think of the following code:
console.log(a); var a = "hello";
What will the output be? Will it output "hello"
, ReferenceError
or undefined
? The correct answer is undefined
, I recommend trying it out yourself if you don't believe me.
This is because the compiler outputs it like this:
var a; console.log(a); a = "hello";
What about a function? If hoisting works as it says it does then wherever we put a function, it'll be hoisted to the top and defined.
hello(); function hello() { console.log("Hello"); }
This results in it actually printing out "Hello"
. If you do not want this behaviour you can assign an anonymous (or named) function expression to a variable instead:
hello(); var hello = function () { console.log("Hello"); }
This will instead result in TypeError: hello is not a function
.
Closure
Closure is when a function has access to the context it was declared in even though its executed outside its own lexical scope. This means that, if you understand Lexical Scope you'll more easily understand Closure.
Let's try it out with a simple example:
function makeLogger() { var a = 2; function log() { console.log(a); } return log; } var log = makeLogger(); log(); // 2
We could also do something like this:
function makeAdder(a) { return function add(x) { console.log(a + x); } } var add = makeAdder(2); add(1); // 3 add(2); // 4
Here we can clearly see that we execute the add()
function outside of its declared lexical scope, yet it can still access it.