§5.2.

JavaScript

Some programming languages are the result of years of careful design and planning.[1] In contrast, Netscape hired Brendan Eich in 1995 to prototype the JavaScript language in just ten days.

There’s nothing wrong with creating a language in ten days. The short timeline is likely to be why the language has such a pragmatic design. This pragmatic design is perhaps the reason for its incredible success. However, more time spent planning might have reduced the idiosyncrasies in the language.

In this section, I will explore JavaScript in greater depth. Mastering the details will help you write simpler and clear code:

  1. You can exploit the underlying simplicity of JavaScript.

  2. You can avoid encountering the idiosyncrasies of JavaScript.

Values

In practice, there are six data types in JavaScript that you need to know about: [2]

  1. undefined

  2. string (e.g., 'hello', "hello" and `hello`)

  3. number (e.g., 1, 3.14, 2.99792e8) [3]

  4. boolean (true and false)

  5. function

  6. Object (e.g., {name: 'AIP', chapter: 5}) and null

Undefined and null

undefined and null are special values that typically indicate no result or a missing value.

In JavaScript, the value of any variable is initially undefined:

$ node
Welcome to Node.js
Type ".help" for more information.
> let place;
undefined
> place
undefined
> typeof place
'undefined'
> place = 'Sydney';
'Sydney'
> place
'Sydney'
> typeof place
'string'

In the example above, the variable place was undefined until it is set to be the string 'Sydney'.

JavaScript also has a null value that is similar to undefined. In practice, you might use null to represent values that are known to be empty but undefined for unknown or uninitialized values. For example, fax_number === undefined might indicate that it is not known whether you have or do not have a fax number, whereas fax_number === null might indicate it is known that you do not have a fax number.

Strings

In JavaScript, strings typically represent textual data (e.g., "This is a string!"). Behind the scenes, strings are sequences of 16-bit values. Text is Unicode data encoded into 16-bit values using an encoding called UTF-16 (see Chapter 11 for more information).

The length of a string is the number of 16-bit values. Usually, the length turns out to be the same as the number of characters (e.g., "Hello".length === 5 and "你好".length === 2). However, this is not always the case with Emoji and other rare characters (e.g., '😺'.length === 2).

Numbers

In JavaScript, all numbers are 64-bit floating-point numbers. In other words, JavaScript makes no distinction between integers (such as 1) and floating-point numbers (such as 1.0). [4]

The IEEE 754 standard defines the representation of 64-bit floating-point numbers. One bit stores the sign of a number (i.e., whether the number is positive or negative), 52 bits store the digits of the number (the significand) and 11 bits store the exponent (the mantissa).

64-bit floating-point provides ample precision for most applications. The 11 bit exponent means that JavaScript can represent numbers as large as 1.7976931348623157x10308 or as small as 5x10-324. The 52-bit significand means that JavaScript can exactly represent any integer from -9007199254740992 to 9007199254740992. However, JavaScript does not have unlimited precision. In the following transcript, you can see that numbers have limited precision.

$ node
Welcome to Node.js
Type ".help" for more information.
> 9007199254740992 + 2
9007199254740994
> 9007199254740992 + 1
9007199254740992
> 1.0000000000000002
1.0000000000000002
> 1.00000000000000012
1.0000000000000002
> 1.00000000000000011
1
>
Puzzle: Number representation

Use your understanding of JavaScript numbers to explain the output below.

$ node
Welcome to Node.js
Type ".help" for more information.
> "hello"[1]
'e'
> "hello"[1.5]
undefined
> "hello"[1.0]
'e'
> "hello"[1.00000000000000011]
'e'
>
Advanced puzzle: Precision

64-bit IEEE floating-point numbers reserve 52 bits for the significant. Why is 9007199254740992 equal to 253, rather than 252?

Tip
To solve this puzzle, you may need to research the representation of floating-point numbers in memory.

Booleans

Boolean values in JavaScript are either true or false.

JavaScript can convert any value to a boolean. This conversion occurs automatically in any expression expecting a boolean value (e.g., the condition in if, while, do …​ while and …​ ? …​ : …​).

For example, the following code is valid:

if (100) {
    console.log('100 is equivalent to true');
}

JavaScript uses the following rules to convert to boolean:

Value

Boolean equivalent

false, undefined and null

false

the number 0 or NaN

false

empty string ""

false

true

true

any non-0 or non-NaN number

true

any non-empty string (e.g., "hi")

true

any object (e.g., {})

true

JavaScript developers sometimes informally describe this conversion as 'truthiness': false, undefined, null, "", 0 and NaN are said to be 'falsy' values; everything else is equivalent to boolean true and is said to be 'truthy'.

