This blogpost is a brief review of the book titled, Modern JavaScript for the Impatient, written by Cay S. Horstmann

My intention of going through this book is to crank out a quick dashboard that involves Java Script, React JS, Node components that interact with GBQ. I have always worked with R, Python and have been limited by the one page app restriction of shiny, streamlit. Of course, there are ways to make a one page look like a multiple page app but at its core, shiny and streamlit apps are single page apps. Transition to an app based on JavaScript and React JS will not be easy as one needs to spend considerable time understanding each of these tech stack elements as well as understand the way to make them work with each other. There are people who dedicate their entire working lives to master this part of tech stack. My attempt is more humble as I want to churn out merely a bare minimum working dashboard that involves JavaScript stack on front end and back end

Preface

The book is mainly targeted towards a programmer who wants to learn JS without getting bogged down with obsolete JS that one comes across in many videos, tutorials and books. Someone like me who has coded in R, Python, C++ might be put off, if they had to learn the historical baggage that comes with JS. In that spirit, the author mentions five golden rules that one must keep in mind to avoid “classic” features of JS

  • Declare variables with let or const, not var
  • Use strict mode
  • Know your types and avoid automatic type conversion
  • Understand prototypes, but use modern syntax for classes, constructors and methods
  • Don’t use this outside constructors or methods

About the author

From Cay S. Horstmann’s homepage

I grew up in Northern Germany and attended the Christian-Albrechts-Universität in Kiel, a harbor town at the Baltic sea. I received a M.S. in computer science from Syracuse University, and a Ph.D. in mathematics from the University of Michigan in Ann Arbor. I taught computer science at San Jose State University for almost thirty years and held visiting appointments at universities in Germany, Switzerland, Vietnam, and Macau. I was a “serial entrepreneur” before that was a thing, including a stint as VP and CTO of a dot com startup that went from three people in a tiny office to a public company. In my copious spare time I write books and develop online courses for beginning and professional programmers.

Values and Variables

Every value in JavaScript is one of the following types:

  • A number
  • The Boolean values false and true
  • The special values null and undefined
  • A string
  • A symbol
  • An object

The non-object types are collectively called primitive types

JS has two kind of comments. Single line comments start with //. Multiple line comments start with /* and end with */

You can store a value in a variable with the let statement

1
let counter = 0

In JS, variables do not have a type. You are free to store values of any type in any variable. If you do not initialize a variable, it has a special value undefined

Avoid two obsolete forms of variable declarations, with the var keyword and with no keyword at all

The name of a variable must follow the general syntax of identifers. An identifier consists of Unicode letters, digits and the underscore and dollar characters.

JS has no explicit integer type. All numbers are double-precision floating-point numbers.

JS has both functions and methods.

JS has two ways to indicate the absence of a value. When a variable is declared but not initialized, its value is undefined. This commonly happens with functions. When you call a function and fail to provide a parameter, the parameter variable has the value undefined. The null value is intended to denote the intentional absence of value.

String literals are enclosed in single or double quotes.

Template literals are strings that can contain expressions and span multiple lines. These strings are delimited by back-ticks

JavaScript objects are very different from those in class-based languages such as Java and C++. A JavaScript object is simply a set of name/value pairs or properties like this :

1
{name: 'RK', age:42}

Such an object has only public data and neither encapsulation nor behavior. The object is not an instance of any particular class. In other words, it is nothing like an object in traditional OOPS.

A property name is always a string. If the name doesn’t follow the rules of an identifier, quote it in an object literal

In JS, an array is simply an object whose property names are strings ‘0’, ‘1’, ‘2’ and so on. Strings are used because numbers can’t be used as property names. Let’s say you declare

1
const numbers = [1,2,3,'many']

This is an object with five properties : '0', '1', '2', '3' and 'length'. One needs to use the bracket notation to access the first four properties. For convenience, the argument inside the brackets is automatically converted to a string. This gives one the illusion of working with an array in a language such as Java or C++

An array can have missing elements:

1
const some_numbers = [, 2, 9] // No properties '0', '2'

A trailing comma does not indicate a missing element. For example [1,2,7,9,] is an object with four elements.

When an array needs to be converted to a string, all elements are turned into strings and joined with commas

JS like Java, has no notion of multidimensional arrays, but you can simulate them with arrays of arrays

