Deep JavaScript Foundations, v3
Introduction
Introduction
Link to the course’s code exercises
The instructor talks about the depth that this course gets into. But why dive deep into JavaScript? Consider the following code:
We as developers tend to assume mental models about code and when something goes wrong instead of having a better understanding about the code. The unintuitive-ness of JavaScript does not mean that it was poorly defined. Read the spec docs!
Understanding Your Code
According to the JavaScript spec for the ++
operator:
12.4.4.1 Runtime Semantics: Evaluation UpdateExpression : LeftHandSideExpression **++
- Let
lhs
be the result of evaluating LeftHansSideExpression.- Let
oldValue
be ?ToNumber
(?GetValue(lhs)
).- Let
newValue
be the result of adding the value 1 tooldValue
, using the same rules as for the + operator.- Perform ?
PutValue(lhs, newValue)
.- Return
oldValue
.
The purpose of this course is to help you understand the DNA of JavaScript to better understand when you are doing as a developer.
To translate the above ++ spec into a function, it may look something like this:
Whenever there is a divergence between what your brain thinks is happening and what the computer does, that’s where bugs enter the code.
Course Overview
JavaScript can be divided into 3 pillars:
-
Types
-
Primitive Types
-
Abstract Operations
-
Coercion
-
Equality
-
TypeScript, Flow, etc…
-
-
Scope
-
Nested Scope
-
Hoisting
-
Closure
-
Modules
-
-
Objects (Oriented)
-
this
-
class{}
-
Prototypes
-
OO vs. OLOO
-
It is important to understand all of these pillars of JavaScript to get a better understanding of the language and therefore be more effective in your work.
Types
Primitive Types
“In JavaScript, everything is an object.” - false
This is not an accurate statement. Many things in JavaScript behave like objects, but that does not make them objects. The spec lays it out. The primitive types are: Undefined, Null, Boolean, String, Symbol, Number, and Object.
-
undefined - has one and only one value; undefined
-
string - any value wrapped in quotes (single or double)
-
number - refers to all of JavaScript’s numbers
-
boolean - the values
true
andfalse
-
object -
{}
-
symbol - used mostly to create ‘pseudo’ private-keys
-
undeclared - is this a type? not exactly, but it does have a behavior
-
null - defined as a type, but its behavior is quirky. More details on this later.
-
function - not listed [in the spec] as a type, but functions do have a very specific set of attributes that could make a function considered to be a type. Technically referred to as a sub-type of an object.
-
array - numerically indexed, lengths assigned. Another sub-type to an object.
-
bigint - not in the spec, but likely coming to the spec. When it does land in the spec, it will be a primitive type.
Defining/assigning a type when developing typically indicates that you expect your data to behave in a certain way. You should be defining your data w/ intent and understanding. Most of the above are not objects, so it is not correct to say that everything is an object in JavaScript. Variables don’t have types, values do.
typeof Operator
When we assign a value to a variable, we can use the typeof
operator to determine the type of the value stored in the variable. undefined
is the default value assigned to a variable when a value is not assigned. undefined
does not mean no value, but a value of undefined
. typeof
when applied to a variable storing a function will return ‘function’, which is not a primitive type.
BigInt
Coming soon functionality, does not behave like a standard number primitive type. It will be important to know the difference between a ‘number’ and a ‘bigint’.
Kinds of Emptiness
undefined vs. undeclared vs. uninitialized
These words are not synonymous, in JavaScript they are two entirely different concepts. undeclared
means that the variable does not exist, undefined
means that the variable exists, but has no value. uninitialized
means the variable has never been initialized.
NaN & isNaN
NaN does not mean ‘not a number’, it means ‘invalid number’. Zero is not a number to use when you would like to indicate that there is no value, because zero has value. NaN is basically a number that has no value. NaN with any other mathematically valid number will return NaN. NaN is not equal to NaN. NaN is the only value that is not equal to itself. The isNan
utility coerces values to numbers before checking its value. Number.isNan
does not do any coercion. Using this utility, you can determine with certainty whether a value is actually NaN or not; returns true
or false
. typeof NaN
is a number.
Negative Zero
The negative representation of the value zero. If you stringify -0, it returns as ‘0’. -0 === 0
? true
negative zero is neither less than or greater than zero. Enter Object.is()
which allows you to test for the negative zero. It can also be used to check NaN values. A negative zero could be used to track the direction an element was traveling when it gets to zero. If an element was traveling in a negative direction (towards zero), when it gets to zero, it would be useful to denote which direction it was traveling rather than just zero. Negative zero to the rescue!
Type Check Exercise
Make a Polyfill for Object.is(..)
-
`Object.is(..)1 should take two parameters.
-
It should return
true
if the passed in parameters are exactly the same value (not just===
), orfalse
otherwise. -
For
NaN
testing, you can useNumber.isNan(..)
, but first see if you can find a way to test without usage of any utility? -
For
-0
testing, no built-in utility exists, but here’s a hint:-Infinity
. -
If the parameters are any other values, just test them for strict equality.
-
You cannot use the built-in
Object.is(..)
Polyfill Pattern
Note: Since your JS environment probably already has
Object.is(..)
, to test your polyfill you’ll have to first unconditionally define it (noif
guard), and then add theif
guard when you’re done.
To define a polyfill, it looks like this:
In the (Code Exercises)[https://static.frontendmasters.com/resources/2019-03-07-deep-javascript-v2/deep-js-foundations-v2-exercises.zip], the file types-exercises > object-is > ex.js
you will find the file that has all of the Object.is(..)
tests.
Type Check Exercise Solution
Click to view the solution
Fundamental Objects
In addition to Primitive Values are Fundamental Objects. AKA Built-In Objects or Native Functions. Under the following circumstances, it is recommended to use the new
keyword:
-
Object()
-
Array()
-
Function()
-
Date()
-
RegExp()
-
Error()
And with these, it is not recommended to use the new
keyword:
-
String()
-
Number()
-
Boolean()
They can be used with new
, but you should not use that keyword with them. String(), Number(), and Boolean()
automatically coerce whatever value is passed into them into that Primitive Type, and that behavior is more useful than making an object out of them.
Coercion
Abstract Operations
From ECMAScript documentation:
The ECMAScript language implicitly performs automatic type conversion as needed. To clarify the semantics of certain constructs it is useful to define a set of conversion abstract operations. The conversion abstract operations are polymorphic; they can accept a value of any ECMAScript language type. But no other specification types are used with these operations.
AKA coercion
The first abstract operation we are looking at is ToPrimitive
. This abstract operation takes an optional type
hint. The algorithms in JavaScript are recursive, so if we pass something that cannot be coerced into a Primitive Type, ToPrimitive()
will re-run until it either finds a type that it can set or error out. The way that ToPrimitive
works is, by way of hint, trying either valueOf()
or toString()
the order of which depends on the ‘hint’ you have passed in (number or string).
toString
ToString
takes any value and gives us the representation of that value in string form. One of the corner-cases of ToString
is that is coerces ‘-0’ to ‘0’. Calling ToString
on an array removes the brackets, nulls and undefined(s) get left out. On an object, ToString
will write “[object Object]”. You can overwrite ToString
to behave however you want i.e. JSON.stringify an object.
toNumber
ToNumber
takes any value and gives us the representation of that value in numeric form. One strange behavior is turning empty quotes to the number 0. It strips off leading zeros and can handle hexadecimal values. null
is coerced to ‘0’ and undefined
is ‘NaN’. When ToNumber
is used on an object, the algorithm will eventually hit ‘valueOf()’ and then ‘toString()’ and return NaN
. On an array, empty strings are coerced to 0.
toBoolean
Anytime you have any value that is not a boolean and you need a boolean value, ToBoolean()
will eventually be used. Essentially, there is a lookup table that qualifies values as either Falsy or Truthy. Falsy values are: "", 0, -0, null, NaN, false, undefined
Truthy values are basically any value not listed under ‘falsy values’, some examples: "foo", 23, {a:1}, [1,3], true, function(){..}
. The list of truthy values is quite long.
Cases of Coercion
Template literal strings (available since ES6) coerce values into different types. The plus operator spec states that if either side of the plus is a string, the ToString
method will be invoked. The remainder of this section stresses the importance of understanding when and why coercion occurs and being intentional with how you are using it in your applications. For Boolean
values specifically, the instructor recommends only coercing undefined, null, or {}
. There are too many corner cases with numbers and strings to make using Boolean
coercion make sense.
Boxing
Accessing methods on primitive values occurs through a process called boxing, which is a form of implicit coercion. All programming languages have type conversions, because it is absolutely necessary. You will always have cases where you have a string where you need to deal with it as a number or a number that you have to deal with as a boolean.
Corner Cases of Coercion
It is impossible to design a system that will not have corner cases. A lot of the corner cases have to deal with Number
, String
has a few as well. If you construct an instance of the boolean object and pass in false, it will return true. Not only does an empty string become zero, but so does any string that is full of white space. Booleans implicitly coerce themselves to numbers, which can be dangerous!
Philosophy of Coercion
Intentional Coercion
You cannot deal with type conversion corner cases by avoiding coercions. Instead, you have to adapt a coding style that makes value types plain and obvious. A quality JavaScript program embraces coercions, making sure the types involved in every operation are clear. Thus, corner cases are safely managed. Within your programs, you get to decide how to deal with coercion and it is in your best interest to pay attention to what you are doing by making them obvious in your code. JavaScript’s dynamic typing is not a weakness, it is one of its strong qualities. It’s one of the reasons JavaScript is so ubiquitous.
Culture of Learning
You should use the tools as effectively as you can and when someone is on your code base that does not quite understand, you should work with them to raise their level of understanding. On our development teams, we should promote advancement amongst all team members. Be effective with your communication by writing clear and communicative code.
Code Communication Q&A
You should not rely on code comments as a crutch for the code to explain itself. Code comments should tell you why something is doing something.
Implicit Coercion
Most developers tend to think that implicit mechanisms are magical. That when something happens behind the scenes and it wasn’t obvious, then it’s some sort of magic. And we tend to equate magic with bad. This is a predominant reason why anti-coercion perspective exists, because people feel like the implicitness of coercion is the downfall. Where they point to the explicitness of type casting in something like Java or C++. Like you would never automatically convert a number into some sort of float or something, but JavaScript does all this sort of automatic stuff and then they say that’s a weakness of JavaScript because it’s magical and bad. It is actually neither bad nor magical, but abstraction.
Not all abstractions are good, but some are necessary. Implicitness, in functional programming, actually makes the implicitness more explicit. Implicitness is not bad, it is the proper use of abstraction. We want to hide key unnecessary details to re-focus the reader and increase clarity. Part of the reason that JavaScript has such a low point of entry is that it does not force the user to focus on such fine details. It would betray JavaScript’s DNA to say that anything implicit should be avoided. Take the following examples:
The second example is more readable and is a fine example of implicit coercion not being a bad or magical thing. And in this example:
If both of the numbers are strings, JavaScript wouldn’t turn them into numbers, the less than operator will do an alphanumeric comparison. Again, if you can guarantee that each of the variables for the less than comparison would be numbers, then the second version of the if statement is just fine and we can deal with implicit coercion without fear. If you have the choice to use coercion but it’s not obvious, it would be in your purview to make it obvious. If you communicate your intent, it will not trip up other readers of your code. Is showing the extra details helpful to the reader (of your code) or not? Sometimes yes, sometimes no. Be a critical and analytical thinker, be an engineer and not a code monkey!
Understanding Features
“If a feature is sometimes useful and sometimes dangerous and if there is a better option then always use the better option”.
–Doug Crawford
But what is useful and what is dangerous? Only what Doug thinks is useful or dangerous? Is there a universal definition for what is useful or dangerous? And what about ‘better option’? How is that measured? This statement is not useful in its abstract sense.
As defined by the instructor:
Useful: When the reader is focused on what is important.
Dangerous: When the reader cannot tell what will happen.
Better: When the reader understands the code.
The takeaway is this; It is irresponsible to knowingly avoid usage of a feature that can improve code readability.
Coercion Exercise
Working With Coercion
In this exercise, you will define some validation functions that check user inputs (such as from DOM elements). You’ll need to properly handle the coercions of the various value types.
Instructions
-
Define an
isValidName(..)
validator that takes one parameter,name
. The validator returnstrue
if all the following match the parameter (false
otherwise):- must be a string
- must be non-empty
- must contain non-whitespace of at least 3 characters
-
Define an
hoursAttended(..)
validator that takes two parameters,attended
andlength
. The validator returnstrue
if all the following match the two parameters (false
otherwise):- either parameter may only be a string or number
- both parameters should be treated as numbers
- both numbers must be 0 or higher
- both numbers must be whole numbers
attended
must be less than or equal tolength
Coercion Exercise Solution
Click to view the solution
Equality
Double & Triple Equals
==
checks value (loose) and ===
checks value and type (strict)? This is a very common misconception. If you’re trying to understand your code, it’s critical that you learn to think like JavaScript. Check the JavaScript specification for Abstract Equality Comparison and Strict Equality Comparison to dig deeper into the difference between ==
and ===
. When the types of the two elements you are comparing with ==
are the same, then (according to the spec) the ===
is performed. Ideally, you should try to have the value types and make that obvious as much as possible. You should not be doing equality comparisons when you have no idea what the types are.
For ===
if the types are different, it returns false
. The basic difference between ==
and ===
is whether or not we are going to allow any coercion to occur. JavaScript’s equality is based on identity, not structure. Consider the following:
While, the above elements may be of the same type, they are effectively different from one another. However, if workshop2 = workshop1
then we could get a true statement to return from a comparison of these two objects.
In summary, ==
allows coercion (types different), ===
disallows coercion (types same).
Coercive Equality
Like every other operation, is coercion helpful in an equality comparison or not? You should consider that statement whenever making a decision to use coercion. Again, driving home the point to be a critical, analytical thinker vs. a code monkey. The decision between using ==
or ===
is a trailing indicator to whether you truly understand your program or not. It is perhaps better to get to the root of why you would be using ===
… do you truly not know what types
of data your function will be working with? How can you fix that? If you can solve this issue, deciding and making obvious what your types are, you will find that you have better code with fewer bugs.
Through coercive equality, you have the option to treat the null
and undefined
values as equal (read the spec…). Which of the following is ‘better’?:
Click to view the code
Arguably, the second if
statement is more concise and readable, and since we know that null == undefined
this would work exactly the same as the first if
statement. This is an example of using coercive equality.
Double Equals Algorithm
According to the Abstract Equality Comparison algorithm, if one of the operators is a number and the other is a string or boolean, it will convert the non-numeric operator into a number using ToNumber()
. Whether or not you like how the algorithm functions is not relevant, understanding how it works and why you’ve received a certain outcome from using the algorithm is what matters.
==
prefers numeric comparison.
Considering the above, which of the below if
statements is more concise?
Click to view the code
Arguably, the second comparison is cleaner, especially if we can guarantee that either variable will only ever be a number or a string. The coercion that would occur from using ==
would be acceptable for this use case. You can choose to structure your code in such a way that coercion is a useful and obvious system, rather than the complex magic that some people feel it to be. If you invoke ==
with something that is not already of a primitive type, it will invoke ToPrimitive()
on that.
Double Equals Walkthrough
If you wrote a function like this:
Click to view the code
What would happen? Would it work coercively? In the above case, yes… but should it? More importantly, why would it work? Why would a number somehow be coercively equal to an array holding that number? According to the specification, the ==
algorithm would execute in the following way:
Click to view the code
The array gets transformed into its primitive type, which is a stringified version of the array (“42”). Then, 42 is compared to the string “42”. The ==
algorithm prefers numeric comparison, therefore the string “42” is now turned into the number 42. And now that both operators are the same type, ==
will do a ===
comparison and finally return true. All of this is an example of when coercion could be a bad thing for a developer. Just because this works does not mean that you should use it. You should reduce the surface area and not make a comparison between numbers and arrays of numbers. The fix for this would not be to use a ===
comparison, but to actually fix the problem, the comparison between two things that are not the same type. Fix it so that the comparisons that you are making make sense.
Double Equals Summary
If the types are the same: ===
If null
or undefined
: equal
If non-primitives: ToPrimitive()
Prefer: ToNumber()
Double Equals Corner Cases
Corner Cases: Booleans
Just don’t do this… If you want to all the boolean conversion of an array to be true, that’s fine.
The comparison of [] == true || false
is unnecessarily complicating the above code. It is simpler, and also more understandable, to just have JavaScript perform the Boolean
operation on the original array instead of comparing it to true or false. In this case, implicit coercion does not have a gotcha whereas the explicit coercion does.
Corner Cases: Summary
How to avoid these corner cases with ==
.
Avoid
-
==
with 0 or “” or “ “ - when either value of the comparison can one of these values -
==
with non-primitives -
==
true or==
false : allow ToBoolean or use===
The Case for Double Equals
You should prefer ==
in all cases. Knowing types
is always better than not knowing them. Static Types is not the only (or even necessarily best) way to know your types. ==
is not about comparisons with unknown types. It’s best to not use ==
when you do not know the types. ==
is about comparisons with known types, optionally when conversions are helpful.
If you know the types in a comparison:
If both types are the same, ==
is identical to ===
Using ===
would be unnecessary so prefer the shorter ==
If the types are different, using ===
would break
Prefer the more powerful ==
or don’t compare at all
If the types are different, the equivalent of ==
would be two (or more) ===
(i.e. slower)
Prefer the faster ==
over multiple ===
If the types are different, two (or more) ===
comparisons may distract the reader
Prefer the cleaner ==
All of the above suggestions are only applicable if you can ensure that the reader of your code can be certain of your types. When you know the types, ==
is the more sensible choice.
If you do not know the types:
Not knowing the types means not fully understanding the code
If possible, refactor the code
The uncertainty of not knowing the types should be obvious to the reader
The most obvious signal about the uncertainty of types is ===
Not knowing the types is equivalent to assuming type conversion / coercion
Because of corner cases, the only safe choice is ===
If you can’t or won’t use known and obvious types, ===
is the only reasonable choice. Even if ===
would always be equivalent to ==
in your code, using it everywhere sends a wrong semantic signal: “Protecting myself since I don’t know/trust the types”. Making your types known and obvious leads to better code. If types are known, ==
is best. Otherwise, fall back to ===
.
Equality Exercise
Wrangling Equality
In this exercise, you will define a findAll(..)
function that searches an array and returns an array with all coercive matches.
Instructions
-
The
findAll(..)
function takes a value and an array. It returns an array. -
The coercive matching that is allowed:
- exact matches (
Object.is(..)
) - strings (except “” or whitespace-only) can match numbers
- numbers (except
NaN
and+/- Infinity
) can match strings (hint: watch out for-0
!) null
can matchundefined
, and vice versa- booleans can only match booleans
- objects only match the exact same object
- exact matches (
Equality Exercise Solution
Click to view the solution
The point of this exercise was to drive home the point that coercion can be safe when you have eliminated the corner cases and made it obvious that you have done so.
Static Typing
TypeScript & Flow
The instructor does not use TypeScript or Flow, because he believe that they are solving problems in a way that makes his code worse. But… here are some details:
Benefits:
-
Catch type-related mistakes - very useful!
-
Communicate type intent - makes code more obvious
-
Provides IDE feedback - 🔥
Caveats:
-
inferencing is best-guess, not a guarantee
-
Annotations are optional - need to opt-in
-
Any part of the application that isn’t typed introduces uncertainty
Inferencing
Consider the following:
The above is an example of static typing that comes with the use of TypeScript or Flow. Because you initially set the value of teacher
to a string, these statically typed systems will infer that teacher
should always be a string… but what if you want to reassign teacher
to hold an object? You’re SOL. You can also strictly declare that teacher
should only ever be a string:
…and you would of course see the same error as before.
Custom Types
With TypeScript and Flow, you can define custom types. The above defines that an object of a type that has a property called name that is of type string. You can pass values of that type as parameters and receive values back as parameters. Any of the above only works based on the ‘guarantee’ that things are assigned correctly.
Validating Operand Types
Something undervalued about TypeScript is that it can tell us that certain operations would be invalid. The following would produce an error in TypeScript:
The only issue with how TypeScript handles the above, in the instructors opinion, is the inability to change how TypeScript handles coercion on a case-by-case basis.
TypeScript & Flow Summary
If you are interested in the differences and similarities between TypeScript and Flow, check out this article. TypeScript and Flow are very useful in that a lot of people are finding them to be helpful to solve their typing issues, helpful to make their types more obvious. The only issue is that you are unable to configure these.
Static Typing Pros
They make types more obvious in code.
Familiarity: they look like other language’s type systems.
Extremely popular nowadays. With TypeScript coming from Microsoft and Flow coming from Facebook, there is no doubt that these static typing systems are here to stay. Learning and using them is actually a pretty valuable tool to add to your toolset.
They are very sophisticated and good at what they do.
Static Typing Cons
They use “non-JS-standard” syntax (or code comments). It is not very likely that these will be adopted into JavaScript spec.
They require a build process, which raises the barrier to entry. While build processes are common these days, it is nice to be able to just write vanilla JS and load it in the browser.
Their sophistication can be intimidating to those without prior formal types experience. The barrier to entry ramps up very quickly.
They focus more on “static types” (variables, parameters, returns, properties, etc.) than value types. JavaScript is a dynamically typed language, so static typing is kinda weird…
Understanding Your Types
JavaScript has a (dynamic) type system, which uses various forms of coercion for value type conversion, including equality comparisons. However, the prevailing response seems to be: avoid as much of this system as possible, and use ===
to “protect” from needing to worry about types. Part of the problem with avoidance of whole swaths of JavaScript, like pretending ===
saves you from needing to know types, is that it tends to systemically perpetuate bugs. You cannot write quality JavaScript programs without knowing the types involved in your operations. Alternatively, many choose to adopt a different “static types” system layered on top. While certainly helpful in some respects, this is “avoidance” of a different sort. Apparently, JavaScript’s type system is inferior, so it must be replaced, rather than learned and leveraged. Many claim that JavaScript’s type system is too difficult for newer devs to learn, and that static types are (somehow) more learnable.
Instructors claim: the better approach is to embrace and learn JavaScript’s type system, and to adopt a coding style which makes types as obvious as possible. By doing so, you will make your code more readable and more robust, for experienced and new developers alike. As an option to aid in that effort, the instructor created Typl, which he believes embraces and unlocks the best parts of JavaScript’s types and coercion.
Scope
Scope
One of the three main pillars of JavaScript is its scope system and how it deals with scope. Specifically, lexical scope. What is the lexical scope mechanism of JavaScript?
Scope is where to look for things. But what are you looking for? All variables in a JavaScript application are either receiving the assignment of a value or you are retrieving the value of a variable. There is not other purpose for the existence of variables in JavaScript programs. It is extremely common for people to think about JavaScript as running top-down, line by line. But JavaScript is not an interpreted language (which would run top-down), it is a compiled language or at the very least, you could say it is parsed. There is some processing that has to happen before execution occurs.
An example of JavaScript being parsed would be having an error somewhere in your code that is thrown at runtime before any JavaScript has even been executed. How did JavaScript know the error was there if it is supposed to execute top-down? There must have been a processing step. Enter compiler theory… In compiler theory, there are essentially four steps (sometimes two are combined, so sometimes there are three steps); lexing and tokenization, parsing (which turns the stream of tokens into an Abstract Syntax Tree), the last step is code generation, which takes the abstract syntax tree and produces some kind of other executable form of the program.
The way that (JavaScript) processing happens before we first execute, is there is a stage where it goes through compilation and parsing, and it produces an abstract syntax tree. It also produces a plan for the lexical environment. That is, where all the lexical scopes are and what is going to be in them. It prepares this plan, and that is the executable code that is handed off to be executed by the other part of the JavaScript engine.
Understanding the above, it is important to think of JavaScript as a two-pass system rather than a single-pass. JavaScript organizes scopes with functions and blocks.
Compilation & Scope
Looking at the above code, you can likely understand everything that will happen, but is that the entire picture? But how exactly does the JavaScript engine think about / handle this code? To understand the complete picture, we need to consider that JavaScript has both a compiler and a scope manager. These two pieces of JavaScript will help us complete the picture of what exactly is happening with the above code. The compiler and scope manager will have some back and forth while sorting through the JavaScript code to organize the code in terms of global scope and local scope. The first var
and the functions will be in global scope while the var
(s) inside of the functions will be in the local scope of their function(s). This is then handed over, as part of the execution plan, to the virtual (JavaScript) machine can run this code.
In a lexically scoped language, which JavaScript is, all of the scopes that we are dealing with are determined at compile time, not at run time. This allows the (JavaScript) engine to more efficiently optimize because everything is known.
Executing Code
After the compiler and scope manager have had their pass at the JavaScript code, execution can happen. Now, the ‘conversation’ will be between the scope manager and the virtual machine (JavaScript engine). The virtual machine will try to run the code and at each step will check with the scope manager to see whether the references exist.
Key to understanding lexical scope: If JavaScript cannot find the reference to a variable that was declared within a scope, it (JavaScript) will go up a level (scope) to look for the reference and continue doing so until it finds the reference or has no additional scope to step through.
Compilation and Scope Q&A
Reread the last two sections or read this stolen from here:
A relatively basic concept in JavaScript is that each declared function creates its own scope. What gets a little more mind bending is the concept of a closure - a function which is able to remember and access its lexical scope even when that function is executing outside its lexical scope.
Lexical scope is the scope model used by the JavaScript language, which differs to some other languages which use dynamic scope. Lexical scope is the scope defined at lexing time.
So, what is lexing time?
This digs into the mechanics of how JavaScript engine works. Despite commonly being referred to as an interpreted language, JavaScript compiles code immediately before executing it. For example the statement: var a = 2;
is split into two separate steps at lexing time:
var a
This declares the variable in the scope, before code execution.
a = 2
This assigns the value 2 to the variable a, if it is found in the available scope.
The lexing phase of compilation determines where and how all identifiers are declared, and thus how they will be looked up during execution. This is the same mechanism which results in “hoisting” variables. The variables are not actually moved within the source code, the declarations simply occur during the lexing phase and so the JavaScript engine is aware of these before execution.
Consider these examples:
Example 1:
Example 2:
Example 3:
Example 1 is straightforward and works as expected, however note the subtle difference between other two examples. Example 2 logs that the value of a is undefined, but the identifier a has itself been declared; compared with example 3 in which the identifier a has not been declared, hence resulting in a reference error.
This demonstrates that during the lexing phase, the JavaScript engine declares the variables first, before the following step in which the values are assigned to the identifiers - this is hoisting. Because functions are also defined at this time (lexing phase), we can say that lexical scope is based on where variables and blocks of scope exist at author time, and thus are locked down at the end of the lexing phase. Scope is not defined at runtime, rather it can be accessed at runtime.
Again, a closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.
So… lexical scope is the author-time scope created by a closure. It is the ‘outer’ scope of a function which is defined inside a closure.
Code Execution: Finishing Up
Continues walking through the code in the ‘Compilation & Scope’ section above. Nothing that hasn’t already been covered. Reread the first few sections of this unit.
Lexical Scope Review
JavaScript is not an interpreted language, in the sense that it does not execute code line-by-line. Instead, JavaScript should be thought of as a two-pass processing. The first pass is compilation / parsing, during which the main thing that happens is the scope / plan is created and the identifiers are mapped. The second pass is execution. Let’s walk through some code… again.
The (compilation) process for the above code is something like this:
Compiler: Hey Scope Manager, I have a formal declaration for a variable called teacher
, have you ever heard of that before?
Scope Manager: No, but I have created it in the global scope.
C: Hey Scope Manager, I have a formal declaration for otherClass
, heard of it?
SM: No, but I have created it in the global scope.
C: BTW, that last thing, that was pointing at a function, so we need a new scope.
SM: Yep.
C: Great, inside of that function’s scope I have another formal declaration for a variable called teacher
, nahmean?
SM: No, but I have created it in the function’s scope.
C: Fine. I have another formal declaration for ask
, have you ever…?
SM: No, but I have created it in the global scope.
C: BTW, that last identifier, ask
, that was pointing at a function, make a new scope.
SM: Yeah, ok. A new scope has been made for the ask
identifier which points at a function.
C: Cool beans. Inside of that function scope, I have a formal declaration for a variable called question
, have you ever?
SM: Nope, but I have created it in the function’s scope.
With no additional declarations in the global scope, the compilation is complete. Time to process / execute the code.
Execution Engine: Hey Scope Manager, I have a target reference for teacher
, ever heard of it?
Scope Manager: Yep, here it is!
EE: Hey Scope Manager, now I have a source reference for otherClass
, have you ever heard of that?
SM: Yep, here it is!
EE: Great. Inside of that scope I have a target reference for teacher
, heard of it?
SM: Yep, here it is!
EE: Ok, now I have a source reference for console
, have you (local scope) ever heard of it?
SM: Nope, let me go up a level (to global scope) and check.
EE: Alright then, global scope, have you ever heard of console
?
SM: Yeah, here it is. You have access to any methods or properties on this object.
EE: 🔥❗️ Now I have another source reference, this time for something called ask
?
SM: Sure, I have heard of it, here it is.
EE: Cool. Now in this functions local scope there is a target reference to question
, have you heard of that?
SM: Yep, here it is.
EE: Awesome, now I have another source reference for console
, (local scope) have you heard of it?
SM: Nope, let me go up a level (to global scope) and check.
EE: Have you (global scope) heard of console
?
SM: Yes! You have access to any methods or properties on this object.
EE: But do you also have any knowledge of the source reference to something called question
?
SM: Yep, here it is.
That completes the program. All of the above happens in a fraction of a millisecond, but it is important to understand how your JavaScript code is compiled and executed.
Compilation Review
Another review, like the last section, but this time walking through the following code:
Try to walk through the code and have the conversion(s) of the two passes (compilation and execution) that JavaScript goes through.
Dynamic Global Variables
In the last section, you can see a variable topic
that was never (formally) declared. What happens? Is there an error or not? (un)Fortunately(?), there is no error, but a new global variable topic
is created for you. This is maybe a ‘bad’ thing, but it is in fact how JavaScript handles this undeclared variable. It comes from the early days of JavaScript when it was considered that people may forget to (formally) declare a variable, and instead of erroring out, one would be created. So forgiving!
But if you turn on ‘strict mode’, the ‘auto’ creation of an undeclared variable would behave differently.
Strict Mode
Turning on strict mode "use strict"
would throw a ReferenceError for the undeclared variable in the previous section. Use strict mode! Most transpilers use strict mode for you. That’s nice. Using strict mode vs. not can introduce some different behaviors for how errors are handled, for example sometimes in non-strict mode errors will be handled silently, but if you use strict mode, you will get feedback for your errors.
Nested Scope
Now let’s look at some code with a nested scope:
The final line of the above code would throw a ReferenceError because ask()
does not exist in the global scope and therefore cannot be located.
Undefined vs Undeclared
What is the difference between undefined and undeclared?
Undefined: A variable exists, but at the moment it has no value.
Undeclared: Never formally declared in any scope that we have access to.
Lexical Scope Elevator
Imagine lexical scope as a building and current scope is the first floor of that building. If you do not find what you are looking for on the floor (scope) that you are on, keep going up until you either find it or not. Global scope is the top floor and if you do not find what you are looking for there, there is nowhere else to go.
🏢
Scope & Function Expressions
Function Expressions
myTeacher
is a function expression. Unlike a function declaration, which adds any declarations into its own scope, a function expression
will add declarations within its own scope, which is not the same scope as its identifier. i.e. myTeacher
is in the global scope, but anotherTeacher
is in its own scope, as is anything declared within it.
The final line console.log(anotherTeacher);
will throw a ReferenceError because anotherTeacher
is not declared in the global scope.
What is a named function expression? It is a function expression that has a name…
The first function above is an anonymous function expression, while the second function is an example of a named function expression. It is suggested that you should 💯% prefer the named function expression over the anonymous function expression, like always, forever, because…
Naming Function Expressions
Here’s why you should always prefer named function expressions over anonymous function expressions:
-
A named function expression creates a reliable self-reference to the function from inside of itself. Useful if you are going to make the function recursive, useful if the function is an event handler and needs to reference itself to unbind itself, useful if you need to access any properties on the function, such as; its length or its name. Any time you need to have a self-reference to a function, the only answer is that it needs to have a name.
-
More debuggable stack traces. If you use a name for your function expression, it will show up in the stack trace.
-
More self-documenting code. If you have an anonymous function, it is a lot harder to determine exactly where an error may be and you have to look between the code and console to make that determination. If you have a named function expression, the process is simplified simply because the name appears with your errors.
The purpose of code is not to make it as convenient as possible for you to type, but to communicate clearly your intent. The argument is made that it is likely more clear and simple to instead use function declarations over function expressions. The purpose of a function name is to tell you, “why does this thing exist?”.
Arrow Functions
Arrow functions are anonymous. The instructor believes that you should not be using arrow functions as a replacement for all other functions. Named (arrow) function expressions? You would be saving characters (used) be just using a function declaration and making your code more readable. More concise code != more readable code
Function Types Hierarchy
Named Function Declaration > Named Function Expression > Anonymous Function Expression. This is not intended to be a hardline rule, you do have to look at the various use cases. But, they are organized in an order that provides a reader of you code with the most information to least.
Function Expression Exercise
Function Expressions
In this exercise, you will be writing some functions and function expressions, to manage the student enrollment records for a workshop.
Note: The spirit of this exercise is to use functions wherever possible and appropriate, so consider usage of array utilities map(..)
, filter(..)
, find(..)
, sort(..)
, and forEach(..)
.
Instructions (Part 1)
Note: In Part 1, use only function declarations or named function expressions.
You are provided three functions stubs – printRecords(..)
, paidStudentsToEnroll()
, and remindUnpaid(..)
– which you must define.
At the bottom of the file you will see these functions called, and a code comment indicating what the console output should be.
printRecords(..)
should:- take a list of student Ids
- retrieve each student record by its student Id (hint: array
find(..)
) - sort by student name, ascending (hint: array
sort(..)
) - print each record to the console, including
name
,id
, and"Paid"
or"Not Paid"
based on their paid status
paidStudentsToEnroll()
should:- look through all the student records, checking to see which ones are paid but not yet enrolled
- collect these student Ids
- return a new array including the previously enrolled student Ids as well as the to-be-enrolled student Ids (hint: spread
...
)
remindUnpaid(..)
should:- take a list of student Ids
- filter this list of student Ids to only those whose records are in unpaid status
- pass the filtered list to
printRecords(..)
to print the unpaid reminders
Instructions (Part 2)
Now that you’ve completed Part 1, refactor to use only =>
arrow functions.
For printRecords(..)
, paidStudentsToEnroll()
, and remindUnpaid(..)
, assign these arrow functions to variables of such names, so that the execution still works.
As the appeal of =>
arrow functions is their conciseness, wherever possible try to use only expression bodies (x => x.id
) instead of full function bodies (x => { return x.id; }
).
Function Expression Solution: Functions
Click to view the solution
Function Expression Solution: Arrow Functions
Click to view the solution
Advanced Scope
Lexical & Dynamic Scope
The idea of nested scope and that a compiler, parser, and processor is figuring out all the scope ahead of time before being executed, this is lexical scope. The vast majority of programming languages are lexically scoped.
Another model for scoping is dynamic scoping. Bash script is an example of a dynamically scoped language.
Lexical scope is fixed at author time and is not affected by run time conditions. Dynamic scoping, as the name implies, means that scope can be affected at run time.
Lexical Scope
Lexical scope is popular because it can be optimized. When you are compiling code, defining variables and functions, etc, at run time there is no additional computation required. Conceptually, you can think of your programs scope as bubbles. There is a bubble around each function, and another around any nested functions, and one globally. You may be able to find a plugin that helps visualize your scopes as well! vscode-levels
Dynamic Scope
In a dynamically scoped language (not JavaScript), the value assigned to a variable is dependent on where it was called from. Consider the following:
In the above code, what is the value of teacher
in the ask
function? In a lexically scoped language (JavaScript), the value of teacher would come from the global scope, because the ask
function does not have any reference to the teacher
variable, so the value of teacher
would be "Kyle"
. But in a dynamically scoped language (not JavaScript), the value of teacher
would depend on where the ask
function was called from, in this case otherClass
. In otherClass
, the value of teacher
is "Suzy"
, so in the ask
function, teacher
would return "Suzy"
.
This can be quite flexible, but it is not how JavaScript works…
Dynamic Scope is determined by the conditions at run time. Even though JavaScript does not have dynamic scoping, there is a feature of the language that allows us to use this type of behavior which will be discussed in a later unit.
Function Scoping
How would you fix an issue where you want/need to have two variables have the same name without them colliding? Wrap at least one of them in a function! But now you have a new scope that still has a naming collision 😞. Enter the principle of least exposure/privilege! The principle states that you should default to keeping everything private and only exposing the minimal necessary. This essentially sets up a defensive posture and is one of the core principles of software engineering. It solves the following problems:
- Name collisions
- It protects your things so that somebody else cannot accidentally or intentionally misuse that thing.
- Protect yourself for future refactoring.
If you expose something, it’s almost a guarantee that someone is going to use it. As soon as someone uses it, you are restricting your ability to refactor it. Because if you refactor it, you are going to break someones code.
IIFE Pattern
IIFE - Immediately Invoked Function Expression - Using a function expression to create a scope, immediately invoking it. If you are making an IIFE, there is likely some purpose for it and it should be named! Whether they are anonymous or not, IIFEs are just functions, which means that you can pass values into them. An interesting use case for an IIFE is a try / catch
. If you are setting the value of a variable based on the result of a try / catch
statement, it could be confusing to your reader… make it an IIFE!
IFFEs can be used any place that you need an expression, and any time you need a statement or scope in an expression.
Block Scoping
Block scoping is done with curly braces {}
, instead of with functions. let
and const
exist so that you can make a declaration inside of a block and it turns that block into a scope. i.e.
Blocks are not scopes until they have a let
or a const
inside of them which implicitly makes them a scope. You should use let
in places where it already makes sense for it to be used, inside of a block scope. let
is a replacement for your already semantically signaled block scope(s).
Is let
the new var
? Nope. let
is a new tool that we should add on to our existing usage and there are still reasons to use the var
keyword.
Choosing let or var
Consider the following:
In the above code, you could replace both var
(s) with let
, but the instructor suggests to only replace the var
in the for loop
with let
. Here’s why: If you have a variable that belongs to the entire scope of the function, the correct and semantic way to signal that to your reader is not to use a let
at the top level of your function scope, but to use a var
because that is the thing it has always done for 24 years. Again, you could replace the initial var
with let
, it is recommended not to because doing so would remove a small amount of important semantic information from the reader, who may then not know your intent. let
is supposed to signify a very localized usage of a variable, ideally within a couple lines of code.
If your code has something that is naturally block scoped, use let
. Here’s an example of some code that would not work if you replaced var
with let
:
If the var
(s) were replaced with let
(s), the let
(s) would be scoped to their blocks (try / catch
) and would not be available for the return
statement. 🤯
var
attaches itself to the function scope and is preferable in the above case because it is able to break out of the unintended block scope of the try / catch
.
Explicit let Block
If you are going to use let
for only a few lines of code and not again within the same function, it is recommended to put it in its own block scope. Like so:
const
const
is even better than let
! But const
does not carry its own weight in JavaScript. const
is a variable that cannot be reassigned. const
can be mutated! const teachers = ["Me", "You"]; teachers[1] = "Someone Else";
There is baggage that comes with const
. What the const
keyword is actually supposed to inform is something along the lines of: For the rest of this [code] block, I am not going to be reassigned. The instructor will only use const
with primitive values, which are immutable; strings, booleans, and numbers.
const Q&A
Q: If you only use const
for strings, would that be a good use case?
A: Yes, I only use const
for primitive, immutable values; strings, numbers, and booleans.
Q: With arrays or objects, I usually just put freeze
or deepfreeze
around it?
A: I do like to use object.freeze
which is a shallow read-only lock of all the properties in an array or object.
Hoisting
Until a couple of years ago, the word ‘hoisting’ literally did not appear in the JavaScript specification. Turns out, hoisting is not a real thing. The JavaScript engine does not hoist, it does not move things around the way it is suggested with hoisting. Hoisting is a made up [English] language convention to discuss the idea of lexical scope. Consider the following code:
What will happen?! JavaScript will, in its first of two passes, parse the above code and create the variables, but without any value assigned to them, so both student
and teacher
would return undefined
. But, the concept of hoisting does not mean that any actual code has been moved around. Functions hoist, they are taken at compile time and defined in such a way so they can be used earlier in the scope than when they’ve been declared.
Hoisting Example
Unlike var
hoisting, function hoisting can be useful. Do you usually put all of your function calls at the end of your files? How about putting them all at the top so you can immediately see them?! You can!
let Doesn’t Hoist
let
doesn’t hoist is not true… let
and const
hoist, but not in the same way as var
or functions. A var
will initialize to undefined
, but a let
or const
will not be initialized. let
or const
will hoist, but will throw a temporal dead zone [TDZ] error. But why TDZ? TDZ exists because of const
. Imagine const
being attached inside of a block scope. And also imagine if const
initialized itself to undefined
. Then you console.log
that const
and it returns undefined, then later it returned the actual value that was assigned to it, now your const
can have two different values assigned to it? This academically violates the concept of const
. So they make the TDZ and also assign this behavior to let
.
It is advised that you declare all let
or const
on the very first line of your blocks to avoid TDZ errors.
Q: Why do function expressions not hoist?
A: When you assign a function expression to a variable, the variable’s declaration itself hoisted, but the assignment only comes at run time.
Hoisting Exercise
Hoisting
In this exercise, you will refactor some code that manages student enrollment records for a workshop, to take advantage of function hoisting.
Instructions
Refactor all inline function expressions to be function declarations. Place function declarations at the bottom (that is, below any executable code) of their respective scopes.
Also, pull function declarations to outer scopes if they don’t need to be nested.
Hoisting Exercise Solution
Click to view the solution
The purpose of this exercise is to flatten the scope of the program, which makes it [scope] easier to manage. Additionally, this can make your code more readable, which makes it easier for both your future self or another developer to dig in and work with your code.
Closure
Origin of Closure
Closure, according to the instructor, is one of the most important ideas ever invented in computer science. The instructor goes on to tell the story of the developer of JavaScript and how pervasive closure is across all development languages yet most developers do not know how to define it. Closure predates computer science, coming from lambda calculus. To understand closure, you have to understand lexical scope.
What is Closure?
Closure is when a function is able to “remember” and access its lexical scope, the variables outside of itself, even when the function is executed outside of that lexical scope. The first part, a function is able to access variables outside of itself, is lexical scope; if teacher
is not defined in my scope, is it in the ‘next-level-up’ scope? And so on, until it finds teacher
or not. But without the second part, even when the function is executed outside of that lexical scope, is what makes closure… closure. The preservation, or linkage, back to the original scope where a function was defined, no matter where it is passed, it retains its value and retains its scope, that is closure.
Here is an example of closure:
Closing Over Variables
myTeacher();
would return Suzy
. Closure is not capturing values, but preserving access to variables. One cannot effectively use closure until you get away from the perception that closure captures values. Consider the following:
i
would only return 4
because we need another variable, or variables, to store the various ‘states’ that i
would be through its iteration. One way to ‘solve’ this would be to declare a new variable and assign it to the value of i
. i.e.
For each iteration, j
will be ‘updated’ with the ‘new’ value of i
, so the console will log the expected values of 1,2,3. An arguably better / more concise approach would be to do the following:
Using let
also produces the expected result because let
creates a new i
for each iteration. This is new behavior as of ES6. The point is if you need to close over different variables, then you need the different variables, not try to capture different values.
Q: The i
declared in the for loop
is interpreted as a new variable for each iteration?
A: Yes, it [the i
] is interpreted as if each iteration there is a new declaration of i
and JavaScript takes care of assigning it the value that it at the end of the previous iteration. All for loop
(s) have this type of let
variance. for of
, for in
, and for
loops have this.
In summary, closure is a preservation of the linkage to a variable, not the capturing of that value.
Module Pattern
Now that we understand lexical scope and closure, we can look at the module pattern. First let’s look at what is not a module, like this:
The above code is not a module, it is what could be called a ‘namespace’. Not really a syntactic feature of the [JavaScript] language, but it’s an idiom that we make namespaces with objects. While there is nothing wrong with writing your code in this way, it is definitely not a module. The reason being is that the module pattern requires the concept of encapsulation. Encapsulation is to hide data and behavior. The idea of a module is that there are things that are ‘public’, that’s your public API, and there are things that are private, things that nobody on the outside can touch. If you want to have a module, you need to have encapsulation (data hiding).
The ‘classic’ module pattern, sometimes referred to as the revealing module pattern, encapsulates data and it does so with closure. You can’t have a module if you don’t have closure. Here is an example of what a module does look like:
The above module has two components to it.
-
An outer enclosing function. In this case, the outer enclosing function is an IIFE. When you run a module as an IIFE, it is kind of similar to a Singleton. Because IIFEs run once and then they are done. But it’s not really done because the closure prevents its scope from going away.
-
The second component of the module is the inner function, which is closed over two variables
teacher
andquestion
. Since it is closed over theteacher
variable, theworkshop
object on the outside, which has reference to theask
function, preserves the inner scope through closure.
The module pattern keeps private state private and exposes things on an object, as you can see in the publicAPI
object, where ask
is exposed. This usage of closure is actually closing over variables that are designed to change state over time. That’s the whole purpose of a module, to track state over time. You could go so far as to say that if you have something that you are calling a module but it does not track state in any way, it is not really a module.
Here is an example of a module in a factory function style:
With the Module Factory pattern, you can create as many instances of the module and they will all have their own state. The module pattern is arguable the most prevalent and important of all code organization patterns. ~80-90% of all JavaScript that’s ever been written has used some mechanism like the module pattern as it’s code organization pattern. But, the module pattern in JavaScript is more of a syntactic hack in that it is not exactly a language feature or first-class citizen, but rather a methodology of using the tools in a way that accomplishes some end goal.
ES6 Modules & Node.js
Because the module pattern is actually not a part of JavaScript, and many wanted them to be, modules eventually found their way to the language in ES6. In the current implementation, they are still very much a WIP. The issue being that TC39 and Node.js did not communicate about how modules would be implemented at first. There have been some conversations about how to fix this, but where its landed is that to use modules in Node, you have to use a new file extension name; .mjs
. There is a working group within Node that is trying to get full support for modules, which they will implement in phases. And at the time of writing this, Node should have pushed phase 1 of 4 of module support in Node. The ES6 module pattern looks like this:
Because the above is a module, it is assumed that everything is private. The way you make something public is by using the export
keyword, everything you do not export will be private. In ES6, modules are file-based. Which means it is impossible to have more than one ES6 module in the same file. Which means that for each module your application needs, you need to have a separate file for it. This can grow quickly. And since you have to compile everything back to the module format we’ve previously covered anyway, wouldn’t it make sense to just write your modules in that format and skip the compilation step?
ES6 Module Syntax
To import modules into your files, there are two major styles used:
The first import is known as a named import
, the second style is known as a namespace import
which is effectively collecting all (*) of the exports and placing them in the namespace of workshop
. The namespace import
is more similar to how modules have been done in JavaScript over the last 20 years, whereas the named import
style is more of a new school thinking. Neither is right or wrong, but comes down to how you prefer to work with your modules.
Whichever way you choose to implement your modules, the same underlying structure / purpose applies. You are organizing a set of behavior into a cohesive unit, hiding data in it and exposing a minimal necessary API. The module pattern is one of the three core pillars of JavaScript and touches everything else in a foundational way.
Module Exercise
Modules
In this exercise, you will refactor some code that manages student enrollment records for a workshop, to use the module pattern.
Instructions
-
Wrap all of the functions in a module factory (ie, function named
defineWorkshop()
). This function should make a return a public API object. -
The returned public API object should include the following methods:
addStudent(id,name,paid)
enrollStudent(id)
printCurrentEnrollment()
enrollPaidStudents()
remindUnpaidStudents()
,
-
Move the
currentEnrollment
andstudentRecords
arrays inside the module definition, but as empty arrays. -
Create an instance of this module by calling
defineWorkshop()
, and name itdeepJS
. -
Define all the student records by calling
deepJS.addStudent(..)
for each. -
Define the student enrollments by calling
deepJS.enrollStudent(..)
for each. -
Change the execution code (the console output steps) to references to
deepJS.*
public API methods.
Work from this code
Module Exercise Solution
Click to view the solution
Modules are pretty important in JavaScript, maybe even in all of software development. You will get a lot of mileage out of thinking in this pattern, practicing it, trying to implement it more. Try to find opportunities to implement the module pattern!
Objects
Objects Overview
Another of the ‘three core pillars’ of JavaScript that is important to understand is the ‘Objects’ Oriented system. Objects, this
, and Prototypes make up the objects oriented system. Objects oriented instead of object oriented because JavaScript is not strictly a class system, there are classes that have been layered on top of it.
The this Keyword
Arguably the most confused of all the JavaScript features, the this
keyword. If you are coming to JavaScript from another language that uses the this
keyword, you may have had some difficulty understanding how this
was implemented in JavaScript.
A function’s this
references the execution context for that call, determined entirely by how the function was called. In other words, if you look at a function that has the this
keyword in it, it is defined by how the function was called. This is counterintuitive because most people think you could look at a function and figure out what its this
keyword is going to point out. But the definition of the function does not matter at all, to determining the this
keyword. The only thing that matters is how the function was invoked.
A this
-aware function can have a different context each time it is called, which makes it more flexible and reusable. Here is an example of the dynamic flexibility of the this
keyword in action:
The this
keyword exists so we can invoke functions in different contexts. There are four different ways to invoke a function. Each of the four ways to call a function in JavaScript will define what is the this
keyword differently.
Implicit & Explicit Binding
The first of the four methods to invoke a function we’ll cover is implicit binding.
Revisiting the namespace pattern, how does the this
keyword behave inside of this pattern? When ask
function is invoked, how do you know what the this
keyword will point at? Because of the ‘call site’ (workshop), the this
keyword will point at the workshop
object. This is exactly how the this
binding works in other languages. This is the most intuitive form of the this
keyword because it decides the method based upon what object you call it from. The idea of having implicit binding is useful because this is how we share behavior among different contexts. Here is another example of implicit binding where the ask
function is being used more than once and has two different this
contexts:
In the above code, you can see that the ask
function is being assigned to different contexts (workshop1
workshop2
) and because of this, the this
keyword will point at the respective objects that it was called from. Another method to invoke functions which we’ve seen before is the .call
method. Along with the .apply
method, the .call
method takes, as their first argument, a this
keyword.
Above, on the first ask.call
, when you pass in workshop1
, it is saying invoke the ask
function with the this
context of workshop1
. It is similar to the implicit binding example at the beginning of this section, the function is still being shared, but it is now being done explicitly rather than implicitly. We are saying, wherever this function comes from, invoke it in this particular context, which I am going to specify. .call
and .apply
can be used to explicitly tell JavaScript what context to invoke a function in.
A variation of explicit binding is called hard binding, which looks like this:
The first setTimeout
above is not the call site for workshop.ask
, so the binding is lost and bound to the global object in this case. A common solution to this can be seen on the second setTimeout
which hard binds the workshop.ask
function to the workshop
object. If you use .bind
to hard bind the this
to a particular scope, you are effectively removing the flexibility of the this
keyword, which is not the intended purpose of the this
keyword. If you do not need the flexibility, you may be better off refactoring your code to a module pattern which provides a fixed, predictable behavior.
If you are only occasionally using hard binding when setting up a this
aware function, you are probably getting enough benefit out of your code as it is written and do not need to refactor it. If you are frequently using hard binding when setting up a this
aware function, you should probably refactor your code. If you want flexible dynamism, use a this
keyword, if you want predictability, use closures, use lexical scope.
The new Keyword
The third way to invoke a function is the new
keyword, which may seem like we are invoking a ‘class constructor’, but it is really just an unfortunate syntactic trick to make it look like it is dealing with classes when it really isn’t. The new
keyword does four very specific things when invoking a function and the purpose of the new
keyword is to invoke a function with a this
keyword that points to a whole new empty object. You could get the same behavior if you used .call()
, which would also invoke a function with a this
keyword that points to a whole new empty object, the only difference is the ‘syntactic sugar’ that the new
keyword provides. Here are the four things that the new
keyword does when it is used to invoke a function:
-
Create a brand new empty object.
-
- Link that object to another object.
-
Calls the function with
this
pointing to the new object. - If the function does not return an object, the
new
keyword assumes thatthis
should be returned.
If you put new
in front of any function, even an empty one, all four of the things will happen.
Default Binding
The final way of invoking a function is with default binding, which looks like this functionName(argument);
. If your program is in ‘strict mode’, and this
has no explicit binding, this
will be undefined and calling a function that refers to this
will throw a TypeError, because in ‘strict mode’ this
needs to be explicitly defined. The reason being, that if you have created a this
aware function, there is no reason that this
should not be defined. Define this
in strict mode! Use one of the other three methods to invoke a function; new
keyword, .call
, .apply
, or .bind
, or a context object, i.e. workshop.ask()
Binding Precedence
What if you had a [function] call site like this:
In the above code, the function ask
is being called with new
, a context object, .bind
, that’s three of the ways to invoke a function being used at once. This is not a useful thing to do, but let’s look at how it works and which of the function invoke(rs?) takes precedence. The order of binding precedence is as follows:
-
Is the function called by
new
? If so, the newly created object will be thethis
keyword. -
Is the function called by
.call
or.apply
(.bind
uses.apply
)? If so, the context object that is specified will be used. -
Is the function called on a context object? If so, use that object.
-
DEFAULT: Global object (except strict mode).
Arrow Functions & Lexical this
But what about arrow functions and the this
keyword?! Lexical this
occurs. But what is lexical this
? An arrow functions this
is not hardbound to its parents this
, the correct way to think about what an arrow function does is; An arrow function does not define a this
keyword at all. So if you put a this
keyword inside of an arrow function, it will behave just like every other variable, which in this case means that it will resolve itself to a scope that does define a this
keyword. The [JavaScript] spec states: An arrow function does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an arrow function must resolve to a binding in a lexically enclosing environment.
Resolving this in Arrow Functions
Ummm… why is this
returning undefined? Well, what is the parent scope for the ask
function in the above code? Did you guess global? If you did, that is correct; the workshop
object is not a scope, it is an object, so the ask
function would look in the global scope for this
, which in this case is in fact undefined. 😞
It is recommended that the only time you should use an arrow function is when you will benefit from the lexical this
behavior. The only alternative is a hack such as var self = this;
. Which is not a good alternative because the this
keyword never points at a function but at a context, so if you absolutely must do the var self = this;
hack, it is probably better to write it as var context = this;
. (Preference of the instructor, may help readability) But really, the purpose, or function, of the arrow function should be to adopt the this
of a parent scope.
Only use => arrow functions when you need lexical this.
If you are going to use an arrow function to have access to lexical this
, you need to combat the following three issues with using an anonymous function:
-
Anonymous functions don’t have a self-reference - In case you need to do recursion or binding.
-
Anonymous functions don’t have a name - Assign it to a variable or a property so that it gets a name inference.
-
Anonymous functions don’t have readily obvious reasons for their existence - Somehow, make it clear to the reader what your function’s purpose is.
this Exercise
this
In this exercise, you will refactor some code that manages student enrollment records for a workshop, from the module pattern to the namespace pattern using the this
keyword.
Instructions
-
Remove the
defineWorkshop()
module factory, and replace it with an object literal (nameddeepJS
) that holds all the module’s functions, as well as thecurrentEnrollment
andstudentRecords
data arrays. -
Change all internal function references and references to the data arrays to use the
this
keyword prefix. -
Make sure any place where a
this
-aware callback is passed is hard-bound withbind(..)
. Don’tbind(..)
a function reference if it’s notthis
-aware.
Work from this code
this Exercise Solution
Click to view the solution
Q: Would you [instructor] use this type of namespace object with so many functions in practice?
A: If I am going to create a namespace(d) object, the only reason I am going to use this approach (instead of the module approach) is when I know that I have two or three objects that I want to link through a prototype chain and have them work with each other.
Q: What is the rule for binding this
?
A: You only need to bind this
if the method is a this-aware function.
ES6 class Keyword
The class pattern is by far the most prevalent used in JavaScript. The class system is syntactic sugar layered on top of the prototype system. The class pattern looks like this:
Classes don’t have to be statements, they can be expressions and they can be anonymous. Classes can be defined with or without an extends clause. In the above code a class is being defined, nothing is being extended. You can choose to define a constructor
and you can add methods (no comma needed). If you want to extend a class, it could look something like this:
If you have a child class that extends the a method of the same name in its parent class, you can refer to the parent from the child by using super.
; i.e. super.ask...
. If you pay attention to the [JavaScript] spec, classes have a lot of features coming down the pipeline, almost making them a language inside of a language, not just syntactic sugar. Class methods are not auto-bound to the this
keyword. The entire class system is built on the idea that your methods do not exist on their instances, but on your prototypes.
Fixing this in Classes
There is a proposal for @bound
which would auto-bind this
in classes, but here’s why that is considered to be aa bad idea. Any ‘solution’ to ‘auto-bind’ this
to a class is not what the intended behavior of classes in JavaScript is. Check out this gist to see how it could be done - see 2.js The whole purpose of this
aware functions is so that they can be dynamic. Forcing them into another mode of working, which is like classes in other languages, is how you end up with ‘hacky’ solutions like the Gist.
class Exercise
class
In this exercise, you will refactor some code that manages student enrollment records for a workshop, from the namespace pattern to the class
pattern.
Instructions
-
Define a class called
Helpers
that includes the functions that are notthis
-aware. -
Define a class called
Workshop
that extendsHelpers
, which includes all the other functions. Hint:constructor()
andsuper()
. -
Instantiate the
Workshop
class asdeepJS
.
Work from this code
class Exercise Solution
Click to view the solution
Prototypes
Prototypes
Now that we have learned what the syntactic sugar layer looks like, lets look under the hood and find out how the prototype system that classes are layered on top of actually works. First observation: Objects that exist in our programs that we can see, have created, or that we interact with are always created by ‘constructor calls’ (via new
). When you use new
in front of a function call, JavaScript is constructing an object to be used for this
binding of that function call. The following statement is not entirely correct:
A constructor call makes an object based-on its own prototype
Let’s learn more about classes from a Computer Science[CS] perspective to understand why ‘based-on’ in the above statement is incorrect. The most common way in CS to describe the difference between a class and an instance is that a class is the abstract pattern for a thing, like a blueprint. The instance is similar to when an architect takes the blueprint and builds the thing. Class-oriented development is fundamentally a copy operation. Continuing on the blueprint / thing being built analogy, what would happen if after you’ve completed the blueprint and built the thing, you went back to the blueprint and added something, would it then be reflected in the thing that was built? Or how about the other way around? If you added something to the thing, would it be reflected in the blueprint? The answer to both is no. The moment the thing is instantiated from the blueprint, the relationship between the two ceases to exist. The instantiation creates a ‘copy’ and everything is inherited.
Speaking of inheritance, a parent / child relationship between classes is also fundamentally a copy operation as previously described. One small caveat to the idea of creating a copy… JavaScript doesn’t do any copying… To properly describe what is happening when a constructor call makes an object, it doesn’t make it ‘based-on’ the prototype, it makes it linked to the prototype. Some would argue that copy vs link is just two sides of the same coin, but copying and linking are fundamentally opposite to one another. The difference between them can completely change based on whether your mental model is all about copying vs linking. The difference matters because when your program breaks, why did it break? It broke because you had a divergence from what you thought your program was doing, your mental model, and what the system was actually doing. If you are thinking copying and it is doing linking, what do you think is going to happen? BUGS!
Prototypal Class
Underneath the syntactic sugar of class
is the prototype
, which looks quite similar. This is useful for our understanding of how classes work.
The Prototype Chain
Above our code is an Object
(with a capital ‘O’) which is a function that is built-in to JavaScript. This function has several methods on it; .keys
, .values
, and various other utilities. Another thing that exists in JavaScript above our code is an object (that doesn’t actually have a name) called prototype
. Prototype gets its name from the method on Object
of the same name. object.prototype
also has a lot of important things on it like toString
valueOf
and a ton of other fundamental utilities that exist in JavaScript. All non-primitives directly descend from object.prototype
, so its a really important object. The final thing that exists ‘above our code’ in JavaScript is the constructor
, which is coming from the prototype
object and points back to the Object
function. All of this stuff is referred to as the line zero environment in this workshop.
Now if you look at the code from the section just before this, when the first function Workshop
is parsed, there is a similar set of relationships created as described above. Workshop
is a function which gets a link to a prototype
which links back to the function with a constructor
, but in this case, the prototype
object of the Workshop
function points back to the prototype
of the ‘line zero environment’. Still looking at the code from the last section, when deepJS
and reactJS
are created with new Workshop...
, a new object is created for them, with a single property teacher
and the object has a link to the original Workshop
function prototype object. So when you call ...ask
, the deepJS
and reactJS
objects do not have this function, but because of their link to the Workshop
functions prototype object, they are able to retrieve the ask
function from there.
If reading all that is too confusing, here’s a diagram:
Dunder Prototypes
How is it possible to set the constructor
property of deepJS
when that property does not exist? Because while the object deepJS
does not have the constructor, the prototype chain points to the original object which does have a constructor
property that deepJS
can use. The .__proto__
on deepJS
is what is referred to as a ‘Dunder Prototype’, underscore underscore proto underscore underscore = double underscore proto = dunder proto. deepJS
does not have a dunder prototype on it, nor does what it is directly linked to Workshop.prototype
, and what Workshop.prototype
is linked to object.prototype
also does not have a dunder prototype, but it does have a getter that is dunder prototype.
this & prototypes Q&A
Q: If a function is already bound using .bind
can it later be re-bound to another object?
A: There is only one possible way to override the binding, you cannot call .bind
on it again, but you could invoke it [the function] again with the new
keyword, which would have the effect of overriding its existing behavior to that of the newly constructed object.
Q: If you define a variable inside of an arrow function, is that variable scoped to the arrow function or to the parent scope [of the arrow function]?
A: Arrow functions do have lexical scope, they just don’t have a this
keyword.
Q: In the case of a this
-aware callback, does the value of this
depend on how the higher order function calls the callback?
A: Yes, the call site is the only thing that matters. If you pass a function into .map
, how .map
invokes your callback is the only thing that determines its this
binding. Unless you have passed in a hardbound function, in which case, your hardbinding will take precedence.
Q: What happens when you set dunder proto?
A: It is not very common to use dunder proto as a setter. It is technically both a getter and a setter. It is almost always used only to reference it. It could be considered an anti-pattern to rewire the prototype chain of an object. But as of ES6, the dunder proto is officially something that can set the proto linkage from one object to a different object.
Q: Do the prototype objects come with every function?
A: Regular functions have prototypes, arrow functions do not.
Shadowing Prototypes
If you were to do the above, add a ask
to deepJS
, what would happen? This would be considered ‘shadowing’, two different levels of the prototype chain with the exact same property. When in deepJS.ask
, what would the this
point to? The answer of course depends on where the call site is, and in this case, when you call deepJS.ask
, the implicit binding will bind the this
to deepJS. So, the code is effectively recalling deepJS.ask
again, which results in infinite recursion and stack overflow. The this.
does not work in place of a super()
. If you intended to go up one level in the prototype chain to Workshop
, how would you do that? __proto__
(dunder proto)!
If you left out the .call
, the above code would point to Workshop
. So doing the above would allow you to go one level up the prototype chain and then invoke it in our this
context. When you try to shadow without using the class system, when you are in prototypes and you try to shadow, you end up creating this complete breakage. So why would you shadow in the first place? Because shadowing is how you do polymorphism. In class design theory, the whole point of having a child class is so you can inherit something from the parent, override it and then call super
to access the parent version of it, to extend it.
Prototypal Inheritance
If you wanted to do a true sort of child class in the prototypal style, you’d do something like this:
In the above code, the way you make AnotherWorkshop
extend or inherit from Workshop
is seen on the line where Object.create
is called. This is changing the initial [prototype] link of AnotherWorkshop
to that of Workshop
. Object.create
does two things:
-
Creates a brand new empty object.
-
Links the new object to another object.
This is the same as the first two steps of the new
algorithm but as a specific API method called Object.create
.
Classical vs Prototypal Inheritance
In classical oriented languages; C++, Java, etc… When you make a class called Workshop
and you instantiate it, you are conceptually, and in some cases physically, copying down into those instances. And when you make a child class called AnotherWorkshop
that extends Workshop
, you are copying down into AnotherWorkshop
, and when you instantiate that child class, you are doing more copies.
When you try to do prototypal inheritance, you have a Workshop.prototype
object, and then you make other objects, they are then linked to the Workshop.prototype
object. And all the other operations you do are always just linked to the original Workshop.prototype
, no copying.
Prototypal inheritance is not a useful term to describe what is actually happening. It is what it is and it is definitely not a class based language.
Inheritance is Delegation
There is nothing wrong with JavaScripts inheritance system, but it is important to understand that it is a different pattern than classes. JavaScripts design pattern is called delegation. JavaScripts prototype system is a delegation system, not a class system. Neither prototype nor class system is ‘wrong’ or ‘bad’, they are just different. The prototype system is actually more powerful than a class system because the prototype system can be seen as a ‘super set’ and a class system can be seen as a ‘sub set’. Why? Because you can implement a class system inside of a prototypal language, but you cannot do the reverse.
What can we do with delegation if we set aside our preconceptions that classes are the only design pattern that matters?
OLOO Pattern
OLOO - Objects Linked to Other Objects
Here is an OLOO style block of code:
Everything is an object! And all of the linking between the objects is happening thanks to Object.create
. But how does Object.create
make the magic? Take a look at this Object.create
polyfill for some insight.
The polyfill makes an empty function, sets its prototype to ‘o’ and calls new on whatever function to give us the newly created object. Essentially, Object.create
takes all of the constructor functions, .prototype
(s), and new
keywords and hides them inside of Object.create
, which leaves just a clean, simple linkage between objects.
Delegation-Oriented Design
The Delegation design pattern does not consider parent / child relationships, but peer-to-peer. If you were working on a Login Page that required a couple of objects, one for authentication and another for the UI, a delegation design pattern would handle this in the following way:
The above two objects AuthController
and LoginFormController
are linked through the prototype chain, so when one needs something from the other, it can find it and they can work together. One of the many benefits of this linking is that your code is more testable.
Wrapping Up
Wrapping Up
Know your JavaScript, ask more questions, be more curious, know why the code works. In doing this, you will have a better chance of fixing things when they break. Take it seriously, read the spec every once in a while to develop your mental model. Through the understanding of that tool communicate your ideas and intent more effectively.