JavaScript is an easy language to learn. You can grab a snippet from the internet and pop it into your HTML page, and you’ve started on your journey. One reason why it’s easy to learn is that in some respects, it’s not as strict as it should be. It lets you do things that it possibly shouldn’t, which leads to bad habits. In this section, we’ll take a look at some of these bad habits and show you how to turn them into good habits.
The first step is looking at variables, scope, and functions, which are all closely tied together. JavaScript has three types of scope: global, function (using the var keyword), and lexical (using let or const keywords). JavaScript also has scope inheritance. If you declare a variable in global scope, it’s accessible by everything; if you declare a variable with var inside a function, it’s accessible only to that function and everything inside it; if you declare a variable with let or const in a block, it’s accessible inside the braces and everything inside that block, but unlike var, access doesn’t bleed through to the surrounding function block.
Modern practice tends to frown on using the var keyword, which will eventually be deprecated. var comes with a lot of baggage, and if you’re coming from other languages, its scoping can be difficult to work with and can trip up even the most experienced developer. We’ll discuss it here, though, because a lot of JavaScript has been built with var.
With ES2015, the language specification introduced the let and const keywords, which are lexically (block) scoped. These keywords have greater similarity with other variable-definition schemes. The difference is explained in more detail in the following sections.
Start with a simple example in which scope is used incorrectly.
const firstname = 'Simon'; 1 const addSurname = function () { const surname = 'Holmes'; 2 console.log(firstname + ' ' + surname); 3 }; addSurname(); console.log(firstname + ' ' + surname); 4
This piece of code throws an error because it’s trying to use the variable surname in the global scope, but it was defined in the local scope of the function addSurname(). A good way to visualize the concept of scope is to draw some nested circles. In figure D.1, the outer circle depicts the global scope; the middle circle depicts the function scope; and the inner circle depicts lexical scope. You can see that the global scope has access to the variable firstname and that the local scope of the function addSurname() has access to the global variable firstname and the local variable surname. In this case, lexical scope and function scope overlap.
If you want the global scope to output the full name while keeping the surname private in the local scope, you need a way of pushing the value into the global scope. In terms of scope circles, you’re aiming for what you see in figure D.2. You want a new variable, fullname, that you can use in both global and local scopes.
One way you could do it—and we’ll warn you now that it’s bad practice—is to define a variable against the global scope from inside the local scope. In the browser, the global scope is the object window; in Node.js, it’s global. Sticking with browser examples for now, the following listing shows how this would look if you updated the code to use the fullname variable.
const firstname = 'Simon'; const addSurname = function () { const surname = 'Holmes'; window.fullname = firstname + ' ' + surname; 1 console.log(fullname); }; addSurname(); console.log(fullname); 2
This approach allows you to add a variable to the global scope from inside a local scope, but it’s not ideal. The problems are twofold. First, if anything goes wrong with the addSurname() function and the variable isn’t defined, when the global scope tries to use it, you’ll get an error thrown. The second problem becomes obvious when your code grows. Suppose that you have dozens of functions adding things to different scopes. How do you keep track of them? How do you test them? How do you explain to someone else what’s going on? The answer to all these questions is with great difficulty.
If declaring the global variable in the local scope is wrong, what’s the right way? The rule of thumb is always declare variables in the scope in which they belong. If you need a global variable, you should define it in the global scope, as in the following listing.
var firstname = 'Simon', fullname; 1 var addSurname = function () { var surname = 'Holmes'; window.fullname = firstname + ' ' + surname; console.log(fullname); }; addSurname(); console.log(fullname);
Here, it’s obvious that the global scope now contains the variable fullname, which makes the code easier to read when you come back to it.
You may have noticed that from within the function, the code still references the global variable by using the fully qualified window.fullname. It’s best practice to do this whenever you reference a global variable from a local scope. Again, this practice makes your code easier to come back to and debug, because you can explicitly see which variable is being referenced. The code should look like the following listing.
var firstname = 'Simon', fullname; var addSurname = function () { var surname = 'Holmes'; window.fullname = window.firstname + ' ' + surname; 1 console.log(window.fullname); 1 }; addSurname(); console.log(fullname);
This approach might add a few more characters to your code, but it makes it obvious which variable you’re referencing and where it came from. There’s another reason for this approach, particularly when assigning a value to a variable.
JavaScript lets you declare a variable without using var, which is a bad thing indeed. Worse, if you declare a variable without using var, JavaScript creates the variable in the global scope, as shown in the following listing.
var firstname = 'Simon'; var addSurname = function () { surname = 'Holmes'; 1 fullname = firstname + ' ' + surname; 1 console.log(fullname); }; addSurname(); console.log(firstname + surname); 2 console.log(fullname); 2
We hope that you can see how this could be confusing and is a bad practice. The takeaway is always declare variables in the scope in which they belong, using the var statement.
You’ve probably heard that with JavaScript, you should always declare your variables at the top. That’s correct, and the reason is because of variable hoisting. With variable hoisting, JavaScript declares all variables at the top anyway without telling you, which can lead to some unexpected results.
The following code listing shows how variable hoisting might show itself. In the addSurname() function, you want to use the global value of firstname and later declare a local scope value.
var firstname = 'Simon'; var addSurname = function () { var surname = 'Holmes'; var fullname = firstname + ' ' + surname; 1 var firstname = 'David'; console.log(fullname); 2 }; addSurname();
Why is the output wrong? JavaScript “hoists” all variable declarations to the top of their scope. You see the code in listing D.8, but JavaScript sees the code in listing D.9.
var firstname = 'Simon'; var addSurname = function () { var firstname, 1 surname, 1 fullname; 1 surname = 'Holmes'; fullname = firstname + ' ' + surname; 2 firstname = 'David'; console.log(fullname); }; addSurname();
When you see what JavaScript is doing, the bug is a little more obvious. JavaScript has declared the variable firstname at the top of the scope, but it doesn’t have a value to assign to it, so JavaScript leaves the variable undefined when you first try to use it.
You should bear this fact in mind when writing your code. What JavaScript sees should be what you see. If you can see things from the same perspective, you have less room for error and unexpected problems.
Lexical scope is sometimes called block scope. Variables defined between a set of braces are limited to the scope of those braces. Therefore, scoping can be limited to looping and flow logic constructs.
JavaScript defines two keywords that provide lexical scope: let and const. Why two? The functionality of the two is slightly different.
let is a bit like var. It sets up a variable that can be changed in the scope in which it is defined. It differs from var in that its scope is limited as described earlier, and variables declared this way aren’t hoisted. As they’re not hoisted, they’re not tracked by the compiler the same way as var; the compiler leaves them where they are on the first pass, so if you try to reference them before they’re defined, the compiler complains with a ReferenceError.
if (true) { let foo = 1; 1 console.log(foo); 2 foo = 2; 3 console.log(foo); 4 console.log(bar); 5 let bar = 'something'; 6 }
const has the same caveats as let. const differs from let in that variables declared in such a way aren’t allowed to change, either by reassignment or redeclaration; they’re declared to be immutable. const also prevents shadowing—redefining a previously defined outer scoped variable. Suppose you have a variable defined in global scope (with var), and you try to define a variable with const with the same name in an enclosed scope. The compiler will throw an Error. The type of the error returned depends on what you’re trying to do.
var bar = 'defined'; 1 if (true) { const foo = 1; 2 console.log(foo); 3 foo = 2; 4 const bar = 'something else'; 5 }
Because of the clarity afforded by declaring variables with let and const, this method is now the preferred way. Issues of hoisting are no longer a concern, and variables behave in a more conventional way that programmers familiar with other mainstream languages are more comfortable with.
You may have noticed throughout the preceding code snippets that the addSurname() function has been declared as a variable. Again, this is a best practice. First, this is how JavaScript sees it anyway, and second, it makes it clear which scope the function is in.
Although you can declare a function in the format
function addSurname() {}
JavaScript interprets it as follows:
const addSurname = function() {}
As a result, it’s a best practice to define functions as variables.
We’ve talked a lot about using the global scope, but in reality, you should try to limit your use of global variables. Your aim should be to keep the global scope as clean as possible, which becomes important as applications grow. Chances are that you’ll add various third-party libraries and modules. If all these libraries and modules use the same variable names in the global scope, your application will go into meltdown.
Global variables aren’t the “evil” that some people would have you believe, but you must be careful when using them. When you truly need global variables, a good approach is to create a container object in the global scope and put everything there. Do this with the ongoing name example to see how it looks by creating a nameSetup object in the global scope and use this to hold everything else.
const nameSetup = { 1 firstname : 'Simon', fullname : '', addSurname : function () { const surname = 'Holmes'; 2 nameSetup.fullname = nameSetup.firstname + ' ' + surname; 3 console.log(nameSetup.fullname); 3 } }; nameSetup.addSurname(); 3 console.log(nameSetup.fullname); 3
When you code like this, all your variables are held together as properties of an object, keeping the global space nice and neat. Working like this also minimizes the risk of having conflicting global variables. You can add more properties to this object after declaration, and even add new functions. Adding to the preceding code listing, you could have the code shown next.
nameSetup.addInitial = function (initial) { 1 nameSetup.fullname = nameSetup.fullname.replace(" ", " " + initial + " "); }; nameSetup.addInitial('D'); 2 console.log(nameSetup.fullname); 3
Working in this way gives you control of your JavaScript and reduces the chances that your code will give you unpleasant surprises. Remember to declare variables in the appropriate scope and at the correct time, and group them into objects wherever possible.
18.218.234.83