Functions

JavaScript allows you to define your own functions:

function sayHello() {
    console.log('hello');
}

function square(x) {
    return x * x;
}

A function definition declares a variable (sayHello), creates a JavaScript function object and then assigns it to the variable.

This assignment can be performed explicitly by using a function expression:

let sayHello = function () {
    console.log('hello');
};

let square = function (x) {
    return x * x;
}

Since 2015 (the ECMAScript 6 standard), JavaScript has supported the “arrow” notation as a shorthand for function expressions:

let sayHello = () => { console.log('hello'); };

let square = (x) => x * x;
Tip
Some programming languages distinguish between functions and methods. The term method is used in object-oriented programming to refer to a function associated with an object. However, JavaScript does not make any fundamental distinction between functions and methods. Functions can be attached or detached to an object as required, so the terms function and method are interchangeable in JavaScript.

Objects

In JavaScript, objects are dictionaries (i.e., lookup tables) that map from keys to values:

let obj = {name: "Alice", birth: 1994};

The key name can be examined using a property accessor (dot notation):

obj.name

The use of dot notation (above) is identical and equivalent to using bracket notation (below):

obj["name"]

The value of a property can be anything: booleans, numbers, strings, other objects or even functions:

obj.greeting = () => { console.log("hello, world"); };

obj.greeting(); // This will print hello, world

In JavaScript, the keys of an object are always strings. If you use a boolean, a number or even an object as a key, it will first be converted into a string. In the transcript below, 1 is converted into the string "1" when used as a key. Similarly, both {} and {different: true} are converted into the string "[object Object]" when used as keys of obj:

$ node
Welcome to Node.js
Type ".help" for more information.
> obj = {name: "Alice", birth: 1994}
{ name: 'Alice', birth: 1994 }
> obj[1] = "Hello"
'Hello'
> obj["1"]
'Hello'
> obj[{}] = "Hello, again"
'Hello, again'
> obj[{different: true}]
'Hello, again'
> Object.keys(obj)
[ '1', 'name', 'age', '[object Object]' ]
>

Classes and inheritance

JavaScript has a class keyword. The following example defines and instantiates a Person:

// Define the Person class
class Person {
    constructor(firstName, lastName) {
        this.name = firstName + " " + lastName;
    }

    showName() {
        console.log("My name is " + this.name);
    }
}

// Use the person class
let alice = new Person('Alice', 'Nelson');
alice.showName();

JavaScript’s approach to classes and inheritance is unusual. Its approach is known as prototypal inheritance. JavaScript simulates inheritance using chains of objects linked by a prototype property.

The class keyword is known as syntactic sugar. There is no class type in JavaScript. Instead, a class definition is superficial syntax that makes the language easier to use and therefore “sweeter”. JavaScript translates the syntactic sugar into simpler features.

Internally, the syntactic sugar of the Person class is translated into function and prototype definitions:

class Person {
    constructor(firstName, lastName) {
        this.name = firstName + " " + lastName;
    }

    showName() {
        console.log("My name is " + this.name);
    }
}

…​is translated into…​

function Person(firstName, lastName) {
    this.name = firstName + " " + lastName;
}

Person.prototype.showName = function () {
    console.log("My name is " + this.name);
}

This translation has two parts:

  1. the constructor method of the class is translated into an ordinary JavaScript function,

  2. then the methods of the class are translated into properties of the .prototype of the function.

First, I will examine the constructor. In JavaScript, object constructors are functions and, vice versa, all functions are object constructors. The Person function can be invoked with or without the new keyword. Invoking Person with the new keyword instantiates a new object:

$ node
Welcome to Node.js
Type ".help" for more information.
> function Person(firstName, lastName) {
... this.name = firstName + " " + lastName;
... }
undefined
> new Person('Alice', 'Nelson')
Person { name: 'Alice Nelson' }
> Person('Bobby', 'Brady')
undefined
>

When the Person('Bobby', 'Brady') is invoked without new, the code is run as an ordinary function. The function has no return statement, so the result is undefined.

