Skip to content

Latest commit

 

History

History
594 lines (461 loc) · 23.1 KB

KyleSimpson_you-don't-know-js-yet-get-started.md

File metadata and controls

594 lines (461 loc) · 23.1 KB

Notes on You Don't Know JS Yet: Get Started

  • Official name of JS is ECMAScript (ES)

    • Since 2016 annual releases as ES2019, ES2020
  • Contrary to some perceptions there are not multiple versions of JS in the wild: there is just one and it's the version maintained by TC39 and ECMA

  • Various JS environments (browser JS engines, Node.js, etc.) add APIs into the global scope of JS programs to give environment specific capabilities (i.e. alert(), console.log, fs.write())

Typical paradigm-level code categories include procedural, object-oriented (OO/classes), and functional (FP):

  • Procedural style organizes code in a top-down, linear progression through a pre-determined set of operations, usually collected together in related units called procedures.
  • OO style organizes code by collecting logic and data together into units called classes.
  • FP style organizes code into functions (pure computations as opposed to procedures), and the adaptations of those functions as values.
  • Languages can be heavily slanted toward a paradigm
    • C: procedural
    • Java/C++: class-oriented/OO
    • Haskel: FP
  • JS is a multi-paradigm language

What's in an interpretation

  • interpreted script or compiled program (p.21)
  • JS source code is parsed before it is executed (23)
    • Spec requires early errors, statically determined (e.g. duplicate parameter names) to be reported before code starts executing
  • Sensible also to consider it to be a compiled language
  • Because it is compiled, we are informed of static errors before the code is executed.
    • Explained to draw a distinction with "interpreted" and "scripted" languages, which have often been regarded as less mature than "compiled" languages

Web Assembly (WASM)

  • Aims to reduce the parse and compile time of JS = performance gains

  • Parsing/compilation of WASM-targeted program happens ahead of time

    • JS engine is provided a binary-packed program, which is ready to be executed with very little processing
  • Another motivation is to bring non-JS programming languages (e.g. Go) to the web platform

  • All code is automatically defaulted to strict mode in ES6 modules (30)

JS is an implementation of the ECMAScript standard (version ES2019 as of this writing), which is guided by the TC39 committee and hosted by ECMA. It runs in browsers and other JS environments such as Node.js.

JS is a multi-paradigm language, meaning the syntax and capabilities allow a developer to mix and match (and bend and reshape!) concepts from various major paradigms, such as procedural, object-oriented (OO/classes), and functional (FP).

JS is a compiled language, meaning the tools (including the JS engine) process and verify a program (reporting any errors!) before it executes.

Declaring and using variables

  • let allows for more limited access to a variable than var
    • block scoping vs regular/function scoping
  • Common challenge that we should avoid var to favour the ES6 variables
    • Counterpoint: var usefully conveys that "this variable will be seen by a wider scope (of the whole function)"
    • That scoping may be appropriate, and the developer may want to use var to indicate that
  • const = cannot be reassigned
    • Not, cannot be changed
  • Advice to avoid const with objects because their values can be changed/mutated without the object itself being reassigned

Functions

function robsNamedFunction() {
    ...
}
  • function declaration because it appears as a statement by itself (rather than as an expression in another statement)
  • Association between the identifier (robsNamedFunction) and the function value occurs at compile time
let robsNamedFunction = function() {
    ...
}
  • function expression because the function is an expression that is assigned to a variable
  • Association between the identifier and the function value occurs at run time

It's extremely important to note that in JS, functions are values that can be assigned and passed around. Not all languages treat functions as values, but it's essential for a language to support the functional programming pattern

  • Curious about any implications of the compile vs. run phase of identifier association
    • Maybe affects static analysis?

Comparisons

  • === is often considered to check both the type and value
    • In fact, all comparisons check type and value, but === just doesn't allow for any type coercion
NaN === NaN;    //false
0 === -0;       //false
  • Two special values where the === operator is designed to "lie"
    • "Deep historical and technical reasons"
  • Can use Number.isNaN(...) or Object.is(...)
Object.is(0, -0);            // false
Object.is(-0, -0);           // true
Object.is(NaN, 0/0);         // true
  • Object.is(...) as the ==== check
  • With objects, a content-aware comparison is referred to as "structural equality"
    • In JS === provides a check of identity equality for objects and NOT structural equality

