It's common for Javascript developers at all levels to deal with scope and its associated implications. In this post we'll cover the basics of Javascript scope and some of the issues commonly encountered.
Scope, in general, refers to how the browser's javascript engine looks up identifier names at run time to in order to set how they will be looked up during execution. That definition implies that there is a lexing phase of the engine which is done prior to executing.
Javascript has two lexical scopes: global and function level. With ES6 there is also block level scoping; but more on that later. These lexical scopes are also nested. For instance:
Here we have three separate lexical scopes. The default global scope which contains declarations for a
and foo
, the lexical scope declared within the foo()
function which contains x
, b
and bar
, and the lexical scope declared within bar()
which contains y
and c
.
Notice that outside of a function, the default scope of declared variables is the global scope (window
in the browser). Also, that scopes can be nested. Nested scopes have access to the scope's they are declared in, which has two important implications:
- nested scopes can access variables declared in their parent scopes (there is a scope lookup chain)
- nested scopes can shadow variables declared in their parent scope.
The first point allows closures, meaning we can define a function which will retain access to it's enclosing scope, even outside of that enclosing scope.
var times = function(n) {
return function(x) {
return x * n;
};
}
var times2 = times(2);
console.log(times2(4));
// => 8
Here, the function returned by times allows us to create functions that multiply their argument by a given value, n
. The function returned retains a reference to n
, even though the lexical scope of that function is not accessible outside of the actual function. n
, in effect, becomes a free variable that is only accessible to the function defined and returned within the scope in which n
was declared.
Scope lookup during the lexical phase also stops once it finds the first match. This means you can shadow a variable further up the scope chain.
var a = 4;
function foo(x) {
var a = x; // shadows parent 'a' declaration
console.log(a);
}
console.log(foo(6)); // => 6
console.log(a); // => 4
Here, the locally declared a
shadows the a
declared in the parent, global scope. Without the var
keyword, we would have been reassigning to the variable a
in the parent scope.
var a = 4;
function foo(x) {
a = x; // woops!
console.log(a);
}
console.log(foo(6)); // => 6
console.log(a); // => 6
Hoisting also plays a factor with Javascript's scoping. In Javascript, var
and function(){}
declarations are hoisted to the top of the current scope; and hence, those identifiers are available to any code in that scope.
(function(){
console.log(inc(4)); // 5
console.log("a =",a); // a = undefined?
var a = 1;
function inc(n){ return ++n; }
})();
Uh, oh. So we can clearly access inc()
which was hoisted and it looks like we can use a
without a Reference Error; but, a
is undefined?
Javascript only hoists the declaration and not the initialized value. The value will still end up being assigned at the same spot you have the original declaration. The above code actually gets modified and works as follows:
(function(){
var a;
function inc(n){ return ++n; }
console.log(inc(4)); // 5
console.log("a = ", a); // a = undefined
a = 4; // Ah-ha!
})();
This is the same for variables assigned function expressions as well. Because function expressions are just values in an assignment statement.
(function(){
console.log(inc(4)); // 5
console.log(dec(4)); // ReferenceError: dec undefined
var dec = function(n) { return --n; }
function inc(n){ return ++n; }
})();
With the new ES6 specification, lexical scope has been altered in a handful of ways, primarily with the new let
and const
declarations and with the shorter =>
function syntax.
In ES5, as discussed above, if you wanted to declare a lexically scoped block of code, you could do so by creating a closure, typically with an immediately invoked function expression or IIFE. ES5 was essentially lexically scoped to functions.
var greet = (function() {
var greeting = "Hello!";
return function(name) {
console.log(greeting + ", " + name + "!");
};
})();
greet("Cap'n Tight Pants");
// => "Hello, Cap'n Tight Pants!"
With ES6's let
declaration we can now have lexically scope blocks of code, without using functions or IIFEs.
var a = 1, b = 2, c = 3;
{
let a = 4, b = 5, c = 6;
console.log("a,b,c = ", a, b, c);
}
console.log("a,b,c = ", a, b , c);
// => a,b,c = 4 5 6
// => a,b,c = 1 2 3
Something you might encounter with let
declarations also, is that according to the ES6 spec, let
declarations don't hoist like var
and function
declarations.
With let
declarations, you'll get a reference error if you try to access a let
declared variable before its declaration in the code (according to the ES6 spec).
console.log("a = ", a);
console.log("b = ", b);
var a = 4;
let b = 6;
// => a = undefined
// => b = ReferenceError!
However, if you are using Traceur or Babel(6to5), this will not be the case, as transpiling this dead zone behavior would require much more work and nearly reimplementing the Javascript run-time to handle those cases.
const
is the other new, block scope declaration type, which, as you would suspect creates constant variables.
const a = 4;
console.log("a = ", a);
a = 3; // TypeError! a is readonly
A const
declared variable must be initialized with a value when it is declared. Declaring const a;
and trying to assign a value to it later on will throw an error. Also, const
works by limiting the assignment to a variable, not by freezing a variable's value. So, if you assign a reference type to it, you can still modify the underlying properties of that reference type:
const a = { name: 'Mal', ship: 'Serenity' };
a = 4; // TypeError
a.name = 'Wash' // no problem!
const b = [1,2,3];
b.unshift(0); // b = [0,1,2,3]
const
declared reference variables hold a constant reference to that variable, not a constant value - so the underlying reference assigned to a const can change.
We've already encountered the idea that Javascript var
and function
declarations are lexically scoped to the enclosing function or scope they are contained in. But, what about the this
keyword inside of functions? What does it reference?
Most familiar is the use of this
in functions used as constructors with the new
keyword. When using new
, this
refers to the eventual object that will be returned from the function.
function Tribble(color) {
this.color = color;
}
var my_tribble = new Tribble("brown");
my_tribble.color; // "brown"
When calling an method on an object, this
refers to the object that is the context that that function is being called with; in most cases, this is the object prototype or instance the function is assigned to.
var tardis = {
where: "Gallifrey",
go: function() {
console.log("Off to " + this.where + ", Allons-y!");
}
};
tardis.go(); // "Off to Gallifrey, Allons-y!"
When calling tardis.go()
, the this
references the current context of that function, which is the tardis
object. But, even without a context object, this
still refers to the functions context, which turns out to be window
for browsers.
var where = "Gallifrey";
function go() {
console.log("Off to " + this.where + ", Allons-y!");
}
go(); // "Off to Gallifrey, Allons-y!"
this
, it seems, is fairly flexible and potentially dubious in Javascript depending on what you're trying to do.
ES6 specifies a short-hand syntax for declarations for functions using the fat arrow (=>
) syntax. However, =>
functions bind this
to their enclosing scope, unlike regular functions which create a new scope entirely.
var even = (x) => x % 2 == 0;
console.log(even(2)); // true
console.log(even(3)); // false
var a = 4,
list = [1,2,3].map((n) => n + a);
// list = [5,6,7]
The left side of the =>
declares the function's arguments and the right side declares the return value, or a block of code as the function body.
let evens = [1,2,3,4,5,6].filter((n) => {
if (even(n)) {
return n;
}
});
console.log(evens); // [2,4,6]
There is much more to Javascript scoping than this (ha, get it?); and I would suggest you take a look at Kyle Simpson's You Don't Know JS: Scope & Closures and the ES6 spec related to scoping of let
and const
as well for a more detailed coverage and understanding.
If you are using Babel or Traceur, be sure and reference their documentation for any missing parts or inconsistencies from the ES6 specification as well, which will make it easier when you transition for transpilers to native ES6 in the browser.