Skip to content

Instantly share code, notes, and snippets.

@mikahimself
Last active October 19, 2024 04:48
Show Gist options
  • Save mikahimself/02dc24dd8cb3848af1338cfa1af2390b to your computer and use it in GitHub Desktop.
Save mikahimself/02dc24dd8cb3848af1338cfa1af2390b to your computer and use it in GitHub Desktop.
Notes from Deep JavaScript Foundations v3

Deep JavaScript Foundations v3

Types

Primitive types

In JavaScript, everything is NOT an object. ECMAScript language types are:

  • Undefined
  • Boolean
  • String
  • Symbol
  • Number
  • Object
  • Null

Functions and arrays are not included in the list because they are not primitive types; instead, they are subtypes of object.

typeof operator

The typeof operator does not check what the type of a variable is, it checks what the type of the value stored in a variable is.

let v = "1";
typeof v;     // "string"
v = 2;
typeof v;     // "number"

typeof ALWAYS returns a string from what is essentially a short list of enums. So, never check for typeof item == undefined. typeof has a few exceptions that you should keep in mind, though:

let v = null
typeof v    // "object" :o

v = function(){};
typeof v;   // "function" 

v = [1, 2, 3]
typeof v;   // "object"
Array.isArray(v)    // true

undefined vs undeclared vs. uninitialized

Even though undefined and undeclared seem like synonyms, they most definitely are not synonymous in JavaScript. If a variable is undeclared, it has never been created in any scope that we currently hava access to. However, if a variable is undefined, it has been declared but has, at the moment, no value. Still typeof will consider an undeclared variable as undefined. ES6 introduced another concept of "emptiness" that can be called uninitialized; this state is present for block scoped variables that do not get initialized, or, in other words, get initially set as undefined.

NaN and isNaN

NaN is often said to be an acronym for Not a number. This, however, is not true, as NaN indicates that something is an invalid number. We can have several different kinds of representations of numbers in JavaScript that can be successfully coersed into numbers, and not result in NaN:

let myAge = Number("0o46")      // 38 in octal format
let myNextAge = Number("39")    // 39

However, when we try and convert a value that has no possible numeric representation, we get a NaN as a result:

let myCatsAge = Number("n/a");  // NaN

Any operations that use NaN as part of the operation will produce NaN as the result. Take for instance the following example:

myAge - "My son's age"          // NaN

What happens here is that JavaScript sees the minus operator that can only be used with numbers, and tries to coerce the string into a number. This obviously fails, producing NaN, which means that the calculation will become myAge - NaN. And since NaN used in any mathematical operation results in NaN, since it is an invalid number.

NaN is the only value in JavaScript that lacks the identity property, which means that NaN is not equal to itself. Hence the following:

let myCatsAge = Number("n/a");  // NaN
myCatsAge === myCatsAge         // False :o

Since the equals operator cannot be used to define whether something is NaN, JavaScript has functions for checking if something is a NaN; isNaN() and Number.isNaN(). Naturally, these produce different results:

isNaN(myAge)                    // false, since this is a number
isNaN(myCatsAge)                // true, and correctly so
isNaN("my son's age")           // true, but only because isNan() coerses the value into a number before checking it

Number.isNaN(myCatsAge)         // true
Number.isNaN("my son's age")    // false

Negative zero

The IEEE 754 standard requires that JavaScript has both negative and a positive zero value. The uses cases for signed zero might seem rare, but it can be used, for instance, in games or applications that deal with direction. -5 can be interpreted as something that is moving left, but when the object that is moving comes to a halt, the direction would be lost when using 0. So that's where the values might come in handy. But testing for negative zero used to be a pain:

let trendRate = -0;
trendRate === -0          // true

trendRate.toString();     // "0" - whoopsie!
trendRate === 0;          // true - whoopsie!
trendRate < 0;            // false
trendRate > 0;            // false

With ES6 and the addition of Object.is(), checking for negative zero became simpler:

Object.is(trendRate, -0)  // true
Object.is(trendRate, 0)   // false

Fundamental objects aka Built-in objects aka native functions

Fundamental objects in JavaScript that you should always use the new keyword with are:

  • Object()
  • Array()
  • Function()
  • Date()
  • RegExp()
  • Error()

...and the fundamental objects with which you shouldn't use new are:

  • String()
  • Number()
  • Boolean()

So, in practice this means the following:

let yesterday = new Date("April 8, 2020")
yesterday.toUTCString();

let noOfBeast = String(beast.number)
// "666

Coersion

Abstract operations

Abstract operations the crucial building block in dealing with type conversion, or coercion in JavaScript. Abstract operations are not actual functions that you can call, but a set of algorithmic steps that a JavaScript engine needs to take to convert something from one type to another. Abstract operations are by nature recursive, which means that if the toPrimitive operation returns a value that's not a primitive, the operation will get called again and again until it results in a primitive, or in some cases an error.