In JS, all object values are held by reference, are assigned and passed by reference-copy, and are compared by reference (identity) equality

  • To properly cover all edge cases in structural equality is very complex, which is why it doesn't exist in JS

    • For instance, stringifying a function doesn't take into account things like closure
  • Coercive comparisons: The == operator can be considered coercive equality

    • Simple enough to prefer ===, but you don't have a choice with < or >=, which will allow coercion first if types differ
var x = "10"
var y = "5"

x < y   // true!
  • If both values are strings, the relational operators will perform an alphabetical comparison
    • Could imagine this being a sneaky case where a numeric value is passed into a context as a string and a comparison yields an unexpected result
  • Two patterns for organizing code (data and behaviour): classes and modules

A class in a program is a definition of a "type" of custom data structure that includes both data and behaviors that operate on that data.

  • Classes define how the data structure works, but are not themselves concrete values
  • A class must be instantiated with the new keyword
  • Does React do this "under the hood" for class components? i.e. When I say <MyComponent />, is React instantiating an instance of that class?
class Page {
    constructor(text) {
        this.text = text;
    }

    print() {
        console.log(this.text);
    }
}

class Notebook {
    constructor() {
        this.pages = [];
    }

    addPage(text) {
        var page = new Page(text);
        this.pages.push(page);
    }

    print() {
        for (let page of this.pages) {
            page.print();
        }
    }
}

var mathNotes = new Notebook();
mathNotes.addPage("Arithmetic: + - * / ...");
mathNotes.addPage("Trigonometry: sin cos tan ...");

mathNotes.print();
  • Key point that the data (e.g. text string and pages array) are organized alongside their behaviors (e.g. print and addPage methods)
  • We can write a program without an organizing mechanism like a class, but it would be harder to reason about and maintain
    • Reminds me of some of the massive 600+ line files I've had to work with where you need multiple tabs open at various lines of the file
class Publication {
  constructor(title,author,pubDate) {
      this.title = title;
      this.author = author;
      this.pubDate = pubDate;
  }

  print() {
      console.log(`
          Title: ${ this.title }
          By: ${ this.author }
          ${ this.pubDate }
      `);
  }
}
class Book extends Publication {
    constructor(bookDetails) {
        super(
            bookDetails.title,
            bookDetails.author,
            bookDetails.publishedOn
        );
        this.publisher = bookDetails.publisher;
        this.ISBN = bookDetails.ISBN;
    }

    print() {
        super.print();
        console.log(`
            Publisher: ${ this.publisher }
            ISBN: ${ this.ISBN }
        `);
    }
}

class BlogPost extends Publication {
    constructor(title,author,pubDate,URL) {
        super(title,author,pubDate);
        this.URL = URL;
    }

    print() {
        super.print();
        console.log(this.URL);
    }
}
  • Book and BlogPost extend the general Publication class with more specific behavior
  • super() delegates the initialisation work to the parent class's constructor
  • Both child classes have a print() method that overrides the inherited method
    • polymorphism = both inherited and overridden methods can have same name
    • Inheritance allows children classes to cooperate with parent classes by accessing / using its behavior and data, while being organized in their own separate logical units (as a class)

Classic Modules

  • ES6 added a module syntax form ot native JS syntax, but another module pattern has been important and common previously

an outer function (that runs at least once), which returns an "instance" of the module with one or more functions exposed that can operate on the module instance's internal (hidden) data

function Publication(title, author) {
    var publicAPI = {
        print() {
            console.log(title, author)
        }
    };

    return publicAPI;
}

function Book(bookDetails) {
    var pub = Publication(bookDetails.title, bookDetails.author);
    
    var publicAPI = {
        print() {
            pub.print()
            console.log(bookDetails.publisher)
        }
    };

    return publicAPI;
}
  • Similar to classes, but with methods and data accessed as identifier variables in scope rather than via this.
  • All data and methods are public with a class, but a module factory function exposes public methods explicitly via the returned object, while other methods remain private inside the factory function
var robsBook = Book({
    title: "Rob's cool book",
    author: "Rob"
})

robsBook.print()
  • Usage is quite similar to a class, just that there's no new keyword and the module factory function is called directly

ES Modules

  • Introduced in ES6 to provide same general utility as classic modules
  • Implementation is very different though
  • Instead of a wrapping context that the factory functions provided: the file for the ESM is the wrapping context
    • ESMs are always module based: one file = one module
  • Use export keyword to add a variable or method to the ESM's public API definition
    • Defined in a module, but not exported = stays hidden
  • We don't "instantiate" an ES module
    • First import creates a single instance
    • All other imports just receive a reference to that same single instance
    • singleton