JSON is a lightweight text format for exchanging object data between applications. It uses JavaScript syntax for object and array literals with a few restrictions:

  • Values are object literals, array literals, strings, floating point numbers and the values true, false and null
  • All strings are delimited by double quotes, not single quotes
  • All property names are delimited by double quotes
  • There are no trailing commas or skipped elements

The JSON format per se doesn’t support object references (although an IETF draft exists), hence JSON.stringify() doesn’t try to solve them and fails accordingly.

Object Literals from a medium article: In plain English, an object literal is a comma-separated list of name-value pairs inside of curly braces. Those values can be properties and functions. Here’s a snippet of an object literal with one property and one function.

1
2
3
4
5
6
var greeting = {
    fullname: "Michael Jackson",
    greet: (message, name) => {
        console.log(message + " " + name + "!!");
    }
};

All members of an object literal in JavaScript, both properties and functions, are public. The only place you can put private members is inside of a function.

Assigning an object literal to another variable only performs a shallow copy, which means you get the reference of the object instead of the actual value.

You cannot copy an object literal without manually copying all the values.

Lastly, you can mutate members of an object literal, meaning you can add and remove them as you please.

Object literal should be used if you want to create objects on the go with no intention of copying values to another object or maybe mapping one object to another.

If you will need to create multiple instances of a structure and perform operations based on predefined values, then you should use a function constructor

Destructuring is a convenient syntax for fetching the elements of an array or values of an object.

The left-hand side is not an object literal. It is a pattern to show how the variables are matched with the right-hand side. While destructuring, one can provide a default that is used if the desired value is not present in the object or array.

Control Structures

JavaScript, like Java and C++ differentiates between expressions and statements. An expression has a value. A statement never has a value. Instead, it is executed to achieve some effect.

1
let x = 2*3

The above is a statement whose effect is to declare and intialize the x variable. Such a statement is called a variable declaration. The simplest form of a statement is an expression statement. It consists of an expression followed by a semicolon. An expression statement is only useful for an expression that has a side effect.

Statements don’t have values, but the JavaScript REPL displays values for them anyway.

In JS, certain statements must be terminated with semicolons. The most common ones are variable declarations, expression statements, and nonlinear control flow. However JS will helpfully insert semicolons for you. The basic rule is simple. When processing a statement, the parser includes every token until it encounters a semi colon or an “offending token” - something that could not be part of the statement. If the offending token id preceded by a line terminator, or is a }, or is the end of input, then the parser adds a semicolon.

