In an earlier blog post, I discussed JavaScript functions at length. If you found yourself on Team Scar before, hopefully that post made you start thinking about switching sides; conversely, if you already liked JS, I hope I didn’t ruin it for you! I mentioned that I would cover JavaScript scope in my next post, so let’s get on with it!
If you learned C/C++, then went on another C-style language (i.e. Java or C#), then you are very much like me. Scope in those languages is created by { }. In JS, relying on { } to define your scope is just pointless. Functions create scope. Only functions can create scope and anything outside of a function exists as a part of the global object. Most of the frustration towards JS is probably due to developers misunderstanding it. Sure, the language does have some design flaws, but what language doesn’t?
We as developers tend to work around quirks and, over time, we stop noticing. Since Pascal was the first language I ever used, I thought it didn’t make much sense to use == for checking equality. (Pascal uses the = operator to check for equality and x:= value for assignment which seems more elegant.) Hopefully learning a bit about how the language works may make developing in it a bit more pleasant!
SCOPE
var king = "Mufasa" function longLiveTheKing () { king = "Scar" } console.log(king) //Scar
Right away, you can see JS offers no protection for the variables you create outside of a function. JS is lexically scoped, meaning inner functions have access to variables defined in outer functions, (I like to think of it as child functions having access to variables defined by their parents.) Keep in mind that all your code is scoped to the global object by default; therefore, all variables created in this scope can be accessed by any function. It’s also important to know this relationship between child and parent is one-way; a parent does not have access to its children’s variables.
var king = "Mufasa" function parent () { var my_name = king console.log("My name is " + my_name + " and these are my descendants...") function child () { var parent_name = my_name var my_name2 = "Simba" console.log("My parent's name is " + parent_name + " and my name is " + my_name2) function grand_Child () { parent_name = my_name2 var my_name3 = "Kiara" console.log("My parent's name is " + parent_name + " and my name is " + my_name3) } grand_Child() } child() } parent()
In the above code snippet, I’ve nested the function child inside the parent function and the grand_child function inside the child function creating the scope hierarchy: global -> parent -> child -> grand_child. The innermost function, grand_child (the rightmost in the hierarchy), has access to all variables and functions its defined within (all the functions left of grand_child in the hierarchy). The relationship of visibility is only one-way however. The parent function only knows about the variables and functions it defines, not those which its children define. The parent knows about the variables and functions it defines; however, it does not know about the variables and functions the child creates.
This lexical scoping can be visually seen and is actually quite helpful: you can determine scope by looking at the ‘shape’ of the code (assuming it’s well indented of course!). Here is a trickier example, but if you understand the basic mechanics of scope, then this should be very straightforward.
var king = "Mufasa" function parent () { var my_name = king console.log("My name is " + my_name + " and these are my descendants...") function child () { var parent_name = my_name var my_name2 = "Simba" console.log("My parent's name is " + parent_name + " and my name is " + my_name2) function grand_Child () { parent_name = my_name2 var my_name3 = "Kiara" console.log("My parent's name is " + parent_name + " and my name is " + my_name3) } grand_Child() } function brother () { var sibling_name = my_name var my_name2 = "Scar" console.log("My name is " + my_name2 + ", " + sibling_name + " is my sibling and these are my descendants...") function child () { var parent_name = my_name2 var my_name3 = "Kovu" console.log("My parent's name is " + parent_name + " and my name is " + my_name3) } child() } brother() child() } parent()
In the above code snippet, which functions does the function ‘brother’ have access to?
If you came up with brother -> child, parent, and itself, then you would be correct! Since function scope is created within a function, calling child within brother will always call the child function within its scope or its local scope. Also worth noting, the brother function does not have access to any of the functions defined within parent -> child and would not have access even if we renamed brother -> child to brother -> childOfScar.
HOISTING
var color = "Green" function changeColor () { if (color === undefined) { var color = "Orange" } console.log(color) } changeColor() console.log(color)
If you already understand hoisting, then you should know that the code snippet writes the color green to the console twice. If you didn’t get that, feel free to continue reading.
When we define variables and functions in JS, something known as hoisting occurs. This process will hoist variable definitions and function declarations to the top of the scope in which they are defined. Assuming we have variables/functions that have global scope, no matter where they are defined, they will be brought up to the top of the code. It is important to keep in mind that the assignment of a variable takes place on the line the variable was initially defined.
In our example, we declare the variable color and the function changeColor. When the code is run, both our function and variable declarations get hoisted to the top. It ends up looking like this:
var color function changeColor () { if (color === undefined) { color = "Orange" } console.log(color) } color = "Green" changeColor() console.log(color)
Remember, variable and function declarations get hoisted, not assignments and function expressions — see my post on JS functions to learn the difference between function declarations and expressions. As I mentioned earlier, the assignment of a variable takes place on the line where it occurs; all variables are undefined until that happens. Let’s try a slightly modified example. (Tricky!)
var color = "Green" var changeColor = function () { if (color === undefined) { var color = "Orange" } console.log(color) } changeColor() console.log(color)
Can you figure out what this snippet outputs?
If you determined the output to be orange followed by green, then you are correct!
In this code snippet, I changed my function declaration into a function expression; an anonymous function is now assigned to the variable changeColor. This example shows a very important property of function expressions: the function body is not hoisted; it is assigned on the line in which it occurs just like any other variable. Because of hoisting, it is a good practice to declare all variables at the top of the scope they need to be accessible in.
CONTROL STRUCTURES
Variables defined in if…else statements and loops will not behave like they do in other languages such as C++ or Java. Expecting these control structures to behave like other languages can be the cause of many headaches. You must always keep in mind that defining a variable inside a control structure makes it accessible to everything defined in the same scope as the control structure — the function the control structure is defined within.
Hopefully, taking the time to understand JS will allow you to appreciate it more. Learning scope rules is only the beginning. Following best practices should help you avoid the most common gotchas in JS and should make developing in JS less of a pain. Maybe you will even join Team Simba’s ever-growing pride!