Iterator-consumers

  • The ... operator
    • Two symmetrical forms: spread and rest
  • Spread is an iterator-consumer
    • Official JS spec only allows spread into an array or a function call
      • Array spread
      • Function call spread
    • But object spread is common enough (needs Babel)
var myNewArray = [...previouslyDefinedIterable]
myGreatFunction(...previouslyDefinedIterable)
  • for...of is another ES6 iterator-consumer
for (let value of previouslyDefinedIterable){
  ...
}

Iterables

ES6 defined the basic data structure / collection types in JS as iterables

  • strings, arrays, maps, sets (and others)

  • Most all these will have keys(), values(), and entries() methods

  • A Map data structure has a default iteration method over its entries rather than its values

    • An entry = tuple (two-element array) with both a key and a value
// Given two DOM elements button1 + button2

var buttonNames = new Map();
buttonNames.set(button1, "Home")
buttonNames.set(button2, "Cart")

for (let [button, buttonName] of buttonNames) {
  button.addEventListener('click', function onClick(){
    console.log(`Clicked ${buttonName}`)
  })
}

Closure

Closure is when a function remembers and continues to access variables from outside its scope, even when the function is executed in a different scope.

  • Closure is an aspect of functions
  • To observe a closure, you must execute a function in a different scope than where it was originally defined
function giveMeAGreeting(greeting) {
  return function giveMeAName(name) {
    console.log(`${greeting}, ${name}!`)
  }
}

const hello = giveMeAGreeting("Hello")
const sup = giveMeAGreeting("Sup")

hello("Harold")
sup("Sandra")
  • Nice higher-order function example where the greeting value is saved in the instance of the inner function thanks to closure
    • The inner function giveMeAName closes over the greeting variable from its outer scope
    • hello and sup are functions that persist the greeting argument passed into giveMeAGreeting

When the giveMeAGreeting(...) function finishes running, normally we would expect all of its variables to be garbage collected (removed from memory)

  • But because the inner function instances are still alive, being assigned to hello and sup, their closures preserve the greeting variables
function counter(step = 1) {
  let count = 0
  return function increment(){
    count = count + step;
    return count
  }
}

const add100 = counter(100)
const add1 = counter(1)

add100();
add100();
add1();

const bigMoney = add100() //300
const broke = add1() //2
  • Instances of the inner function close over both the count and step values from the outer scope
  • Point here is that the variables within a closure are not a snapshot and can be changed

a direct link and preservation of the variable itself

function getData(url) {
  ajax(url, function onResponse(response){
    console.log(`Response from ${url}: ${response}`)
  })
}

Closure is most common when working with asynchronous code, such as with callbacks

  • getData finishes right away, but the url parameter variable stays alive as long as needed
for (let [index, button] of buttons.entries()) {
  button.addEventListener('click', function onClick(){
    console.log(`Button ${index} clicked`)
  })
}
  • Example where the outer scope isn't a function
  • But the click handler is a function, and it creates the closure that preserves the index variable
    • Only functions have closure

this keyword

  • Common misconceptions

    • this refers to the function itself
    • this points to the instance that a method belongs to
  • Functions have two key characteristics determining what they can access:

    • scope: the set of rules that controls how references to variables are resolved
      • When it is defined, a function is attached to its enclosing scope via closure
    • execution context: can be thought of as a tangible object whose properties are made available to a function while it executes
      • Exposed to the function via this
  • Scope is static

    • Contains fixed variables at moment and location a function is defined
  • Execution context is dynamic

    • Dependant on how a function is called
    • Not a fixed characteristic, but a dynamic one determined every time a function is called
  • The benefit of a this-aware function is the ability to flexibly re-use a function with different data from different objects

No context specified

function classroom(teacher) {
  return function study() {
    console.log(`${teacher} says to study ${this.topic}`)
  }
}

var assignment = classroom("Robert")
assignment() // Robert says to study undefined
  • Here, the inner function, study, is dependent on its execution context because it is a this-aware function
  • Without a context specified, the default context is the global object
    • globalThis.topic will equal undefined

Context via object

let homework = {
  topic: "JS",
  assignment: assignment
}
homework.assignment() // Robert says to study JS
  • A copy of the assignment function reference is set on the homework object
    • this for the function call = homework object

Context via call()

let otherHomework = {
  topic: "slam dunks"
}