The JavaScript interpreter internally converts a new expression, as in new Person('Alice, 'Nelson'), into something equivalent to the following:

 let alice = new Person('Alice', 'Nelson');

…​is translated into…​

 // Save the current value for "this"
 var __backup = this;
 // Create an empty object
 this = {};
 // Setup the object, by invoking the function
 Person('Alice', 'Nelson');
 // Save the result
 let alice = this;
 // Restore the original value for "this"
 this = __backup;

In summary: the new keyword creates a new empty object ({}); the empty object is passed to the function via the this keyword.

The next aspect of the class translation is the .prototype. This is how JavaScript achieves prototypal inheritance.

Every function declared in JavaScript has a property .prototype that is initially an empty object.

The prototype can be configured in any way you wish:

Person.prototype.showName = function () {
    console.log(this.name);
}

Person.prototype.species = "Human";

Person.prototype["home"] = "Earth";

Normal invocation of the function does not use the prototype. The prototype is only used when the function is invoked as a constructor (i.e., using new).

Every object in JavaScript has a hidden internal property named [[Prototype]]. If JavaScript cannot find a property on an object, then it will keep searching for that prototype by loading [[Prototype]] and then searching for the property on the ‘prototype’.

Consider, for example, a simple object:

let bobby = {name: "Bobby Brady"};

What happens if we attempt to access bobby.species? There is no species property of bobby, so bobby.species is undefined.

Now, suppose we have some background information that is true for every person:

let person = {species: "Human", home: "Earth"};

Prototypal inheritance uses an internal link between the two objects. Instead of bobby.species returning undefined, the internal linkage is searched to check .species in the person object.

Object.setPrototypeOf(bobby, person) manually sets the prototype linkage. The setPrototypeOf function sets the hidden internal [[Prototype]] property of bobby. The following transcript demonstrates prototypal inheritance:

$ node
Welcome to Node.js
Type ".help" for more information.
> let bobby = {name: "Bobby Brady"}
undefined
> let person = {species: "Human", home: "Earth"};
undefined
> bobby.name
'Bobby Brady'
> bobby.species
undefined
> Object.setPrototypeOf(bobby, person)
{ name: 'Bobby Brady' }
> bobby.name
'Bobby Brady'
> bobby.species
'Human'
>

Note that bobby.species is no longer undefined after establishing the prototype chain. The prototype chain ensures that bobby.species is "Human" (via its prototype: the person object).

The prototype chain can be as deep as you wish. Following example shows a prototype chain configured so that bobby.breathes will search bobby, then person and finally animal for a match to the key "breathes".

$ node
Welcome to Node.js
Type ".help" for more information.
> let bobby = {name: "Bobby Brady"}
undefined
> let person = {species: "Human", home: "Earth"};
undefined
> let animal = {breathes: "Air"};
undefined
> bobby.breathes
undefined
> Object.setPrototypeOf(bobby, person)
{ name: 'Bobby Brady' }
> bobby.breathes
undefined
> Object.setPrototypeOf(person, animal)
{ species: 'Human', home: 'Earth' }
> bobby.breathes
"Air"
>

This brings me to the significance of .prototype on a function. Calling new in JavaScript automatically sets the prototype of the new instance to be the value of the function’s .prototype property.

Therefore, a more accurate translation of new to the underlying logic should include setting the prototype of the newly created instance to be the prototype:

 let alice = new Person('Alice', 'Nelson');

…​is translated into…​

 // Save the current value for "this"
 var __backup = this;
 // Create an empty object
 this = {};
 // Establish the prototype chain
 Object.setPrototypeOf(this, Person.prototype);
 // Setup the object, by invoking the function
 Person('Alice', 'Nelson');
 // Save the result
 let alice = this;
 // Restore the original value for "this"
 this = __backup;

Now, consider how JavaScript handles alice.showName().

First, showName is resolved. It isn’t part of alice, but it is found in the prototype chain as Person.prototype.showName. JavaScript invokes that method, using the original object (alice) as the value of this:

 alice.showName();

…​is translated into…​

 // Find the function in the prototype chain
 var __method = alice.showName
 // Save the current value for "this"
 var __backup = this;
  // Use the target object as "this"
 this = alice;
 // Invoke the function
 __method();
 // Restore the original value for "this"
 this = __backup;

The simple syntax for classes hides a great deal of underlying complexity and subtlety. As a developer, it is your choice to write simple code. By understanding how the mechanism works, you can avoid unnecessary complexity and understand why things break.

The internal mechanisms for classes and inheritance in JavaScript can be quite complex. However, in practice, you do not need to know these internals. The details of prototypal inheritance are not relevant in most ordinary situations that involve the JavaScript class keyword.

Puzzle: Prototypal inheritance

Is the following JavaScript code valid? Does it run without errors? What will it output? Why?

class Person {
    speak() {
        console.log("Hello");
    }
}
class Dog {
    speak() {
        console.log("Woof woof");
    }
}
class Cat {
    speak() {
        console.log("Meow meow");
    }
}

let tiger = new Dog();
let fluffy = new Cat();
let jan = new Person();
let greg = new Person();

Dog.prototype.speak = Person.prototype.speak;
Object.setPrototypeOf(greg, fluffy);

tiger.speak();
fluffy.speak();
jan.speak();
greg.speak();
Tip
You can experiment with this code by entering it directly into Node.js.
Puzzle: Binding

The following code does not work properly. Why not?

class Order {
    constructor(title, price, quantity) {
        this.title = title;
        this.price = price;
        this.quantity = quantity;
    }
    getValue() {
        return this.price * this.quantity;
    }
}

let pizzas = new Order("Margherita", 19.95, 2);
let calc = pizzas.getValue;
let total = calc();
console.log(total);

The following code (which uses the Order class again) makes use of .bind(…​). What does bind do and why does the code work?

let drinks = new Order("Coca Cola", 1.50, 5);
let drinksCalc = drinks.getValue.bind(drinks);
let drinksTotal = drinksCalc();
console.log(drinksTotal);

Equality

Equality (==) in JavaScript is particularly complex.

In JavaScript, 1 == 1 and "1" == "1" because these values are equal. However, equality is complicated when the types differ (addition/concatenation + in JavaScript has a similar problem).

Here is the table of outcomes for equality (tick-marks ✓ indicate true, blank cells indicate false):

==

false

0

""

true

1

"1"

"x"

NaN

null

undefined

{}

[]

false

0

""

true

1

"1"

"x"

NaN

null

undefined

{}

[]

Exercise: Explain ‘==’

The table of outcomes for equality (==) has structure, but it isn’t immediately obvious. For example, why is true == "1" but not true == "x"?

Before reading on, I encourage you to figure out a simple explanation for how == works in JavaScript. Is there a simple set of rules that can explain the table of outcomes above?

Here are the rules for equality (you might note the similarity to the rules for + from Chapter 4):

  1. Nothing is equal to NaN

  2. null and undefined are equal to themselves and each other, but nothing else

  3. If both operands are objects, arrays, compare the object identities (i.e., are they the same object instance in memory)

  4. Otherwise, convert any objects or arrays into a "primitive" value (i.e., undefined, null, boolean, number or string). In practice, this means that the interpreter uses the toString() method to convert objects and arrays into strings.

    • The default toString() method for an object ({}) returns "[object Object]"

    • The default toString() method for a list ([], [1,2,3], ["a","b","c"]) converts the elements into strings ("", "1,2,3", "a,b,c")

  5. Then, if both primitives are strings, perform a string comparison

  6. Otherwise, convert both sides to numbers and perform a numeric comparison

    • true is equivalent to 1

    • false is equivalent to 0

    • the empty string ( "" ) or a string of whitespace ( " " ) is equivalent to 0

    • other strings are interpreted as a number if possible (e.g., "33.5" is 33.5) and NaN otherwise (e.g., "x" is NaN)

Advanced exercise: Equality

Can you correctly predict which of the following are equal?

  • null == ["0"]

  • "true" == true

  • false == []

  • false == ["x"]

  • true == ["x"]

  • false == ["0"]

  • null == "null"

  • {} == "[object Object]"

  • {} == {}

  • "2" == true + true

  • "false" == "0"

Test your answers in Node.js and then attempt to explain it in terms of the rules for equality listed above.

Equality is difficult and, in many cases, counter-intuitive. However, as a professional developer, you can choose not to rely on these obscure possibilities. For example, you can ensure that you only use == to compare values of the same type.

A better option is to use the === strict equality operator. It was introduced in 1999 to resolve the confusion of the original == equality operator. The following table demonstrates strict equality comparisons: [5]

===

false

0

""

true

1

"1"

"x"

NaN

null

undefined

{}

[]

false

0

""

true

1

"1"

"x"

NaN

null

undefined

{}

[]

You will notice that it is vastly easier to remember and understand strict equality. There are no surprises. For this reason, professional JavaScript developers prefer the use of === strict equality over == equality.

Advanced exercise: Equality and identity

Explain the following output (i.e., why is x == y false but x == z true?):

$ node
Welcome to Node.js
Type ".help" for more information.
> x = {};
{}
> y = {};
{}
> z = x;
{}
> x == y
false
> x == z
true
>

1. For example, other mainstream languages such as Haskell, Scheme, C#.
2. The ECMA JavaScript standards also define Symbols and BigInt, but you are not likely to encounter these often.
3. You can also express numbers in binary (i.e., 42 === 0b101010), in hexadecimal (i.e., 42 === 0x2a) or even octal (i.e., 42 === 052).
4. Representing all numbers as 64-bit floating-point is an unusual feature of JavaScript (and Lua). Most other programming languages have distinct types for 32-bit integers, 64-bit integers, 32-bit floating-point numbers and 64-bit floating-point numbers.
5. In the case of {} and [], I use separate instances for the left and right operands.