Avatar

👋🏻, I'm Julia.

Eloquent JavaScript

#javascript

25 min read

I recently finished reading Eloquent JavaScript. If you haven’t heard of it before, and are interested in learning JavaScript, I’d highly recommend checking this book out which you can read for free online. In my humble opinion, while this books with the basics of JavaScript, I think it’s much easier to comprehend if you have at least some practical experience of JavaScript.

Here are my notes.

Introduction

  • Keeping programs under control is the main problem of programming. The art of programming is the skill of controlling complexity.
  • [Numbers] Many numbers lose some precision when only 64 bits are available to store them. It’s important to be aware of this and treat fractional digital numbers as approximations, not as precise values.
  • [Numbers] NaN stands for “not a number” even though it is a value of number type.
    • e.g. (Infinity - Infinity) or 0 / 0
  • [Strings] JavaScript’s representation uses 16 bits per string element, which describes up to 2^16 different characters. However, Unicode defines about twice as many, at this point. Some characters therefore take up two “character positions” in JS strings.
  • Operators that use two values are called binary operators. Those that use one are unary operators.
    • The minus operator can be used as both a binary and unary operator.
  • There is only one ternary operator, which is the conditional operator. e.g. true ? 1 : 2
  • [Booleans] There is only one value in JS that’s not equal to itself - NaN. NaN denotes the result of a non-sensical computation, hence cannot be equivalent to any other non-sensical computation.
    • console.log(NaN == NaN) // → false
  • [Empty values] The difference in meaning between undefined and null is an accident of JS design, and doesn’t matter most of the time.
  • JS tries to convert values to the type it needs, which may not be what you want. This is type coercion.
    console.log(8 * null) // 0
    console.log("5" - 1) // 4
    console.log("5" + 1) // 51
    console.log("five" * 2 // NaN
    console.log(false == 0) // true
    • When making comparisons, it’s best to use === rather than == to prevent unexpected type conversions.
  • [Logical operators] The || operator returns the value to its left when it can be converted to true, or else returns the value on the right.
    • 0, NaN and "" count as false. Everything else counts as true.
  • [Logical operators] The && operators works the other way round. If the value on the left converts to false, it returns that value, otherwise it returns the value on the right.
    • Side note: This is why in React, we should only use && to render components conditionally if the left hand value actually deduces to a boolean.

Program Structure

  • A fragment of code that produces a value is called an expression. If an expression corresponds to a sentence fragment, a JS statement is a full sentence. A program is a list of statements.

  • [Bindings] Think of these as tentacles rather than boxes. They do not contain values, but rather just grasp them.

    let mood = 'light'
    console.log(mood) // light
    mood = 'dark'
    console.log(mood) // dark
  • A function is a piece of program wrapped in a value. Values given to functions are called arguments.

  • do vs while loops - the do loop always executes its body at least once. The test therefore appears after the loop.

    let yourName
    do {
      yourName = prompt('Who are you?')
    } while (!yourName)
    console.log(yourName)
  • Use break to immediately jump out of the loop. Use continue to continue with the loop’s next iteration.

  • Use counter += 1 (counter ++) and counter -= 1 (counter--) to increment counts.

Functions

  • Functions that don’t have a return statement returns undefined. These functions may be thought of as side effects.

  • Bindings declared with let and const are local to the block they are declared in. Bindings created with var are visible throughout the whole function they appear in, or throughout the global scope if declared outside a function.

    • Use var carefully to prevent pollution of global namespace.
  • Lexical scope refers to the definition space, not the invocation space. In JS, an expression’s definition environment determines the code permitted to access it. i.e. only code within an item’s lexical scope can access it.

  • Function declarations don’t require a semicolon after the function. They are also moved to the top of their scope and can be used by all code in that scope (hoisting).

    function square(x) {
      return x * x
    }
  • Function expressions are not hoisted. They therefore prevent the global scope from being overly polluted.

    let launchMissiles = function() {
      missile.launch();
    };
    
    // Note, because we used let, we can change the binding for launchMissiles
    if (safeMode) {
      launchMissiles = function(); // do nothing
    }
  • Computer stores the context of a program in the call stack. Every time a function is called, the current context is stored on top of this stack. Storing this stack requires space in the computer’s memory.

  • JS is lenient about the number of arguments you pass into a function. If you pass too many, the extra ones are ignored. If you pass too few, the missing ones are assigned the value undefined.

  • A closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

  • A function that calls itself is a recursive function.

    • It should have a base case which exits the function, and should call itself continuously whilst getting closer to the base case, to ensure it doesn’t overflow the stack.
    • In typical JS implementations, running through a simple loop is generally cheaper than calling a function multiple times.
    • Use recursion only if it genuinely makes the problem easier to solve.
  • Functions can be roughly divided into those that are called for their side effects and those that are called for their return value.

    • A pure function is a specific kind of value-producing function that not only has no side effects but also doesn’t rely on side effects from other code. When called with the same arguments, it always produces the same value.

Data Structures: Objects and Arrays

  • Properties that contain functions are generally called the methods of the value they belong to.

  • Remember: push and pop always relate to the last value in the stack. shift and unshift always relate to the first value.

  • The binary in operator tells you whether an object has a property with that name.

    let anObj = { left: 1, right: 2 }
    console.log('left' in anObj) // true
  • Use delete to remove a key (and its value) from an object.

  • Arrays are a kind of object specialised for storing sequences. typeof [] gives us “object”.

  • Numbers, strings and booleans are immutable. Objects are mutable.

    • Comparing different objects will return false even if they have identical properties. This is because the comparison is done by reference.
  • slice takes returns an array with the elements between the start and end indices. The start index is inclusive, the end index exclusive. When the end index is not given, slice takes all elements after the start index.

  • [Three-dots notation] To define a function that accepts any number of arguments, use the rest params (three dots before the function’s last parameter).

    function max(...numbers) {
      // numbers is an array
    }

    You can also use three-dot notation to spread an array.

    let numbers = [5, 1, 7]
    console.log(max(...numbers)) // 7
    
    let words = ['never', 'fully']
    console.log(['will', ...words, 'understand'])
    // ["will", "never", "fully", "understand"]
  • [JSON] To save data in a file or send data over the network, we need to serialise the data (i.e. convert it to a flat description). A popular serialisation format is JSON, which looks similar to JS’s way of writing objects except:

    • Property names must be surrounded by double quotes
    • Only simple data expressions are allowed - no function calls, bindings, computations or comments.
  • Use JSON.stringify and JSON.parse to convert data to and from this format.

Higher-Order Functions

  • Higher-order functions: Functions that operate on other functions, either by taking them as arguments or by returning them.
  • A filter function is a pure function - it does not modify the array it is given, but instead, builds up a new array with elements that pass the test.
  • Higher-order functions start to shine when you need to compose operations. Note however, that they make your code less readable (there’s a tradeoff).
  • Remember that JS strings are encoded as a sequence of 16-bit numbers. These are called code units.
    • UTF-16, the format used by JS strings describes the most common characters using a single 16-bit code unit, but uses a pair of two such units for others. This is generally considered a bad idea today, but something to be aware of. Emojis foor instance, use 2x 16-bit units.

Objects

Encapsulation

  • The core idea in object-oriented programming is to divide programs into smaller pieces and make each piece responsible for managing its own state.
  • Different pieces of such a program interact with each other through interfaces, limited sets of functions or bindings that provide useful functionality at a more abstract level (hiding precise implementation).
  • Properties that are part of the interface are called public. Others, which outside code should not be touching, are called private.
  • Specifying an interface tells everyone they are supposed to talk to your object only through that interface. The rest of the details that make up your object are now encapsulated (hidden behind the interface).
  • More than one type may implement the same interface. Code written to use an interface automatically knows how to work with any number of different objects that provide the interface. This is called polymorphism. (Read more in the subsection below.)

Polymorphism

  • Polymorphic code can work with values of different shapes, as long as they support the interface it expects.
    • e.g. a for/of loop can loop over several kinds of data structures like arrays and strings.

Methods

  • Methods are properties that hold function values.

    • If object.method() is called, the binding called this automatically points at the object is was called on.

    • If you want to pass this explicitly, you can use a function’s call method which takes this as a first argument.

      function speak(line) {
        console.log(`The ${this.type} rabbit says '${line}'`)
      }
      
      let hungryRabbit = { type: 'hungry', speak }
      
      speak.call(hungryRabbit, 'Burp!')
      // The hungry rabbit say 'Burp!'
    • Arrow functions do not bind their own this but can see the this binding of the scope around them.

Prototypes

  • In addition to their set of properties, most objects also have a prototype. A prototype is another object that is used as a fallback source of properties.
  • When an object gets a request for a property that it does not have, its prototype will be searched for the property, then the prototype’s prototype, and so on (prototypal inheritance).
  • You can use Object.create to create an object with a specific prototype.
let protoRabbit = {
  speak(line) {
    console.log(`The ${this.type} rabbit says '${line}'`)
  },
}

let killerRabbit = Object.create(protoRabbit)
killerRabbit.type = 'killer'
killerRabbit.speak('Attack!') // The killer rabbit says 'Attack!'

Classes

  • JS’s prototype system can be interpreted as an informal take on an object-oriented concept called classes. A class defines the shape of a type of object - what methods and properties it has.
    • Such an object is called an instance of the class.
  • Prototypes are useful for defining properties for which all instances of a class share the same value, such as methods.
  • Properties that differ per instance need to be stored directly in the objects themselves. This is what a constructor function does.
  • All functions, including constructors, automatically get a property called prototype, which by default holds a plain, empty object that derives from Object.prototype. You can overwrite it with a new object, or add properties to it.

Class Notation

  • The class keyword starts a class declaration and allows us to define a constructor and set of methods.
    • The constructor method definition is treated specially.
  • Like function, class can also be used in statements and expressions. When used as an expression, it doesn’t define a binding but just produces the constructor as a value. You can omit the class name in class expressions.
    let object = new (class {
      getWord() {
        return 'hello'
      }
    })()
    console.log(object.getWord()) // hello
  • [Overriding derived properties] Adding a property to an object overrides any existing properties with the same name in its prototype.

Maps

  • A map is a data structure that associates keys with values. Use the class Map to store a mapping that allows any type of keys.
  • Maps remembers the original insertion order of the keys. For other differences between Maps and Objects, check MDN.
  • Methods set, get and has come with the interface of the Map object.

Symbols

  • Symbols are values created with the Symbol function. Unlike strings, newly created symbols are unique - you can’t create the same symbol twice.
  • We can include symbol properties in object expressions and classes by using square brackets around the property name. This causes it to be evaluated.
    let stringObject = {
      [toStringSymbol]() {
        return 'a jute rope'
      },
    }
    console.log(stringObject[toStringSymbol]()) // a jute rope

Iterator Interface

  • The object given to a for/of loop is expected to be iterable. This means it has a method named with the Symbol.iterator symbol.
    let okIterator = 'OK'[Symbol.iterator]()
    console.log(okIterator.next()) // {value: "O", done: false}
    console.log(okIterator.next()) // {value: "K", done: false}
    console.log(okIterator.next()) // {value: undefined, done: true}

Getters, Setters and Statics

  • Properties access directly that hide a method call are called getters and are defined by writing get in front of the method name in an object expression or class declaration.

    • You can call the function directly without () since the function is now a property of the class.
    let varyingSize = {
      get size() {
        return Math.floor(Math.random() * 100)
      },
    }
    
    console.log(varyingSize.size) // 73
  • You can do something similar when a property is written to, using a setter.

    class Temperature {
      //...
      set fahrenheit(value) {
        this.celcius = (value - 32) / 1.8
      }
    }
    
    let temp = new Temperature(22)
    temp.fahrenheit = 86
    console.log(temp.celcius) // 30
  • You can attach properties directly to your constructor function, rather than to the prototype. These methods won’t have access to a class instance, but can be used to create instances (for example).

    • Inside a class declaration, use the static keyword
    class Temperature {
      //...
      static fromFahrenheit(value) {
        return new Temperature((value - 32) / 1.8)
      }
    }
    
    Temperature.fromFahrenheit(100)
    // allows us to create a temperature using Fahrenheit directly

Inheritance

  • In object-oriented programming, a class can inherit properties and behaviour from another class. JS’s prototype system allows us to create a new class, much like an old class, but with new definitions for some of its properties.
  • This is done using extends. The class the new class should be based on is called the superclass. The derived class is the subclass.
    class SymmetricMatrix extends Matrix {
      ...
    }
  • Inheritance, encapsulation and polymorphism are fundamental parts of OOP, however inheritance is controversial. Encapsulation and polymorphism can be used to separate pieces of code from each other, whereas inheritance ties classes together.

The Instanceof Operator

  • JS provides a binary operator called instanceof to allow us to determine whether an object was derived from a specific class.
  • The operator will see through inherited types.

Modules

  • A module is a piece of program that specifies what other pieces it relies on and which functionality it provides for other modules to use (its interface).
  • The relations between modules are called dependencies.
  • A package is a chunk of code that can be distributed (copied and installed). It may contain one or more modules and has info on which other packages it depends on.
  • NPM provides a place to store and find packages in a convenient way to install and upgrade them. It is (i) an online service where you can download/upload packages and (ii) a program (bundled with Node.js) that helps you install and manage them.
  • If you run npm publish in a director that has a package.json file, it will publish a package to the registry. Anyone can do this.

Evaluating data as code

There are several ways to take data (string of code) and run it as part of a current program.

  • eval will execute a string in the current scope. This is usually a bad idea because it breaks some of the properties that scopes normally have, such as being able to easily predict which binding a given name refers to.
  • Use the Function constructor which takes 2 arguments: a string containing a comma-separated list of argument names and a string containing the function body. It gets its own scope. It is what we need to build out a module system.
    let plusOne = Function('n', 'return n + 1;')
    console.log(plusOne(4)) // 5

CommonJS

  • The main concept in CommonJS modules is a function called require. This loads the module and returns its interface. Because the loader wraps the module code in a function, modules automatically get their own local scope.
    • All they have to do is called require to access their dependencies and put their interface in the object bound to exports.
  • To avoid loading the same module multiple times, require keeps a store (cache) of already loaded modules.

ECMAScript Modules

  • CommonJS modules work but its notation is slightly awkward (along with some other downsides).
  • ES modules was introduced where the main concepts of dependencies and interfaces remain the same, but the details differ. Instead of calling a function to access a dependency, you use the import keyword.
    • Similarly, the export keyword can be placed in front of a function, class or binding definition to export things.
  • The ES module’s interface is not a single value but a set of named bindings. When you import from another module, you import the binding, not the value, which means an exporting module may change the value of the binding at any time and the modules that import it will see its new value.
  • When there is a binding named default, it is treated as the module’s main exported value.

Building and bundling

  • Fetching a single big file tends to be faster than fetching lots of small ones. We can use bundlers to combine our modules back into a single big file before publishing to the Web.
  • Minifiers make JS programs smaller by automatically removing comments and whitespace, renaming bindings and replacing pieces of code with equivalent code so that the bundle size is reduced.

Asynchronous Programming

The central part of a computer, the part that carries out the individual steps that make up our programs, is called the processor. The speed at which something like a loop that manipulates numbers can be executed depends mostly on the speed of the processor. But many programs rely on external factors, and it’d be a shame to let the processor sit idle while that happens.

Asynchronicity

  • A thread is another running program whose execution may be interleaved with other programs by the operating system - since most modern computers contain multiple processors, multiple threads may even run at the same time, on different processors.
  • Browsers and Node.js makes operations that might take a while asynchronous, rather than relying on threads. This is generally considered a good thing, as programming with threads is difficult and complex.

Callbacks

  • One approach to async programming is to make functions that perform a slow action take an extra argument - a callback function. The action is started, and when it finishes, the callback function is called with the result.
  • However, asynchronicity is contagious. Any function that calls a function that works asynchronously must itself be asynchronous. This can lead to callback hell.

Promises

  • Instead of passing in a callback function, we can use a promise, an async action that may complete at some point and produce a value.
  • Use Promise.resolve() to create a promise. Use .then to get the result of a promise.
  • Use Promise.all to return a promise that waits for all of the promises in the array to resolve, then resolve to an array of the values that the promises produced (in the same order).

Async functions

  • Use the async keyword before a function or method name. When the function is called, it returns a promise.
  • Inside the async function, the keyword await can be put in front of an expression to wait for a promise to resolve, and only then continue the execution of the function. It freezes the function at this point, and resumes once it’s resolved.

Generators

  • Defining a function with function* makes it a generator. When you call a generator, it returns an iterator.

The Event Loop

  • Callbacks are not directly called by the code that scheduled them. Async behaviour happens on its own empty function call stack.
  • An event loop schedules such callbacks to be called when appropriate, one after the other, so that the execution does not overlap.
  • Promises always resolve or reject as a new event. Even if a promise is already resolved, waiting for it will cause your callback to run after the current script finishes, rather than right away.

JavaScript and the Browser

  • A network protocol describes a style of communication over a network. There are protocols for sending email, fetching email, sharing files etc. The Hypertext Transfer Protocol (HTTP) is a protocol for retrieving named resources (chunks of info like webpages).
    GET /index.html HTTP/1.1
  • Most protocols are built on top of other protocols. The Transmission Control Protocol (TCP) is a protocol that all internet-connected devices “speak”. A TCP connection works as follows:
    • One computer must be waiting or listening, for other computers to start talking to it
    • To be able to listen for different kinds of communication at the same time on a single machine, each listener has a number (port) associated with it.
  • The listening computer is called a server, and the connecting computer is called the client.

The Web

  • The World Wide Web (not the Internet as a whole) is a set of protocols and formats that allow us to visit web pages in a browser.
  • Each document on the Web is named by a Uniform Resource Locator (URL).
  • Machines connected to the Internet get an IP address. You can instead register a domain name for a specific address or set of addresses.
  • https://eloquentjavascript.net/13_browser.html
  • In HTML, an & and character followed by a name or character code and a ; is called an entity and will be replaced by the character it encodes.

HTML and JavaScript

  • You can load ES modules in the browser by giving your script tag a type=“module” attribute.

The Document Object Model

  • An HTML document is a nested set of boxes. The data structure the browser uses to represent the document follows this shape. This representation is called the Document Object Modal (DOM).
  • The global binding document gives us access to these objects. Its documentElement property refers to the object representing the <html> tag.

Propagation

  • DOM events propagate outward, from the node where it happened to the node’s parent and on to the root of the document. After all handlers registered on a specific node have had their turn, handlers registered on the whole window get a chance to respond to the event.
    • An event handler can call the stopPropagation method on the event object to prevent handlers further up from receiving the event.
  • Most event objects have a target property that refers to the node where they originated. You can use this to ensure you’re not accidentally handling something that propagated from the node you don’t want to handle.
  • For most types of events, the JS event handlers are called before the default behaviour takes place. You can therefore call preventDefault if you don’t want this normal behaviour to happen.

Events and the event loop

  • In the context of the event loop, browser event handlers behave like other async notifications. they are scheduled when the event occurs but must wait for other scripts that are running to finish before they get a chance to run.
  • If you really need to do some time-consuming thing in the background without freezing the page, use a web worker. A worker is a JS process that runs alongside the main script, on its own timeline.
  • To avoid the problems of having multiple threads touching the same data, workers do not share their global scope or any other data with the main script’s environment. Instead, you have to communicate with them by sending messages back and forth.

HTTP and Forms

  • When the <form> element’s method attribute is GET (or is omitted), the information in the form is added to the end of the action URL as a query string.
  • JS provides the encodeURIComponent and decodeURIComponent functions to encode/decode strings to and from the URL encoding format.
  • Using the POST method places the query string in the body of the request, rather than adding it to the URL.
  • GET requests should be used for requests that do not have side effects but simply ask for information. Requests that change something on the server, like creating a new record, should use other methods like POST.

Fetch

  • Browser JS can make HTTP requests via fetch. Calling fetch returns a promise that resolves to a Response object holding information about the server’s response, such as its status code and headers.
  • The headers are wrapped in a Map-like object that treats its keys (the header names) as case insensitive because headers names should not be case sensitive).
  • To get the actual content of a response, use the text method. Because the initial promise is resolved as soon as the response’s headers have been received, and because reading the response body might take a while longer, this again returns a promise.
    fetch('example/data.txt')
      .then((response) => response.text())
      .then((text) => console.log(text))
    // this is the content of data.txt
  • There’s a similar method called json that resolves to the value you get when parsing the body as a JSON object.

HTTP sandboxing

  • Browsers protect us by disallowing scripts to make HTTP requests to other domains. This can be annoying if we legitimately need to access other domains. For this, servers can include a header like this, in their response, to explicitly indicate to the browser that it’s ok for the request to come from another domain: Access-Control-Allow-Origin: *

The form as a whole

  • The <form> element has a property called elements that contains an array-like collection of the fields inside it.

Storing data client-side

  • The localStorage object can be used to store data in a way that survives page reloads. This object allows you to file string values under names.
  • A value in localStorage sticks around until it is overwritten, it is removed with removeItem or the user clears their local data.
  • Sites from different domains get different storage compartments, which means that data stored by a given website, can in principle, be read and overwritten only by scripts on that same site.
  • Browsers enforce a limit on the size of the data a site can store in localStorage.
  • There is sessionStorage which can also be used, but the content in this object is forgotten at the end of each session, which for most browsers is whenever the browser is closed.

Node

  • Node lets us run JS in a non-browser context. It was originally designed for network tasks to play the role of a node in a network.
  • All input and output in Node is done asynchronously unless you explicitly use the synchronous variant of a function e.g. readFileSync.
  • Error types in Node have code property “ENOENT”.

© 2016-2024 Julia Tan · Powered by Next JS.