Previous: Not the Inheritance You Expected
Hoist Yourself Up
The first thing a lot of people hear about JavaScript is that it doesn’t matter what order we declare and use variables in, because the declaration will get hoisted to the top of the file or function. Let’s look at an example:
var z = 'potato';
function doSomething() {
console.log('foo');
var x = 'bar';
console.log(x);
}
var y = 'lettuce';
console.log(z + 'with' + y);
This gets hoisted to:
var z;
var y;
z = 'potato';
function doSomething() {
var x;
console.log('foo');
x = 'bar';
console.log(x);
}
y = 'lettuce';
console.log(z + 'with' + y);
For this reason, it was a common best practice back in ES5 and earlier to put all of the variable declarations at the top of the function they were declared in (or the top of the file if they were declared globally). That way, it would be more apparent how it would be processed after hoisting.
Let’s Scope This Out
According to the Oxford English Dictionary, scope is, “The extent of the area or subject matter that something deals with or to which it is relevant.” That is exactly what it means in code, so a variable’s scope is the context in which that variable is relevant (valid/accessible).
We cannot access a variable outside of its scope:
function doSomething() {
var x = 5;
console.log(x);
}
console.log(x); // This won't work!
In ES5 and earlier, all JavaScript variables had function scope. This can lead to some unexpected situations such as this example:
function doSomething() {
for (var i = 0; i < 3; i++) {
console.log(i);
}
console.log('Final value of i:', i);
}
doSomething();
> 0
1
2
Final value of i: 3
Even though i
was declared inside the for loop, it is accessible outside of the loop. In this example it doesn’t seem particularly dangerous, but consider this example:
// Adds all numbers that are either a multiple of seven
// or their digits add to seven
function getTheSum(numbers) {
var sum = 0;
for (var i = 0; i < numbers.length; i++) {
var n = numbers[i];
var shouldAdd = false;
if (n % 7 === 0) {
shouldAdd = true;
} else {
var digits = n.toString();
var sum = 0;
for (var j = 0; j < digits.length; j++) {
sum += parseInt(digits.charAt(j));
}
if (sum === 7) {
shouldAdd = true;
}
}
if (shouldAdd) {
sum += n;
}
}
return sum;
}
console.log(getTheSum([22, 7, 4, 16, 8, 49, 34]));
This code should add 7 + 16 + 49 + 34
and return 106
, but instead it returns 41
.
Do you see the flaw in this program? The problem is that sum
is defined inside and outside of the loop. JavaScript hoists the variable definition to the top of the function getTheSum
, which means that the two var sum
declarations are in the same scope.
Here is what this function looks like after hoisting:
// Adds all numbers that are either a multiple of seven
// or their digits add to seven
function getTheSum(numbers) {
var sum;
var i;
var n;
var shouldAdd;
var digits;
var j;
sum = 0;
for (i = 0; i < numbers.length; i++) {
n = numbers[i];
shouldAdd = false;
if (n % 7 === 0) {
shouldAdd = true;
} else {
digits = n.toString();
sum = 0; // <---------------
for (j = 0; j < digits.length; j++) {
sum += parseInt(digits.charAt(j));
}
if (sum === 7) {
shouldAdd = true;
}
}
if (shouldAdd) {
sum += n;
}
}
return sum;
}
console.log(getTheSum([22, 7, 4, 16, 8, 49, 34]));
After hoisting, the issue is much more apparent. When we set sum
to 0
in the inner block, we’re using the same sum
defined above.
This means that while processing the last number, 34
, the function resets sum
to 0
, then adds the digits of 34
(which is 7
), and then adds 34
to sum
to get 41
.
ES2015 solves this by introducing two new keywords, let
and const
. Variables declared using these keywords have block scope. Essentially this means that the variables are defined only within the set of curly braces that the definition is contained within (the parentheses of a for
loop are treated like a block that contains the actual block of the loop).
Let’s try our first example using the let
keyword instead.
function doSomething() {
for (let i = 0; i < 3; i++) {
console.log(i);
}
console.log('Final value of i:', i);
}
doSomething();
> 0
1
2
x Uncaught ReferenceError: i is not defined
at <stack trace>
Now the scope of the variable is correctly limited to the block in which it is used. If we replace var
with let
in our getTheSum()
example, it returns 106
as expected.
If you’ve uses other languages with constants before, you can probably guess that the difference between let
and const
is that const
defines a constant and let
defines a variable. However, you have to be careful because “constant” means something different in different languages.
In languages like C and Swift, constants refer to values that deeply cannot be modified (are immutable). Once an object is declared as a constant, the value must be copied elsewhere before it or any of its properties can be modified.
In JavaScript, however, const
just means we can’t reassign the variable, so the “constantness” (immutability) is shallow.
This means that the following code is valid:
const martin = {
name: "Martin Banks",
job: "Data Entry Clerk",
location: "London, England, UK",
time: new Date("April 15, 2012 11:17:54")
};
martin.time = new Date("May 1, 1150 10:30:00");
But this is not:
const martin = {
name: "Martin Banks",
job: "Data Entry Clerk",
location: "London, England, UK",
time: new Date("April 15, 2012 11:17:54")
};
martin = {
name: "Phillip McCall",
job: "Programmer",
location: "Murray Hill, New Jersey, USA",
time: new Date("June 17, 1984 15:31:45")
};
When writing modern JavaScript, you should use const
by default, and change it to let
if and when you decide to reassign the variable. This can be made easier by using an IDE that will warn you if a const
is reassigned or if a let
isn’t. Never use var
, because it leads to confusion.