ToPrimitive(hint)

The ToPrimitive abstract operation needs to be performed when we have a non-primitive value - for example, an object, an array, or a function - that needs to be converted into a primitive value. The operation takes an optional hint parameter that is essentially a request for the operation of what type of a primitive the caller would like to have returned. So, in the case of a numeric operation, the hint would be a number and in the case of a string based operation, it would be a string. If the hint is left empty, the request is just saying that it'll accept any kind of a primitive as the return value. This hint is NOT, however, a guarantee of the type that will come out as a result.

Behind the scenes, the toPrimitive algorithm will end up invoking the following methods in the order listed to get results:

Hint: "number" Hint: "string"
valueOf() toString()
toString() valueOf()

ToString

The ToString abstract operation takes any value and gives you the representation of that value in string form. If we call the ToString operation on an object, it will in turn call the ToPrimitive with the hint string. The ToPrimitive will then end up calling toString() first, if it is available, and valueOf() if it is not.

For objects, the default behavior is to:

  1. Add square brackets
  2. Add lower-case object
  3. Add the string tag, which for all default objects is Object (resulting in "[object Object]"

In ES6, you can use an ES6 symbol to override the string tag in your own, custom objects, and have it say whatever you like. This can be done by overriding the toString() method of the custom object:

{ toString() { return: "Fiddlesticks"; } }

An even more useful way to use this override would be to, for instance, use JSON.stringify() in the overridden method and return the results:

const myObject = {
  name: "My Own Private Object",
  id: 80085,
  isTrue: true,
  toString: () => {
    return JSON.stringify(this);
  }
};

console.log(myObject);

ToNumber

The ToNumber abstract operation is invoked every time there is a numeric operation and we don't have a number. With this, there are a lot of corner cases and interesting results:

//           ""   0  <-- root of all evil here.
//          "0"   0
//         "-0"   0
//     "  009 "   9
//    "3.14159"   3.14159
//         "0."   0
//         ".0"   0
//          "."   NaN
//       "0xaf"   175
//
//         true   1
//        false   0
//         null   0
//    undefined   NaN

When we invoke the ToNumber abstract operation on a value, it in turn invokes the ToPrimitive with the hint number. This in turn calls the valueOf() function first, and if that is not available, the toString() function. So, what does this do?

For objects and arrays, the default valueOf operation returns this, which then gets deferred to the toString() function. This leads to some interesting results:

//           []   0
//        ["0"]   0
//       ["-0"]   -0
//       [null]   0   // null -> empty string -> 0
//  [undefined]   0   // undefined -> empty string -> 0
//    [1, 2, 3]   NaN
//     [[[[]]]]   0
//
//         {..}   NaN  

Because an object's toString() method returns "[object Object]" that has no numeric representation, we get a NaN for objects by default. We can, however, override the valuOf method for our custom objects and get better results.

{ valueOf() { return 667; } }

ToBoolean

ToBoolean is the last major abstract operation. This operation occurs whenever there is a value that's not a boolean used in a place that requires a boolean. This operation is not algorithmic, but more of a lookup operation. It essentially looks for values that have been defined as being falsy, and if it finds one, return false; otherwise, the ToBoolean abstract operation will return true.

Falsy Truthy
"" "foo"
0, -0 23
null { a: 1 }
NaN [1, 2, 3 ]
false true
undefined function(){}
...

The ToBoolean abstract operation does NOT invoke the ToPrimitive, ToString, or ToNumber algorithms, it just does a lookup. Therefore, the ToBoolean lookup for an empty array results in true.

Cases of coersion

You should avoid coersion because it is buggy and evil and use triple quotes instead

  • Some douchebag

Template literals

Template literals are a case where most people, knowingly or not, use coersion. For example, when using a variable whose value is a number results in an implicit coersion.

let numOfStudents = 16;

console.log(
  `There are ${numOfStudents} students.`
);

String concatenation

String concatenation is another common use-case where implicit coersion occurs:

let msg1 = "There are ";
let numOfStudents = 16;
let msg2 = " students.";
console.log(msg1 + numOfStudents + msg2);

Coersion happens here due to operator overloading. Even though plus is normally associated with numerical operations, but if either of the items used on left of right side of the plus operator is a string, the plus operator prefers string concatenation. And to do this, the item that is not a string is processed with ToString. To do the conversion explicitly, you can use String().

console.log(`There are ${String(numOfStudents)} students.`);

String to number conversion

Coersion often happens when dealing with user input in web applications. For example, when you convert a string from a form element into a number using the unary plus operator, JavaScript will, behind the scenes, invoke the ToNumber abstract operation.

function addStudent(numStudents) {
  return numStudents + 1;
}

addStudent(+studentsInputElem.value);

A more explicit way to deal with this would be to use the Number() function:

addStudent(
  Number(studentsInputElem.value)
);

The minus operator is only defined for numbers, so again, if it encounters a string or another value that is not a number, it will invoke the ToNumber abstract operation.

Coersion with booleans

Implicit coersion is a very common practice with boolean values:

if (studentsInputElem.value) {}

while (newStudents.length) { enrollStudent(newStudents.pop()); }

These, however, can lead to unfortunate bugs. In the first case above, the value will be falsy, if the value is an empty string. If the value string contains empty spaces, though, it will be truthy. In the second cases, we might get unexpected results when a value is NaN.

A safer way to do these:

if (!!studentsInputElem.value) {}         // Double NOT forces a conversion into a primitive boolean
if (Boolean(studentsInputElem.value)) {}

while (newStudents.length > 0) { enrollStudent(newStudents.pop()); }

Boxing

Boxing is a type of implicit coersing that occurs when you have an element that is not an object, and you are trying to use it as if it is an object. For instance, when checking the length of a string. JavaScript implicitly coerces the primitive value into an objects and allows us to access its properties

if (studentNameElem.value.length > 50) {
  console.log("Student's name is too long.")
}

Corner cases of coercion

Every programming language has corner cases related to conversions. Instead of making fun of certain languages for their specific corner cases, it might be more useful to learn those corner cases and work around them. Some of JavaScripts corner cases include:

Number("");                   // 0            WTF?
Number("   \t\n");            // 0            WTF?
Number(null);                 // 0            WTF?
Number(undefined);            // NaN
Number([]);                   // 0            WTF?
Number([1, 2, 3]);            // NaN
Number([null]);               // 0            WTF?
Number([undefined]);          // 0            WTF?
Number({});                   // NaN

String(-0);                   // "0"          WTF?
String(null);                 // "null"
String(undefined);            // "undefined"
String([null]);               // ""           WTF?
String([undefined]);          // ""           WTF?

Boolean(new Boolean(false));  // true         WTF?

For coersion related to numbers, the root of all evil is the fact that an empty string becomes 0. And because the ToNumber abstract operation strips off all leading and trailing whitespace, any string full of whitespace becomes 0, too.

The numerification of Booleans to 1 and 0 lead to interesting issues as well:

Number(true);         // 1
Number(false);        // 0
1 < 2;                // true
2 < 3;                // true
1 < 2 < 3;            // true, but...

// The operation above is actually handled like this:
(1 < 2) < 3;
(true)  < 3;
1 < 3;                // true, but hmm...

// On the flipside:
3 > 2;                // true
2 > 1;                // true
3 > 2 > 1;            // false  WTF?

// And this again is because it is handled like so:
(3 > 2) > 1;
(true) > 1;
1 > 1;                // false

Philosophy of Coercion

Avoiding coersions is not the correct way to deal with corner cases of type conversion. Instead, you have to adopt a coding style that makes value types plain and obvious. A quality JavaScript program embraces coersions and makes sure the types involved in every operation are clear, thus managing corner cases safely.

Rigid typing, static type and type soundness are a reaction to the issues coming from issues related to coersion, but they might not be the right reaction. Instead, dynamic typing could be thought as one of JavaScript's strong qualities.

Implicit coersion

Implicit coersion is neither magic nor evil. Instead, it can be thought of as an abstraction. By using implicit coersion, we are hiding unnecessary details from the reader, refocusing them on the important details and increasing clarity. For instance, boxing is one of these features that hides an unnecessary type conversion to increase legibility.

In the case to template literals, implicit conversion is more readable than using an explicit type conversion, and should not cause any unexpected issues if you use conditional clauses to deal with corner cases related to NaN and -0.

let numOfStudents = 16;

console.log(
  `There are ${String(numOfStudents)} students.`
);

// VS.

console.log(
  `There are ${numOfStudents} students.`
);

Another case where implicit coersion can be useful is with the less than operator that we know to coerce items into a number, if one of them is already a number. When can then use coercion to produce a less verbose if statement.

var workshopEnrollment1 = 16;
var workshopEnrollment2 = workshop2Elem.value;

if (Number(workshopEnrollment1) < Number(workshopEnrollment2)) {}

// VS

if (workshopEnrollment1 < workshopEnrollment2) {}

If we are certain that at least the other value being compare is a number, we can safely let JavaScript handle the type conversion for us. However, we should be aware that if both values are strings, JavaScript will, instead, do an alphanumeric comparison.

So, the real question you, as the author of the code, must ask yourself is that will the reader of the code find the extra type details helpful or distracting, and make a choice based on that.

Equality

== vs. ===

It is a common misconception that double equals checks only the value and triple equals checks both the value and the type. You can get by, in some cases, with trusting this interpretation, but it can lead you to problems in others. However, the reality is somewhat different.

Both equality checks actually perform the type check, and if the types match, they behave in exactly the same way. The main difference is that abstract equality comparison allows coersion, but strict equality comparison doesn't. So, what does the triple equals, or the strict equality comparison actually do?

Strict equality comparison

  1. If Type(x) is different from Type(y), return false.
  2. If Type(x) is Number, then a. If x is NaN, return false b. If y is Nan, return false c. If x is the same Number value as y, return true. d. If x is +0 and y is -0, return true e. If x is -0 and y is +0, return true f. Return false
  3. Return SameValueNonNumber(x, y)

Identity, not structure

Another misconception has to do with dealing with objects and how JavaScripts compares them. JavaScript, when comparing objects, focuses solely on identity and not structure. So, in a case like the following, both comparison operators would yield the same result, that is, false.

let workshop1 = { name: "Deep JS Foundations" };
let workshop2 = { name: "Deep JS Foundations" };

console.log(workshop1 ==  workshop2);   // false
console.log(workshop1 === workshop2);   // false

Coercive equality

So, the main difference between double equals and triple equals is that the former allows coercion to happen. The main thing then is to ask yourself whether coersion is helpful in one particular case or not; is it more safe or is it less safe.

Abstract equality comparison

Let's take a look at how double equals, or the abstract equality comparison works in detail. Points to note are that:

  • Double equals prefers number comparisons
  • Double equals convert non-primitives to primitives unless both items are of the exact same type
  1. If Type(x) is the same as Type(y), then return the result of performing the strict equality comparison x === y.
  2. If x is null and y is undefined, return true
  3. if x is undefined and y is null, return true --> Null and undefined match each other through coercive equality, but do not match anything else in the JavaScript language.
  4. If Type(x) is Number and Type(y) is String, return the resulty of the comparison x == ToNumber(y)
  5. If Type(x) is String and Type(y) is Number, return the resulty of the comparison ToNumber(x) == y
  6. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y
  7. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y)
  8. If Type(x) is either String, Number, or Symbol, and Type(y) is Object, return the result of the comparison x == ToPrimitive(y)
  9. If Type(x) is Object and Type(y) is either String, Number, or Symbol, return the result of the comparison ToPrimitive(x) == y
  10. Return false

