Understanding Function Scopes and Block Scopes in JavaScript
This is originally posted by the author on his blog. This post has been edited for clarity and some parts may appear different from the original post.
Is the following line a valid line of JavaScript code?
{{{}}}
Yes, it is.
These are nested block scopes.
Block scopes
Block scopes are different from function scopes in JavaScript. A function scope is created for every function (and we can nest them, too):
function iHaveScope() {
// local function scope
function iHaveNestedScope() {
// nested local function scope
}
}
We often identify those scopes as local scopes and identify the top-level scope as the global scope. In a browser environment, the global scope is controlled by the window object while in Node.js, it’s controlled by the global object.
It’s hard to completely avoid the global scope, unless you’re coding purely functional style, but you should minimize the use of any global variables because they represent a state and having that defined globally makes it more vulnerable to conflicts and data corruption. You should use an Immediately Invoked Function Expression (IIFE) - when you can — to wrap all your code in a local function scope:
void function() {
// your code here
}()
Block scopes are what you get when you use if statements, for statements, and the like. You can also use them stand-alone with a simple begin-end curly braces {}
, not to be confused with empty object literals.
var a = {} // empty object literal
{ var a } // undefined object in a block scope
if (3 == '3') {
// block scope for the if statement
}
for (var i=0; i<10; i++) {
// block scope for the for statement
}
var
vs. let
The var keyword behaves differently in function scopes and block scopes. A variable declared with var
in a function scope can’t be accessed outside that function scope.
function iHaveScope() {
var secret = 42;
}
secret; // ReferenceError: secret is not defined (in this scope)
A variable declared with var in a block scope is available outside of that block scope.
for (var i=0; i<10; i++) {
// block scope for the for statement
}
console.log(i) // => 10 (why oh why)
The i
variable that we often use in a for loop will continue to exist beyond the scope of that loop, and that does not make sense, really.
Luckily, we now have a different way to declare variables, using let
. Variables declared with let inside a block scope are only accessible inside that scope, making let the ideal solution to the for loop index variable scope problem. If we use let
to declare the i
variable in a for loop, that variable will only be available inside the for loop.
for (let i=0; i<10; i++) {
// block scope for the for statement
}
console.log(i) // ReferenceError: i is not defined (D'oh!)
const
Declaring a variable with const is exactly like let
when it comes to scopes, but creates a constant reference for the variable. We can’t change the value of a constant reference. If we put a primitive value in a constant, that value will be protected from getting changed:
const PI = 3.141592653589793
PI = 42 // SyntaxError: "PI" is read-only
Note that if the constant is an object, we can still change the properties of that object, so be careful about that:
const dessert = { type: 'pie' };
dessert.type = 'pudding'; // Sure thing
console.log(dessert.type) // pudding
We can’t, however, reassign an object declared with const:
const dessert = { type: 'pie' };
dessert = { type: 'cake' }; // SyntaxError: "dessert" is read-only
If we want a completely immutable object, we’ll have to use something else. My favorite library for that is Immutable.js.
Constants are popularly used when importing things from other libraries so that they don’t get changed accidentally. In Node.js, for example:
const _ = require('lodash');
Constants are also great to use when defining functions because we rarely need to update a function after we define it the first time.
Conclusion
In general, I think it’s good to always use const
for your declarations and only switch to let
or var
if you actually need to. With const
, you get the peace of mind that no mutating reassignments are happening on a variable, while with let
/var
, you’ll have to read the code to verify that:
let answer = 42;
// some code ...
// answer MIGHT be 42 here, read the code to be sure.
// ***
// vs.
// ***
const answer = 42;
// some code ...
// answer IS STILL 42 here, no matter what happens above
I hope JavaScript will eventually get more native immutability features baked into the language. While const
does not exactly provide full immutability for all objects, it’s a very good start for primitive values.