Never start a statement with ( or {. Then you don’t have to worry about the statement being considered a continuation of the previous line.

A semicolon is inserted after a nonlinear control flow statement( return, break, continue, throw, yield ) that is immediately followed by a line terminator.

Since semicolons are only inserted before a line terminator or a }, it is important that you specify a line terminator if you have multiple statements on the same line.

Equality of objects means that the two operands refer to the same object. References to different objects are never equal, even if both objects have the same contents

How does JS evaluate x==y ?

  • If the two operands have the same type, compare them strictly
  • The values undefined and null are loosely equal to themselves and each other but not to any other values
  • If one operand is a number and the other a string, convert the string to a number and compare strictly
  • If one operand is a Boolean value, convert both to numbers and compare strictly
  • If one operand is an object but the other is not, convert the object to a primitive type, then compare loosely

There are three ways in which for loop can be used

  • plain old for loop like one uses in Java
  • for of construct to look over an iterable
  • for in construct to loop over the keys

Functions and Functional Programming

One declares a function by providing the name of the function, name of the parameters and body of the function. One does not need to specify the types of function parameters.

A function may choose not to specify a return value, in which case the purpose of the function is mainly to create a side effect

One can use define a function literal using arrow

1
const max = (x,y) => max(x,y)

You provide the parameter variables to the left of the arrow and the return value to the right. If there is a single parameter, one need not enclose it by parenthesis

1
const multiply_by_two = x=> x*2

If arrow function is more complex, one can place the entire logic in a block

A function with free variables is called a closure. Free variables are the variables that are used in the code but are not declared as parameters or local variables

JS has its share of unusual features, some of which have proven to be poorly suited for large-scale software development. Strict mode outlaws some of these features. To enable strict mode, place the line use strict as the first non-comment line in your file

The spread operator spreads out the elements as if they had been provided separately in the call. The rest declaration causes a sequence of values to be placed in to an array

Every declaration is hoisted to the top of its scope. That is, the variable of function is known to exist even before its declaration, and space is reserved to hold its value. With let and const declarations, accessing a variable before it is declared throws a ReferenceError. In strict mode, named functions can only be declared at the top level of a script of function, not inside a nested block. As long as you use strict mode and avoid var declarations, the hoisting behavior is unlikely to result in programming errors

In Java, you can catch exceptions by their type. Then you can handle errors of certain types at a low level and others at a higher level. Such strategies are not easily implemented in JS. A catch clause catches all exceptions, and the exception objects carry limited information. In JS, exception handlers typically carry out generic recovery or cleanup, without trying to analyze the cause of failure.

The purpose of the finally clause is to have a single location for relinquishing resources that were acquired in the try clause, whether or not an exception occurred.

JavaScript, by default, does not support named parameters. However, you can do something similar using object literals and destructuring. You can avoid errors when calling the function without any arguments by assigning the object to the empty object, {}, even if you have default values set up.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function example({ arg1 = 1, arg2 = 2, arg3 = 3 } = {}) {
  return { arg1, arg2, arg3 };
}

function problem({failure = true}) { return failure; }

problem(); //TypeError: Cannot read property 'failure' of undefined

example({ arg2: 4, arg1: 2 }); // { arg1: 2, arg2: 4, arg3: 3 }

example(); // { arg1: 1, arg2: 2, arg3: 3 }

the var keyword only supports function scope. let on the other hand, allows block-scoped variables

A good example of variable hoisting is

1
2
const fac = n => n >1 ? n*fac(n-1):1
console.log(fac(8))

Object Oriented Programming

In JS, all properties are public, and they don’t seem to belong to any class other than Object.

An example of an object declaration is

1
2
3
4
5
6
7
harry = {
    name: 'Harry',
    salary: 90000,
    raise_salary: function(percent) {
        this.salary *= 1+ percent/100
    }
}

A prototype collects properties that are common to multiple objects.Here is a prototype object

1
2
3
4
5
6
const emp_prototype = {
    raise_salary: function(percent) {
        this.salary *= 1+ percent/100
    }

}

When creating an employee object, we set its prototype. The prototype is an “internal slot” of the object. That is the technical term used in the ECMAScript language specification to denote an attribute of an object that is manipulated internally without being exposed to JavaScript programmers as a property. One can read and write the internal slot with the methods Object.getPrototypeOf and Object.setPrototypeof

Lookup in the prototype chain is only used for reading property values. If you write to a property, the value is always updated in the object itself.

One can write a factor function that creates new object instances with a shared prototype. There is a special syntax for invoking such functions, using the new operator. By convention, functions that construct objects are named after whta would be the class in a class-based language.

1
2
3
4
5
6
7
8
function Employee(name, salary) {
    this.name = name
    this.salary = salary
}
Employee.prototype.raise_salary = function(percent) {
    this.salary *= 1+percent/100
}
let z3 = new Employee('RK','0')

When a call to the function is done via new operators, there are a bunch of this that take place:

  1. The new operators creates a new object

  2. The [[Prototype]] internal slot of that object is set to the Employee.prototype object

  3. The new operator calls the constructor function with three parameters, this, name, salary

  4. The body of the Employee function sets the object properties by using the this parameter

  5. The constructor returns, and the value of the new operator is the now fully initialized object

  6. The variable z3 is initialized with the object reference

    The new operator looks just like a constructor call, but it isn’t a class. It’s just a function. Every once in a while, you should remind yourself that a JS class is nothing more than a constructor function, and that the common behavior is achieved with prototypes.

Nowadays, JS has a class syntax that bundles up a constructor function and prototype methods in a familiar form.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Employee {
    constructor(name, salary) {
        this.name = name
        this.salary = salary
    }
    raise_salary(percent) {
        this.salary *= 1 + percent/100
    }

} const x4 = new Employee('RK',0)

One must definitely use the class syntax. The syntax gets a number of fiddly details right that you do not want to manage manually. Just realize that a JS class is a syntactic sugar for a constructor function and a prototype object holding the methods

Classes, unlike functions, are not hoisted. You need to declare a class before you can construct an instance.

A field is private when its name starts with #. A method is private if its name starts with a #

In a class declaration, you can declare a method as static. Such a method does not operate on any object. It is a plain function that is a property of the class.

In JS, as in Java, you use the extends keyword to express the relationship among the Employee and Manager classes.

1
2
3
class Manager extends Employee {

}

Behind the scenes, a prototype chain is etablished. The prototype of Manager.prototype is set to Employee.prototype. That way, any method that is not declared in the subclass is looked up in the superclass.

In Java and C++, it is common to define abstract superclasses or interfaces so that you can invoke methods that will be defined in subclasses. In JS, there is no compile-time checking for methods applications, and therefore, there is no need for abstract methods.

In a subclass constructor, you must invoke the superclass constructor, using super function. However if you do not supply a subclass constructor, a constructor is automatically provided.

To avoid trouble don’t use this inside functions defined with function. It is safe to use this in methods and constructors, and in arrow functions that are defined inside methods and constructors.

If you are using classic JS notation to create objects, one must specify everything as properties.

One thing that is rather peculiar about JS is that class is a cobbled up from a function by setting its prototype slot. If you want to add a bunch of common functions to class function, one must add them to the prototype slot

It is always better to use class syntax as it gets a number of fiddly details right that you do not want to manage manually. In any case JS class is a syntactic sugar for a constructor function and a prototype object holding the methods

If you want to add a method to a class, there are several ways one can do in JS. Here are a few ways

1
2
3
4
5
6
7
8
function create_greetable_1(str) {
    const result = new String(str)
    result.greet = function(greeting) {return `${greeting}, ${this}!`}
    return result

} const g1 = create_greetable_1('World') console.log(g1.greet('Hello'))

In the above example, one adds a property to the object literal. The issue with this is that the function is replicated in memory for all objects that instantiate the object literal. A better way is to use prototypes. There are two ways to add something to an existing prototype slot of any object

1
2
3
4
5
6
const create_greetable_2 = function (str) {
    this.result = new String(str)
}
create_greetable_2.prototype.greet = function(greeting) {return `${greeting}, ${this.result}!`}
const g2 = new create_greetable_2('World')
console.log(g2.greet('Hello'))
1
2
3
4
5
6
7
8
const greetP = {greet: function(greeting) {return `${greeting}, ${this.result}!`}}
const create_greetable_3 = function (str) {
    this.result = new String(str)
    Object.setPrototypeOf(this, greetP)
}

const g3 = new create_greetable_3('World') console.log(g3.greet('Hello'))

Finally one can use the modern JS syntactical sugar

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class create_greetable_4 {
    constructor(str) {
        this.result = new String(str)
    }
    greet(greeting) {
        return `${greeting}, ${this.result}!`
    }
}
const g4 = new create_greetable_4('World')
console.log(g4.greet('Hello'))

Numbers and Dates

All JavaScript numbers are “double precision” values, with a binary representation that occupies eight bytes. You can place underscores anywhere between digits to make the number more legible. The global variables Infinity and NaN denote the “infinity” and “not a number” values.

To format an integer in a given number to a string, one can use toString method

1
2
3
const n = 3434343
n.toString(16)
n.toString(2)

One can use parseInt and parseFloat functions to convert strings to integers and floats.

A few of the number parsing functions are parseInt, parseFloat, Number.parseInt, Number.parseFloat

Number.MIN_SAFE_INTEGER is $2^{53}+1$ and Number.MAX_SAFE_INTEGER is $2^{53}-1$. The rationale behind the above number is that there are 52 digits in the mantissa and one signed bit, a total of 53 bits, that can be used to represent integers in JavaScript.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Number.MAX_SAFE_INTEGER  is the  largest integer  which  can be  used safely  in
calculations.

For example, Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 is true — any integer larger than MAX_SAFE_INTEGER cannot always be represented in memory accurately. All bits are used to represent the digits of the number.

Number.MAX_VALUE on the other hand is the largest number possible to represent using a double precision floating point representation. Generally speaking, the larger the number the less accurate it will be.

One of the ways I tend to remember max safe integer is by linking it to one light year. One light year is about 9 trillion km, 9000 trillion meters. If you forget the units part, the 9000 trillion is the approximately the max safe number in JavaScript.

A big integer is an integer with an arbitrary number of digits. A big integer literal has a suffix n.

In JavaScript, time is measured in smoothed milliseconds from the epoch with a valid range of 100 million days in either direction.

Strings and Regular Expressions

A string is a sequence of Unicode code points. Each code point is an integer between zero and ox10FFF.

UTF-16 (16-bit Unicode Transformation Format) is a character encoding capable of encoding all 1,112,064 valid character code points of Unicode (in fact this number of code points is dictated by the design of UTF-16).

JavaScript stores strings as sequences of UTF-16 code units. The offset in a call refers to the UFT-16 encoding.

Template literals insert the values of the embedded expressions into the template string. One can customize the behavior of template literals with a tag function.

Calling str.split('') with an empty separator splits the string into strings that each hold a 16-bit code units, which is not useful if str contains characters above \u{FFFF}. Hence use a spread operator [...str] instead.

A regular expression literal is delimited by slashes

Arrays and Collections

  • It is better to stay away from Array constructor and use array literals
  • Property keys in an array are always strings
  • If you decrease the length, any element whose index is at least the new length get deleted
  • There is no requirement that an array has an index property for every index between 0 and length-1

To visit all elements of an array, you can use a for loop to visit all elements in order, or the for in loop to visit all index values. One can produce elements from a given array using slice method.

JavaScript API provides a Map class that implements the classic map data structure: a collection of key/value pairs. Every JavaScript object is a map, but there are advantages of using Map

  • Object keys must be strings or symbols, but Map Keys can be of any type
  • A Map instance remembers the order in which elements were inserted
  • Unlike objects, maps do not have a prototype chain
  • You can find out the number of entries with the size property

Maps, like all JavaScript collections, have methods keys, values and entries that yield iterators over the keys, values and key/value pairs.

A Set is a data structure that collects elements without duplicates. A set is considered as a map of [value, value] pairs. Both the keys and values methods yield an iterator over the values, and the entries method yield an iterator over [value, values] pairs.

Weak Maps and Sets are mentioned in this chapter but I have a better understanding of these objects after referring to this online post

Weak maps have no traversal methods, and the map objects are not iterable. If the property you want to monitor is binary, you can use a WeakSet instead of a WeakMap. The keys of weak maps and elements of the weak sets can only be objects and not primitive type values.

Internationalization

Dates, times, currencies, even numbers are formatted differently in different parts of the world. JavaScript provides convenient functions to handle such requirements.

In US, dates are displayed as month/day/year; Germany uses day/month/year whereas China uses year/month/day format.

A locale specifies the language and location of a user, which allows formatters to take user preferences into account. A locale consists of up to five components

  1. A language specified by two or three lowercase letters
  2. Optionally, a script specified by four letters with an initial uppercase that
  3. further specifies the usage
  4. Optionally, a country or region, specified by two uppercase letters or three digits
  5. Optionally, a variant. Optionally, an extension. Extensions describe local preferences for calendars, numbers, and so on.

Let’s say you have some text written by author in Switzerland. It is written in German language but it has currency mentioned as Swiss Franc and not Euros. JavaScript provides all locale-sensitive functions to handle all such text encodings.

toLocaleString function available in various JavaScript objects such as Number, String can be invoked to obtain localization.

When formatting date and time, there are many locale-dependent issues:

  • The names of months and weekdays should be presented in the local language
  • There will be local preferences for the order of year, month and day
  • The Gregorian calendar might not be the local preference for expressing dates
  • The time zone of the location must be taken into account

toLocaleDateString and toLocaleTimeString are additional functions that can be used to choose the type of display format one needs to.

For comparing strings that include unicode characters across languages, a simple comparison based on UTF-16 blocks is not enough. Hence JavaScript provides localCompare method of the String class.

Asynchronous Programming

JavaScript program runs in a single thread. In particular, once a function starts, it will run to completion before any other part of your program starts running. You know that no other code will corruput the data that your function uses. No other code will try to read any of the data until after the function returns. You never have to worry about deadlocks. However there are issues with having single threaded frameworks. If a program needs to wait for something else - it cannot do anything else. JavaScript provides asynchronous framework to work with all such situations. You specify what you want, and provide callback functions that are invoked when data is available or when an error has occurred.

Let’s say you fire an AJAX request to fetch an image, you can associate callback. Once the image is added, you do not have to worry about corruption by concurrent callbacks. The callbacks are never intermingled. They run one after another in a single JavaScript thread. However, they can come in any order.

In more complex situations, that involve some a callback that scans the contents of the image, another callback that adds the image to an object, etc. Each retrieval requires error handling, which leads to more callbacks. EWith a few levels of processing, this sort of programming style turns into “callback hell”

The Promise constructor has a single argument, a function that has two arguments: handlers for success and failure outcomes. This function is called the “executor” function

1
2
3
4
5
6
const myPromise = new Promise((resolve, reject) =>{
    const callback = (args) => {
        if(success) resolve(result) else reject(reason)
    }
    invokeTask(callback)
})

The programming style of using promises is very time consuming and is it better to use await/async syntax that makes working with promises much more natural. The await operator can only occur in a function that is tagged with the async keyword.

I think the relevant section for a JavaScript newbie like me is to go over the sections that cover async/await syntax and not worry about Promises

One can apply the async keyword to arrow functions, methods, named and anonymous functions and object literal methods.

I found a good good tutorial on Fetch API from which I understood the way to use async/await syntax. The only way to get a good idea of Promises is to practice and in that sense, the exercises given at the end of this chapter serve as a good starting point.

A promise is essentially an improvement of callbacks that manage all asynchronous data activities. A JavaScript promise represents an activity that will either be completed or declined. If the promise is fulfilled, it is resolved; otherwise, it is rejected.

JavaScript promises can have three states, pending, resolved and rejected. The pending state is the initial state that occurs when a promise is called. While a promise is pending, the calling function continues to run until the promise is completed, returning whatever data was requested. When a promise is completed, it ends in either the resolved state or the rejected state. The resolved state indicates the promise was successful and that the desired state is passed to the .then() method. The rejected state indicates that a promise was denied, and the error is passed to the .catch() method.

The expression

1
let value = await promise

waits for the promise to settle and yields its value.

Of course it is terrible idea to keep waiting in a JavaScript function. Indeed one cannot use await in a normal function. The await operator can only occur in a function that is tagged with the async keyword

The compiler transforms the code of an async function so that any steps that occur after an await operator are executed when the promise resolves.

This chapter took the longest to go over as I was not familiar with the nuts and bolts of async programming at all. There is a ton of practice I need to put in, before I can understand how this all works.

Modules

A module provides features for programmers, called the exported features. Any features that are not exported are private to the module. A module also specifies on which other modules it depends. When a module is needed, the JavaScript runtime loads it together with its dependent modules. It is important to understand the difference between module and class. A class can have many instances, but a module doesn’t have instances. It is just a container for classes, functions or values.

Node.JavaScript implements a module system that managed module dependencies. When a module is needed, it and its dependencies are loaded. That loading happens synchronously, as soon as the demand for the module occurs. The Asynchronous Module Definition standard defines a system for loading modules asynchronously, which is better suited for browser-based applications.

ECMAScript modules improve on both of these systems. They are parsed to quickly establish their dependencies and exports, without having to execute their bodies first.

Modules are different from plain “scripts”:

  • The code inside a module always executes in strict mode
  • Each module has its own top-level scope that is distinct from the global scope of the JavaScript run time
  • A module is only processed once even if it is loaded multiple times
  • A module is processed asynchronously
  • A module can contain import and export statements

There is so much to modules than covered in the chapter. However the content in this chapter should make any reader curious about the way modules work in other JavaScript based run time environments. I have also had to go through gone through a quick course on Udemy that talks about the details around NPM package manaer

I have skipped the last three chapters of the book and will probably revisit and reread the concepts mentioned, whenever a need arises.

Takeaway

This book is a great resource for someone who has some experience working with other programming languages and wants to get a quick understanding of JavaScript. By removing the excessive focus on browser based examples, the author has made it easy for others to transition to that world of JavaScript.

The book is easy on eyes and can be quickly consumed in a few sittings. Having said that, one gets the best out of the book by working through the exercises at the end of each chapter.

There are so many online courses on JavaScript but nothing beats the learning of reading a good book and soaking up the written material.