Coercion is useful when checking for empty values. You could, if you wanted to, check with triple equals whether a value is null or undefined, or, if you are just interested in whether the value is empty, just check for null with double equals and gain the same result thanks to coercive equality.

let workshop1 = { topic: null };
let workshop2 = {};

if ((workshop1.topic === null || workshop1.topic === undefined) && 
    (workshop2.topic === null || workshop2.topic === undefined)) {}

if (workshop1.topic == null && workshop2.topic == null) {}

Coercion can reduce clutter in numerical comparisons in cases where whe know what types we are dealing with, especially if we are dealing with numbers, since the double equals algorithm prefers numbers over other types.

let workshopEnrollment1 = 16;
let workshopEnrollment2 = workshop2Elem.value;

if (Number(workshopEnrollment1) === Number(workshopEnrollment2)) {}

if (workshopEnrollment1 == workshopEnrollment2) {}

Silly double equals walkthough

You should never write code where you end up comparing a primitive number to an array containing a number. But if you did, here's how it would play out:

let workshop1Count = 42;
let workshop2Count = [42];

// if (workshop1Count == workshop2Count)  // No match, ToPrimitive called on workshop2Count, which gets stringified
// if (42 == "42") // No match, ToNumber called on "42"
// if (42 === 42) // Types are equal, so triple equals called
// if (true)