assignment.call(otherHomework) // Robert says to study slam dunks
  • call() takes an object to set the this reference for the function call
  • A characteristic of objects

    • Provides rules for the resolution of property access
  • Helpful to think of prototypes as a form of linkage between objects

    • prototype chain = series of objects linked together via prototypes

The purpose of this prototype linkage is so that accesses against object B for properties/methods that B does not have, are delegated to object A

  • Notion of delegation is useful

  • Object.create() defines a an object prototypes linkages

    • Object.create(null) creates a standalone object without any of the built in JS object properties or methods
  • Another way to create an object with prototype linkage that used to be very common prior to ES6 is through "Prototypal 'Classes'"

this revisited

  • One of the main reasons this is dynamic based on how a function is called is so that prototype-delegated function calls maintain the expected this
let homework = {
  study() {
    console.log(`Study ${this.topic}`);
  }
};

let jsHomework = Object.create(homework);
jsHomework.topic = "JS";
jsHomework.study(); // "Study JS"

let dunkHomework  = Object.create(homework);
dunkHomework.topic = "slam dunks";
dunkHomework.study(); // "Study slam dunks"
  • Both jsHomework and dunkHomework prototype link to the same homework object
    • Because neither has a study method, they delegate up the chain to homework, which does
    • Cool thing to point out though is that this within study still resolves to the object which executed study
    • Example of dynamic this being determined based on execution context
    • Other languages may place this on homework, since that's where study is defined, but not JS

Unlike may other languages, JS's this being dynamic is a critical component of allowing prototype delegation, and indeed class, to work as expected!

Pillar 1: Scope and Closure

  • JS is lexically scoped
  • Many claim it isn't due to 2 characteristics not present in other lexically-scoped languages
    • hoisting: where all variables declared anywhere in the scope are treated as if they were declared at the beginning of the scope
    • function-scope var: var variables are function scoped even if they appear within a block

Closure is the natural result of lexical scope when the language has functions as first-class values, as JS does.

  • A function maintains access to to its original scope variables regardless of the scope it's executed in (this is closure)

Pillar 2: Prototypes

  • JS is one of only a few languages that lets you create objects directly and explicitly, without first defining their structure in a class

For many years, people implemented the class design pattern on top of prototypes

  • This is prototypal inheritance, by and large avoided in today's JS

  • ES6 class keyword "doubled-down" on the learning toward OO/class-style programming

  • This focus can obscure the potential of the prototype system

    • The ability of two objects to simply cooperate dynamically through sharing a this context
  • Classes are one patten you can build on top of the prototype system

    • But can also forget classes and just let objects cooperate using the prototype chain
    • Called behaviour delegation

    But class inheritance gets almost all the attention. And the rest goes to functional programming, as the sort of "anti-class" way of designing programs.

  • Bits of advice:

    • To help a dev team embrace new practices, let before and after code samples do much of the talking
    • Work on building consensus on why it's important to revisit and reconsider an approach
    • Push for decisions based on an analysis of code rather than authoritative personas
  • Primitive values are always assigned as value copies
  • Object values are always assigned by reference
    • reference: two or more variables point to the same value
    • Two or more variables can have copies of a reference to a single shared object

Primitives are held by value. Objects are held by reference

  • Interesting that other languages might allow you to choose between assigning/passing the value itself or a reference to the value
    • But not JS!
const myFunction = function(arg){
  ...
}
  • On the right side is an anonymous function expression
    • In the cases where an assignment uses =, ES6 will perform name inference to give myFunction.name = "myFunction"
    • But if the function expression is passed as an argument to another function call, there will be no name inference
    • Any errors will then have a stack trace identifying an anonymous function
const myFunction = function robsFunction(arg){
  ...
}
  • On the right side is a named function expression
    • myFunction.name = "robsFunction"

Most developers tend to be unconcerned with using anonymous functions. They're shorter and unquestionably more common

  • Argument on the other hand is that any function in our code has a purpose, and you should name it in order to describe that purpose
    • This prevents the next developer from having to evaluate the code in their head to identify its purpose
const myFunction = () => {...}

onClick(() => {...})
  • Arrow functions are syntactically anonymous
    • They can only have an inferred name if assigned, as in the first example
    • As a call argument, in the second example, they are only ever anonymous. This is a very common use case
  • Interesting challenge to it being used as "a new default"
    • It is new(ish)
    • It is shorter
    • But it has the drawback of being easily anonymous, which doesn't serve debugging well

This kind of function actually has a specific purpose (i.e., handling the this keyword lexically )