Double equals corner cases

Some cases presented against javascript are based on contrived, non-sensical constructs. For example the following:

[] = ![]      // true, WAT!?

JavaScript does produce this odd result, but there is no real world reason to compare an array to a negation of itself. Still, lets walk through the process:

let ws1Students = [];
let ws2Students = [];

if (ws1Students == !ws2Students) { // Yes, but why?}
// Stepping through:
// if ([] == false) // When we negate ws2Students, we essentially consider the array as being truthy and the negate it to false
// if ("" == false) // Comparing non-primitive to a primitive, must convert array to primitive, which becomes ""
// if (0 == false) // Since numbers are preferred, "" becomes 0
// if (0 === 0) // Since numbers are preferred, false becomes 0


if (ws1Students != ws2Students)  { // Yes, but why?}
// if (!(ws1Students == ww2Students))
// if (!(false))
// if (true) // because this is an identity questions. The identity of ws1 is different from ws2

Corner cases: booleans

Using double equals with booleans is almost always a bad decision. Use the implicit ToBoolen conversion instead.

let workshopStudents = [];

if (workshopStudents) { // Yes } // workshopStudents gets implicitly converted using ToBoolean

// Process
// if (workshopStudents)
// if (ToBoolean(workshopStudents))
// if (true)


if (workshopStudents == true) { Nope :( }

// Process
// if (workshopStudents == true)  // array converted with ToPrimitive
// if ("" = true)
// if (0 == true)
// if (0 === 1)
// if (false)


if (workshopStudents == false) { Yep :( }

// Process
// if (workshopStudents == false) // array converted with ToPrimitive
// if ("" == false)
// if (0 == false)
// if (0 === 0)
// if (true)

Corner cases: summary

Avoid:

  • using == with 0, "" or " "
  • using == with non-primitives
  • using == true and == false. Allow ToBoolean instead, or use ===

The case for preferring double equals

Knowing types is always better than not knowing them. Static types is NOT the only - or necessarily the best - way of knowing your types. Uncertainty in this area often leads to bugs.

Double equals is NOT about comparisons with unknown types. In cases where types are unknown, you should never use double equals. You should always strive to know your types and only use double equals when you know the types. Double equals is strictly about comparisons with known types, and optionally in places where conversions are helpful and you want to take advantage of coersion (in which case double equals is your only option).

If you know the types in a comparison:

  • If both types are the same, == is identical to ===. In this case, it is unnecessary to use ===
  • Similarly, if the types do not match, it is pointless to use === as this will always result in false. In this case, you should use the more powerful == or not do the comparison at all.
  • If the types are different, the equivalent of one == would be two or more ===, which is slower, since it is faster for Javascript to do an implicit coercion than for you to do two or more explicit ones
  • If the types are different, two or more === comparisons may distract the reader, as in the null/undefined comparison:
if (workshop1.topic === null || workshop1.topic === undefined)

// vs

if (workshop1.topic == null)
  • When you KNOW the types, whether they are the same or not, == is the more sensible choice.

If you do NOT know the types in a comparison:

  • You do not fully understand the code. If possible, you should refactor this code so that it is easier to undestand and you can know the types
  • You should make the uncertainty of not knowing the types obvious to the reader (through comments, for instance), but the most obvious signal about the uncertainty is ===, if == is reserved for cases where you know the types.
  • Not knowing the types is equivalent to assuming type conversion, and because of corner cases, the only safe choice is ===

In summary, if you cannot or will not use known and obvious types, === is the only reasonable choice. In addition, even if === would always be equal to == in your code, using it everywhere sends the wrong semantic signal to the reader - that you are protecting yourself since you don't know or trust the types. Making your types known and obvious leads to better code, and if types are known, == is the best choice.

TypeScript, Flow, and type-aware linting

Benefits of TypeScript and Flow and the like

  1. They help catch type-related mistakes
  2. They communicate type intent
  3. The provide IDE feedback
  4. Familiarity: they look like other languages' type systems
  5. They are extremely popular these days

Caveats

  1. They use non-JavaScript standard syntax
  2. Inferencing is best-guess, not a guarantee
  3. Annotations are optional
  4. Any part of the application that isn't typed introduces uncertainty
  5. They require a build process, which raises the barrier to entry
  6. Their sophistication can be indimitating to those without prior formal types experience
  7. They focus more on static types (variables, parameters, returns, properties etc.) than value types

Scopes

Scope is where JavaScript looks for variables. While many people think that JavaScript is not a compiled language and that it just gets executed, line by line at runtime, this is not necessarily true. Inserting invalid syntax in line 10 does not mean that lines 1-9 get executed, but it means that you get a syntax error message when you try and run your program. Clearly, JavaScript does some form of processing before executing the code.

During this processing (or compilation even) step JavaScript goes through the code and sets up the scopes of the program using functions and blocks, and arranges the identifiers to be used in correct scopes. JavaScript determines lexical scopes at compile time; this allows the JavaScript engine to optimize programs more effectively, because everything is both know and fixed - scopes do not change at runtime.

Scope and function expressions

function teacher() { /* ... */ }. // Function declaration causes function to live in the global scope as expected

const myTeacher = function anotherTeacher() {   // Function expression creates its own scope 
  console.log(anotherTeacher)                   // ...which means that anotherTeacher will be available for console.log here
}

console.log(teacher);                           // OK
console.log(myTeacher);                         // OK
console.log(anotherTeacher);                    // Reference error

Function declarations attach themselves to the enclosing scope. Function expressions, however, will attach themselves to their own scope.

Named function expressions

In JavaScript, we can use both named function expressions and anonymous function expressions. However, it may be wise to always stick with named function expressions. There are three main reasons for this:

  1. The name provides a reliable self-reference to the function from within itself. This is useful if the function needs to be used recursively, or if the function is an event handler that needs to reference itself to unbind itself.
  2. The name of a named function expression shows up in the stack trace, helping in debugging.
  3. Makes the code more self-documenting -> the name of the function tells what it does.

Lexical scope

You can think of lexical scope as very closely related to the JavaScript compilation process. Even though JavaScript is often said not to be lexically scoped, the JavaScript compiler does in fact, before execution time, figure out all of the nested scopes within your program. Lexical scoping is always fixed at authoring time and is in no shape or form changed at runtime.

Function scoping

When authoring software, one key thing to keep in mind is the Principle of Least Privilege, according to which each module or function should only have access to the minimal set of information it needs to do its job. By using this principle, we avoid three major problems:

  • Naming collisions
  • When you hide something, someone else cannot accidentally or intentionally misuse it
  • If you hide something, you protect yourself for future refactoring

To consider function scoping, here's a simple example. First, the program worked so that it defined a variable called teacher and gave it a value of "Kyle", and printed it out later on in the program. Then, someone added another variable assignment and broke our program:

let teacher = "Kyle"

let teacher = "Suzy"
console.log(teacher)


console.log(teacher) // Was expecting Kyle, got Suzy

To fix this, we could wrap the other assignment within a function:

let teacher = "Kyle"

function anotherTeacher() {
  let teacher = "Suzy"
  console.log(teacher)
}

anotherTeacher()

console.log(teacher)  // "Kyle"

Now the program works again, but we have now have the function anotherTeacher() cluttering our namespace. To solve this, we can use the Immediately Invoked Function Expression pattern, or the IIFE pattern:

let teacher = "Kyle"

( function anotherTeacher(teacher) {
  console.log(teacher)
} )("Suzy");

console.log(teacher)  // "Kyle"

This way, the function gets called immediately once it is declared, and is disposed of afterwards. Because of the enclosing parentheses (function anotherTeacher(){}()) is not a function declaration; for that to happen, the keyword function would have to be the first thing in the statement. This turns what otherwise would be a declaration into an expression.

Block scoping

Block scoping is another pattern that you can use to manage scope and limit visibility. To update the previous example to use block scoping instead of an IIFE, we can simply add braces to define a block and use the let keyword within it. let, unlike var is block scoped, so it doesn't pollute the outer scope. One thing to note is that blocks are not scopes as such, but when you use a let or a const within a block, it is implicitly turned into a scope.

let teacher = "Kyle"

{
  let teacher = "Suzy";
  console.log(teacher);
}

console.log(teacher)  // "Kyle"

Block scoping and the new keywords can be used within places where it already makes sense to block scope things, like for instance when creating a temporary variable within an if statement or a for loop; essentially, you can use it to signal that some variable is local to a certain context.

You shouldn't, however, think of let as a replacement for var in every single context - there are still valid reasons for using the var keyword, too.

Choosing let or var

The following function has use cases for both let and var. Let is an obvious choice for the for loop, since it's scope is limited to the for loop only, not the entire function. For the result variable, however, var might be a more suitable choice since its scope is the entire function, not just a part of it. By using var, you are signalling to the reader that you intend to use this variable across the entire function, not just very locally, in the space of a few lines, as is the general intended use for let.

function repeat(fn, n) {
  var result;
  
  for (let i = 0; i < n; i++) {
    result = fn(result, i);
  }

  return result;

As another example for the use of var, consider the case where you need to use a variable in a place that would be considered a block, and you used a let, what would happen to that variable?

function lookupRecord(searchStr) {
  try {
    var id = getRecord(searchStr);
  } catch {
    var id = -1;
  }
  return id;
}

In the example above, if we used let instead of var, let would attach itself to try scope and catch scope, and would not be available in the return statement. Var, however, hoists itself and attaches itself to the function scope and is thus available. Unlike let, var can be redeclared and reused within the same scope, making it more flexible.

Explicit let block

Another thing to note about block scope is that if you are planning to use variables for just a few lines of the function and declare them at the top of the function, you can and possibly should wrap them in a block to avoid polluting the rest of the function's scope:

function formatString(str) {
  {
    let prefix, rest;
    prefix = str.slice(0, 3);
    rest = srt.slice(3);
    str = prefix.toUpperCase() + rest;
  }
  
  if (/^FOO/.test(str)) {
    return str;
  }
  return str.slice(4);
}

Hoisting

Hoisting as a term is really a metaphor for discussing lexical scope and not really a thing that exists in JavaScript. It is generally used when people want to think JavaScript only processes code once when executing, in stead of looking at code the way JavaScript actually handles it in two separate passes.

student;
teacher;
var student = "you";
var teacher = "Kyle";

In order see how the two pass system in JavaScript works, consider the following example:

var teacher = "Kyle";
otherTeacher();

function otherTeacher() {
  console.log(teacher);
  var teacher = "Suzy";
}

If you assume JavaScript to be a one-pass language, you would assume that the otherTeacher() function prints out Kyle, as this variable has been declared before the otherTeacher function gets called. But since JavaScript parses the content, it knows that another variable teacher has been declared in the scope of the otherTeacher function and tries to use that instead, resulting in undefined being printed out.

Don't believe that let doesn't hoist

It is commonly said that let doesn't hoist. One might be led to believe that this is true, if one considered the following piece of code that leads to a Temporal Dead Zone error:

{
  console.log(teacher);
  let teacher = "Kyle";
}

However, if let did not hoist, the following example should print out "Kyle":

var teacher = "kyle"
{
    console.log(teacher);
    let teacher = "Kyle";
}

Instead, you get the same error here. The reason for this is that when a variable is declared using var without giving it a value, the variable is initialized to undefined. Let and const, however, create a location for the variable, but do not initialize them to any value. The reason behind this is const; if a const variable was initialized as undefined and later assigned the value of 42 it would technically have two values, instead of having a one and only value, and this was chosen to be used with let as well.

Closure

The origins of closure predate computer science; the idea comes from lambda calculus. The mathematical definition, however, is not important to understand to be able to understand the JavaScript concept. Therefore, we take a look at closure from an observational perspective here.

But what is closure? Closure is when a function "remembers" its lexical scope even when the function is executed outside that lexical scope. Uh, what? So, if a function is defined in one scope, and for instance returned, the function still retains a linkage to that original location where it was defined.

function ask(question) {
  setTimeout(function waitASec() {
    console.log(question);
  }, 1000);
}

ask("What is closure?");

In the example above, when the function waitASec() runs, the ask() function has already finished which it closed over should have gone away. It didn't, however, because closure preserved it. So, the waitASec() function is closed over the variable question here, and that is the closure. Note, though, that JavaScript engines perform closure closure on a per scope basis, and not based on individual, closed over variables.

function ask(question) {
  return function holdYourQuestion() {
    console.log(question);
  }
}

var myQuestion = ask("What is closure?");

myQuestion();

Similarly above, the ask() function has finished by the time myQuestion() gets called, but it still has access to the variable question.

Closure works over variables, NOT over values. This means that you have a linkage to a variable and its current value, not a snapshot of what that value might have been at some point.

var teacher = "Kyle";

var myTeacher = function() {
  console.log(teacher);
};

teacher = "Suzy";
myTeacher(); // prints out Suzy

This generally bites people in the bum wnen dealing with looping mechanisms.

for (let i = 0; i <= 3; i++) {
  setTimeout(function() {
    console.log(`i: ${i}`)
  }, i * 1000)
}

In the example above, you might expect to see the values 1, 2, and 3 printed out. However, what happens is that the program prints out the number four three times. But why does this happen? When we use var, the same variable belongs to the whole for loop throughout its iteration. This problem can be solved with ES6 and the use of the let keyword. When using let, we get a brand new i for each iteration.

Module Pattern

Before we get into modules, let's define what aren't modules. The following is a common pattern in JavaScript:

var workshop = {
  teacher: "Kyle",
  ask(question) {
      console.log(this.teacher, question);
  },
}

workshop.ask("Is this a module?");

This would not be a module, but an example of the Namespace pattern which collects properties and functions under an object.

The idea of a module requires some form of encapsulation; the idea that some things can be public and some hidden; public and private. Modules encapsulate data and behaviour together and the state of a module is held by its methods through closure. The module pattern appeared around 2001 and looked like this:

var workshop = (function Module(teacher) {
  var publicAPI = { ask };
  return publicAPI;
  
  // ...
  
  function ask(question) {
    console.log(teacher, question);
  }
  
})("Kyle")

workshop.ask("It's a module, right?");

The module above is presented as an IIFE, and since IIFEs only run once, this module can be thought of as a sort of a singleton. It runs once, but thanks to closure the scope is not going to go away. And to create encapsulation, we use the publicAPI to return only the items that we want to be accessible from the outside.

We can also create modules using regular functions that we can call multiple times. Each time that the function gets called, it creates a new instance of that module, meaning that it is a factory function.

function WorkshopModule(teacher) {
  var publicAPI = { ask };
  return publicAPI;
  
  // ...
  
  function ask(question) {
    console.log(teacher, question);
  }
  
})("Kyle")

var workshop = WorkshopModule("Kyle");
workshop.ask("It's a module, right?");

ES6 Module syntax

  # workshop.mjs
  
  var teacher = "Kyle";

  export default function ask(question) {
    console.log(teacher, question);
  };
# ---
import ask from "workshop.mjs"; // named import syntax -> basically naming the default export as ask

ask("It's a default import, right?")


import * as workshop from "workshop.mjs";

workshop.ask("It's a namespace import, right?")

Objects

This keyword

A function's this keyword references the execution context for that call, determined entirely by how the function was called. The definition of the function does not matter at all to determining the this keyword. A this-aware function can thus have a different context each time it is called, which makes the function more flexible and reusable. In other words, the this keyword is JavaScript's version of dynamic scope.

JavaScript has four different ways of invoking a function, and each one of them is going to answer what the this keyword is differently.

This: implicit binding

In case of implicit binding, the this keyword points to the object with which the ask function was invoked.

var workshop = {
  teacher: "Kyle",
  ask(question): {
    console.log(this.teacher, question)
  }
}

workshop.ask("What is implicit binding?");  // Kyle What is implicit binding?

Implicit binding can be used to share a function between different contents:

function ask(question): {
  console.log(this.teacher, question)
}

var workshop1 = {
  teacher: "Kyle",
  ask: ask,
}

var workshop2 = {
  teacher: "Suzy",
  ask: ask,
}

workshop1.ask("How do I share a method?");
// Kyle How do I share a method?
workshop2.ask("How do I share a method?");
// Suzy How do I share a method?

This: explicit binding

Another way to resolve this would be to use the call method that takes as its first parameter a this keyword and uses it as the context for the function. So, we could define workshop1 and workshop2 without the ask method and do the following:

ask.call(workshop1, "Can I explicitly set the context?");
ask.call(workshop2, "Can I explicitly set the context?");

This: hard binding

When using callbacks, the this binding may get lost due to the way callbacks are handled.

var workshop = {
  teacher: "Kyle",
  ask(question) {
    console.log(this.teacher, question);
  },
};

setTimeout(workshop.ask, 10, "Lost this?");
// undefined Lost this?
setTimeout(workshop.ask.bind(workshop), 10, "Hard-bound this?");
// Kyle Hard-bound this?

In the first case, the workshop.ask is invoked by the browser by saying cb.call(window, "Lost this?"), invoking it in the global object context.

new keyword -- "constructor calls"

function ask(question) {
  console.log(this.teacher, question);
}

var newEmptyObject = new ask("What is 'new' doing here?");

When using the new keyword, we invoke a function with the this keyword pointing at a new, empty object. This achieves four things:

  1. We create a brand new empty object
  2. Link that object to another object
  3. We call the function with this set to the new object
  4. If the function does not return an object, it is assumed that 'this' is returned

Default binding

If none of the three cases described before are available, we use default binding.

var teacher = "Kyle"

function ask(question) {
  console.log(this.teacher, question);
}

function askAgain(question) {
  "use strict";
  console.log(this.teacher, question);
}

ask("What's the non-strict mode default?")
// Kyle What's the non-strict mode default?

askAgain("What's the strict mode default?")
// TypeError

In non-strict mode, JavaScript defaults to using a matching global object. The strict mode on the other hand marks the this, when no bindings exist, as undefined. So when we try to access a property of an undefined value, we get a type error.

The binding rule precedence is:

  1. is the function called by new?
  2. is the function called by call() or apply(). Note that bind() also uses apply()
  3. Is function called on a context object?
  4. DEFAULT: global object, except in strict mode.

Arrow functions and lexical this

var workshop = {
  teacher: "Kyle",
  ask(question) {
    setTimeout(() => {
      console.log(this.teacher, question);
    }, 100);
  },
};

workshop.ask("Is this lexical 'this'?")
// Kyle is this lexical 'this'?

An arrow function does not define a this keyword at all. If you put a this keyword inside an arrow function, it is going to behave as any other variable, and lexically resolve to some enclosing scope that does define a this keyword.

In the example above, the setTimeout's callback goes first to look at the ask() function to look for a this keyword. And the ask() functions this is defined when it is called, and in this case points to the workshop object.

Resolving this in arrow functions

Remember, objects are NOT scopes:

var workshop = {
  teacher: "Kyle",
  ask: (question) => {
    console.log(this.teacher, question);
  },
};

workshop.ask("What happened to 'this'?")
// undefined What happened to 'this'?

workshop.ask.call(workshop, "Still no 'this'?")
// undefined Still no 'this'?

Here, JavaScript starts looking for this from the nearest available scope, which - since object is not a scope - is the global scope. And since there's no this in the global scope, we get an undefined.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment