Graduate Program KB

Table of Contents

  1. JavaScript Foundations
  2. Rethinking Asynchronous JavaScript
  3. Functional-Light JavaScript
  4. The Recent Parts of JavaScript

JavaScript Foundations

Data Types

  • Primitives:

    • Number (includes integers and real numbers together)
    • String
    • Boolean
    • Undefined
    • Symbol
    • Null
  • Reference:

    • Object
    • Array
    • Function
  • Objects, arrays and functions are mutable even if assigned to a constant variable

Variables

  • const creates block-scoped variables that cannot be reassigned values
  • var creates function-scoped variables
  • let creates block-scoped variables
    const a = 10
    var b = 20
    
    {
        let b = 30
        console.log(b) // 30, 'b' references the local variable instead of global variable
    }
    
    console.log(b) // 20, 'b' is only referenced from the global variable
    a = 20 // Error, 'a' cannot be reassigned
    

Coercion

Refers to the automatic conversion of values during operations/comparisons of data

  • String concatenation (combining numbers and strings):

    • When operating on 2 values, if at least 1 is a string, then the numerical value is converted to a string
    let age = 50
    let sentence = 'I am ' + age + ' years old' // 'I am 50 years old'
    
    // String + String = String     Number + Number = Number
    // Number + String = String     String + Number = String
    
  • String to number:

    • Convert a string containing only digits to a number, otherwise, NaN is returned
    let x = '15' 
    let y = Number(x) 
    
    
  • Boolean:

    • Falsy values: ' ', 0, null, NaN, undefined, false
    • Truthy values: Anything that's not falsy, including { } and [ ]
  • Implicit:

    • Logical operators perform a numerical check if at least one value is a number
    • If both values are strings, an alphanumeric operation is performed
  • Equality:

    10 == '10' // TRUE! Allows coercion
    10 === '10' // FALSE! Disallows coercion
    

this

Binding depends on how a function is invoked. Arrow functions don't create a this
  • Implicit

    • Workshop object invokes the ask function, this is the object invoking it
    • Look to left of the dot whenever invoked
    let workshopObj = {
        teacher: 'Ben',
        function ask(question) {
            console.log(this.teacher, question)
        }
    }
    
    workshopObj.ask(question)
    
  • Explicit

    • call invokes the function and passes the context as the first argument, any other arguments passed correspond to the normal function arguments
    • apply is similar to call but other arguments are passed as a single array and it will spread the elements as arguments to the function
    • bind is similar to call but instead of invoking the function, it returns a new function with this binded to the context and it can be invoked later
    • In this case, workshopObj is passed as the context through the call method which this references
    function ask(question) {
        console.log(this.teacher, question)
    }
    
    ask.call(workshopObj, question)
    
  • new binding

    • this is bound to the new object being constructed
  • window binding

    • If no other rules apply, this is binded to the window
    • Errors are thrown if "use strict;" is set

Execution

  • Global context contains a global object (window for browser and global for node)
  • this references the context object
  • When variables, functions and classes are declared, they are stored in the object of the current context and moved to the top of their scope (hoisting)
  • Variables are initially assigned undefined by default
  • When functions are called, it creates an arguments object which is similar to the global object but localised to the scope
  • The argument object stores any parameters passed in as local variables
  • When checking for a reference to a variable, it checks the current context object before scoping out until it reaches the global context

Prototyping and Inheritance

  • The parent constructor passes its fields to the child

    function Worker(name) {
        this.name = name
    }
    
    Worker.prototype.introduceMe = function(message) {
        console.log(this.name, message)
    }
    
    function Teacher(name, subject) {
        Worker.call(this, name)
        this.subject = subject
    }
    
    const worker = new Teacher('Jennie', 'Maths')
    
  • Must set the prototype before adding methods

    Teacher.prototype = Object.create(Worker.prototype)
    Teacher.prototype.sayJob = function() {
        this.introduceMe(`I teach ${this.subject}`)
    }
    
  • In ES6, can just use extends

    class Teacher extends Worker {
        constructor(name, subject) {
            super(name)
            this.subject = subject
        }
    }
    

Rethinking Asynchronous JavaScript

Parallel vs Async

  • Asynchronicity: Perform one task at a time but multiple tasks are executed within the same timeframe
  • Parallelism: Utilisation of multiple threads in an OS, able to run tasks at same time regardless of order
  • Concurrency: Two higher level tasks happening within the same timeframe

Callback

  • Callbacks are continuation of code: A part of the program executes, then another part executes later

    function doTask(callback) {
        callback();
    }
    
    const doLater = () => { ... };
    doTask(doLater);
    
  • Inversion of control:

    • To give control of another part of the program to execute some code
    • When you invoke a function that accepts a callback argument, that function is in control of executing the callback function later
    • Issues: It may or may not execute, it may execute too few or too many times, it may execute too early or late, it may lose context...

Thunks

  • Thunks are a pattern for using callbacks to typically delay executions. It creates a function that outputs a value without needing parameters.

  • For synchronous, thunk is just a function that has everything it needs to return a value

    function add(x, y) {
        return x + y;
    }
    
    let thunk = function() {
        return add(10, 10);
    };
    
    thunk();
    
  • For asynchronous, thunk is a function that doesn't need any arguments except a callback function to return a value

    function addAsync(x, y, callback) {
        setTimeout(function() {
            callback(x + y);
        }, 1000);
    }
    
    let thunk = function(callback) {
        return addAsync(10, 10);
    };
    
    thunk(function(sum) {
        console.log(sum);
    });
    

Promises

  • Promises represent a placeholder for a future value

    • Solves inversion of control issue by providing completion events for asynchronous tasks
    function finish() {
        chargeCreditCard(purchaseInfo);
        showThankYouPage();
    }
    
    function error(err) {
        logStatsError(err);
        finish();
    }
    
    let promise = trackCheckout(purchaseInfo);
    promise.then(finish, error);
    
    • Even though promises use callbacks, there's an increased level of trust
    • Promises will only resolve once, having either a success or error outcome and becomes immutable once resolved
  • The flow control design for promises is to chain them together so they return subsequent promises to the success handler

    doFirstThing()
    .then(function() {
        return doSecondThing();
    })
    .then(function() {
        return doThirdThing();
    })
    .then(complete, error);
    
  • Dynamically chain promises:

    [ ... ]     // Input array
    .map(data)
    .reduce(
        function combine(chain, promise) {
            return chain
                .then(function() {
                    return promise;
                })
            .then(output);
        }, Promise.resolve()
    )
    .then(function() {
        output("Complete!");
    });
    
  • Abstractions:

    • Promise.all takes in an array of promises which must all successfully complete in parallel before moving on
    • Promise.allSettled takes in an array of promises and returns their results once settled
    • Promise.race takes in an array of promises and only return the one that finishes first regardless if it succeeds or fails
    • Promise.any takes in an array of promises and returns the first successful task or an AggregateError if all tasks fail
    • Can append .not to use more combinations of patterns
    • What if a promise won't settle? Pass in another promise which sets up a timeout function that times out promises after some time
  • Asynquence (ASQ) is an open source library for adding abstraction methods to make sequencing and gating promises easier

Generators

  • New type of function in ES6, Generator(yield)

    • In asynchronous programming, generators solve non-sequential and non-reasonable issues of callbacks
    • When a generator is called, it returns an iterator which steps through the generator, pausing anything the yield keyword is encountered
    function* generator() {
        console.log('Hello');
        yield;
        console.log('World');
    }
    
    let iterator = generator();
    iterator.next();    // 'Hello'
    iterator.next();    // 'World'
    
  • If yield is called with a value, that value will be returned to the interator along with a 'done' boolean value

  • Enables messages to be sent out of the generator

    function* main() {
        yield 1;
        yield 2;
        yield 3;
    }
    
    let iterator = main();
    iterator.next();    // { value: 1, done: false }
    iterator.next();    // { value: 2, done: false }
    iterator.next();    // { value: 3, done: false }
    iterator.next();    // { value: undefined, done: true }
    
  • Example:

    • coroutine is a helper function wrapping around a generator to produce an iterator. It returns a function when invoked, returns the next value in the iterator
    • Below, a generator is passed to coroutine and calling run will iterate
    function coroutine(generator) {
        let iterator = generator();
        return function() {
            return iterator.next.apply(iterator, arguments);
        };
    }
    
    let run = coroutine(function*() {
        let x = 1 + (yield);
        let y = 1 + (yield);
        yield (x + y);
    });
    
    run();
    run(10);
    console.log('Meaning of life: ' + run(30).value);
    
    • When the first run is invoked, the yield expression can't be completed so it's paused until something resumes it
    • When the second run is invoked, it passes a value of 10 which can complete the yield expression. Therefore, the generator is resumed
    • After evaluating x, there's another yield expression to calculate y, the generator pauses again
    • Within the console.log, run is called again with a value of 30 which resumes the generator again
    • And for the final yield expression, both x and y are already defined so it can be evaluated straight away
  • Asynchronous generators

    • yield will block a generator while an async call is being executed
    • Values returned from the async call can be passed back to the generator
    • Pause and resume cycle to sequentially trigger async operations
    function getData(data) {
        setTimeout(function() { run(data); }, 1000);
    }
    
    let run = coroutine(function* () {
        let x = 1 + (yield getData(10));
        let y = 1 + (yield getData(30));
        let answer = 1 + (yield getData('Meaning of life: ' + (x + y)));
        yield answer;
    });
    
    run();
    
  • Inversion of control issue (somebody can call iterator.next unexpectedly), can be solved by yielding promises instead

Events + Promises

  • Promises work well with single occurring asynchronous events. But, if there was a stream of events, promises are difficult to use because it can only be resolved once
  • Observables are adapters hooked onto an event source that produces a promise every time a new event comes through
  • Observables don't currently exist in JS, but third-party libraries such as RxJS have begun implementing them

Communicating Sequential Processes

  • CSP is a pattern where individual processes running separately have access to a blocking communicating channel where they can share messages
    const channel = chan();
    
    function* process1() {
        yield put(channel, "Hello");
        const message = yield take(channel);
        console.log(message);
    }
    
    function* process2() {
        const greeting = yield take(channel);
        yield put(channel, greeting + " World");
        console.log("Completed!");
    }
    

Functional-Light JavaScript

Functional Purity

  • Function: A routine that can accepts arguments and returns one or more values

    • It's the semantic relationship between input and computed output
  • Procedure: A routine that can accept arguments but doesn't return values

  • Side effects cause functions to become impure:

    • Caused by indirect inputs and outputs
    • Try avoid side effects where possible by not accessing an external environment to calculate the output of the function
    • A function is pure if for some input, it returns a specific output, no matter how many times it's invoked
    • Side effects make functions harder to test, because the output can be less predictable
    • List of potential side effects:
      • I/O (console, files, etc.)
      • Database storage
      • Network calls
      • DOM
      • Timestamps
      • Random numbers
      • CPU Heat/Time Delay
  • Constants are external to the function but within the paradigm of functional programming if used correctly

    • Constants behave as a constant, value is not re-assigned anywhere else
    • Obvious to the reader that a function is referencing a constant value, has context of the program
      • Set the constant near where functions are declared (reducing surface area)
  • Extracting impurity

    function addComment(userID, comment) {
        let record = {
            id: uniqueID(),     // Side effect
            userID,
            text: comment
        };
    
        commentsList.appendChild(comment);  // Side effect
    }
    
    addComment(42, "Comment");
    
    function addComment(userID, commentID, comment) {
        let record = {
            id: commentID,
            userID,
            text: comment
        };
    
        return buildCommentElement(record);
    }
    
    let commentID = uniqueID();
    let element = addComment(42, commentID, "Comment");
    commentsList.appendChild(comment);
    
  • Containing impurity

    function insertExtraData(newData) {
        let data = API();   // Side effect
    
        if (data === null) {
            return null;
        }
    
        data.push(newData)  // Side effect
    
        return data;
    }
    
    function insertExtraData(newData) {
        let data;
        
        getData(data, newData);
    
        return data;
    }
    
    function getData(data, newData) {
        data = API();
            
        if (data === null) {
            return;
        }
    
        data.push(newData);
    }
    

Argument Adapters

  • The shape of a function is described by the number and types of data passed to it, and the same applies for the output

    • Unary function, one input and one output
    • Binary function, two inputs and one output
    • The method signature is important, it allows other functions to interact with each other by keeping the arguments small
  • Adapt a function to change it's number of arguments

    function unary(fn) {
        return function one(arg) {
            return fn(arg);
        };
    }
    
    function binary(fn) {
        return function two(arg1, arg2) {
            return fn(arg1, arg2);
        };
    }
    
    function f(...args) {
        return args;
    }
    
    let one = unary(f);
    let two = binary(f);
    
    one(1, 2, 3, 4);    // [1]
    two(1, 2, 3, 4);    // [1, 2]
    
  • Flip adapter to switch the position of arguments

    • Can partially or fully reverse the arguments
    function flip(fn) {
        return function flipped(arg1m arg2, ...args) {
            return fn(arg2, arg1, ...args);
        };
    }
    
    function f(...args) {
        return args;
    }
    
    let flipped = flip(f);
    
    flipped(1, 2, 3, 4);    // [2, 1, 3, 4]
    
  • Spread adapter to deconstruct an argument

    function spreadArgs(fn) {
        return function spread(args) {
            return fn(...args);
        };
    }
    
    function f(x, y, z, w) {
        return x + y + z + w;
    }
    
    let spread = spreadArgs(f);
    
    spread([1, 2, 3, 4]);   // 10
    

Point-Free

  • Point-Free functions are definitions of functions without needing its inputs

  • Equational reasoning can be used to determine where two functions have compatible shapes

    • If they have the same shape, can pass one function into the other function interchangeably
  • Refactor functions to Point-Free functions by using adapters

    function isOdd(n) {
        return n % 2 == 1;
    }
    
    function isEven(n) {
        return !isOdd(n);
    }
    
    isEven(4);
    
    function not(fn) {
        return function negated(...args) {
            return !fn(...args);
        };
    }
    
    function isOdd(n) {
        return n % 2 == 1;
    }
    
    let isEven = not(isOdd);
    isEven(4);
    
  • Advanced Point-Free

    function mod(y) {
        return function forX(x) {
            return x % y;
        };
    }
    
    function eq(y) {
        return function forX(x) {
            return x === y;
        };
    }
    
    let mod2 = mod(2);
    let eq1 = eq(1);
    
    function isOdd(x) {
        return eq1(mod2(x));
    }
    
    function compose(fn2, fn1) {
        return function composed(x) {
            return fn(fn1(x));
        };
    }
    
    let isOdd = compose(eq1, mod2);
    let isOdd = compose(eq(1), mod(2));
    

Closure

  • Closure is when a function "remembers" the variables around it, even when that function is executed elsewhere

  • Closures are not necessarily pure, they could return different outputs for an input

    function counter() {
        let count = 0;
        return function increment() {
            return count++;
        };
    }
    
    let inc = counter();
    inc();      // 1
    inc();      // 2
    
  • Lazy and eager function execution

    • In the counter example, the work is deferred (lazy) until inc was invoked
    • Everytime it's invoked, the work is performed again
    • If the count was incremented outside declaring increment, that is eager evaluation
    • The trade-off is the work performed is unnecessary if inc is never called
  • Memoization is caching the result of a function for a given input

    • Reduces computation time
    • Increased memory usage for storing results
    function repeat(count) {
        return memoize(function padAs() {
            return "".padStart(count, "A");
        });
    }
    
    let A = repeat(10);
    A();    // "AAAAAAAAAA", store result
    A();    // "AAAAAAAAAA", not computed again
    
  • Function signatures should go from general to specific, which allows functions to become specialised easier

  • Partial application is a way to obtain specialised functions by specifying the arguments beforehand

    • Preset some arguments beforehand, and receives the rest on the next call
    function ajax(url, data, callback) { ... };
    
    let getCustomer = partial(ajax, API);
    let getCurrentUser = partial(getCustomer, { id: 42 });
    
    getCustomer({ id: 42 }, renderCustomer);
    getCurrentUser(renderCustomer);
    
  • Currying is defining functions by passing in arguments then returning specialised or reshaped functions

    • Doesn't preset arguments, arguments are received one at a time through chaining
    • Strictly curried functions are chained unary functions
    • Loosely curried function can take multiple arguments
    let ajax = curry(
        3,
        function ajax(url, data, callback) { ... };
    );
    
    // Strict currying
    ajax(API)({ id: 42 })( renderCustomer );
    // Loose currying
    ajax(API, { id: 42 })( renderCustomer );
    

Composition

  • Composition is the abstraction of setting the output of one function as the input to another function

  • Functions are evaluated in right-to-left order, or building from the inner-most function

    function minus2(x) { return x - 2; }
    function triple(x) { return x * 3; }
    function increment(x) { return x + 1; }
    
    function shippingRate(x) {
        return minus2(triple(increment(x)));
    }
    
    function composeThree(fn3, fn2, fn1) {
        return function composed(x) {
            return fn3(fn2(fn1(x)));
        };
    }
    
    let f = composeThree(minus2, triple, increment);
    let g = composeThree(increment, triple, minus2);
    
    f(4);   // 13
    g(4);   // 7
    
  • Piping is the opposite of composition, the functions are evaluated from left-to-right instead

    function composeThree(fn3, fn2, fn1) {
        return function composed(x) {
            return fn1(fn2(fn3(x)));
        };
    }
    
    let f = composeThree(minus2, triple, increment);
    
    f(4);   // 7
    
  • Associativity, no matter how the composed functions are combined, they will return the same result

    function composeTwo(fn2, fn1) {
        return function composed(x) {
            return fn2(fn1(x));
        };
    }
    
    let f = composeTwo(
        composeTwo(minus2, triple),
        increment
    );
    
    let g = composeTwo(
        minus2,
        composeTwo(triple, increment)
    );
    
    f(4);   // 13
    g(4);   // 13
    
  • Currying can be applied to composition

    function sum(x, y) { return x + y; }
    function triple(x) { return x * y; }
    function divBy(y, x) { return x / y; }
    
    divBy(2, triple(sum(3, 5)));    // 12
    
    sum = curry(2, sum);
    divBy = cury(2, divBy);
    
    composeThree(divBy(2), triple, sum(3))(5);  // 12
    

Immutability

  • const cannot be re-assigned values

  • Objects are mutable but can be made read-only if the function Object.freeze(object) is used

    • Does not apply to objects within objects unless you freeze them
  • When working with data structures such as objects or arrays, work with copies so the original data is not mutated

    • Programs might use the original data in other functions
    • Should always keep a copy of the original data anyway just in case

Recursion

  • Recursive functions invoke itself to solve subproblems of a larger problem

    • Define at least one base case which indicates when to backtrack
  • With each call, the stack size increases

    • Keeps a reference to the previous invocation (unless the recursive call is made after the return keyword)
    • This is a tail call and it only increases performance when the program is run in strict mode and is running on WebKit
    function recurse(n) {
        if (n == 0) {
            return 1;
        }
    
        return recurse(n - 1);
    }
    
    • Memory overflow issue if too many calls are made
    • A new scope is created (stack/memory frame)
  • Trampolines are alternatives to tail calls

    • A recursive process is converted to a while loop where a function is continuously invoked until it returns the result we need
    • If the result is not returned, it is returning a function instead
    function trampoline(fn) {
        return function trampolined(...args) {
            let result = fn(...args);
    
            while(typeof result === "function") {
                result = result();
            }
    
            return result;
        };
    }
    
    let countVowels = trampoline(function countVowels(count, str) {
        count += (isVowel(str[0]) ? 1 : 0);
    
        if (str.length <= 1) {
            return count;
        }
    
        return function f() {
            return countVowels(count, str.slice(1));
        };
    });
    
    • Optionally, currying can be applied here
    countVowels = curry(2, countVowels)(0);
    let count = countVowels("hello");   // 2
    

List Operations

  • JavaScript's in-built arrays have different list operations

    • map iterates over all items and performs a transformation on each item specified by the callback function

    • filter iterates over all items and filters them in if they fulfill some predicate

    • reduce iterates over all items and performs some type of task to combine them into a single returned value

      • The task is specified by the reducer function, which takes in an accumulator and a single item
      • Optionally, an initial value can be set to the accumulator
    • Fusion is using composition to remove intermediate structures

    • Functor is a design pattern that allows functions to be applied to values inside a generic type without changing the structure of the generic type

  • These functions are impure, therefore, they cannot be composed

Transduction

  • Transduction is the composition of reducers

    • For the other list operations mentioned, they can be reshaped to become composable reducers
  • Transducers are prototypes of a shaped reduce which will become complete if a reducer is passed to it

  • Currying can be applied to transduction

    let mapReducer = curry(2, function mapReducer(mappingFn, combineFn) {
        return function reducer(list, x) {
            return combineFn(list, mappingFn(x));
        };
    });
    
    let filterReducer = curry(2, function filterReducer(predicateFn, combineFn) {
        return function reducer(list, x) {
            if (predicateFn(x)) {
                return combineFn(list, x);
            }
            else {
                return list;
            }
        };
    });
    
    function add1(x) { return x + 1; }
    function isOdd(x) { return x % 2 == 1; }
    function sum(total, x) { return total + x; }
    
    let transducer = compose(mapReducer(add1), filterReducer(isOdd));
    let list = [1, 3, 4, 6, 9, 12, 13, 16, 21];
    
    transduce(transducer, sum, 0, list);     // 42
    into(transducer, 0, list);               // 42
    list.reduce(transducer(sum), 0);         // 42
    

Data Structure Operations

  • Monad is a pattern for pairing data with a set of predictable behaviours that let it interface with other data + behaviour pairings (monads)

  • Handle side effects to purify functions

  • A monad is essentially a functor, a wrapper containing a value and returning functions that are applied to that value

    function Just(value) {
        return { map, chain, ap };
    
        function map(fn) {
            return Just(fn(value));
        }
    
        // bind, flatMap
        function chain(fn) {
            return fn(value);
        }
    
        function ap(anotherMonad) {
            return anotherMonad.map(value);
        }
    }
    
    • flatMap flattens each value in monad
    • Just monad holds a single value like shown above
    • Nothing monad will return Nothing monads from map, chain and ap
    • Maybe monad holds either a Just or Nothing monad
    let user1 = Just("Kyle");
    let user2 = Just("Susan");
    
    let tuple = curry(2, function tuple(x, y) {
        return [x, y];
    });
    let users = user1.map(tuple).ap(user2);
    
    users.chain(identity);      // ["Kyle", "Susan"]
    

Async

  • Observables are asynchronous streams of data that declaratively flow to each other

  • Free-Point concepts can be applied for async operations that utilised observables

    • Functions like map and filter return observables
    • Monads also return observables
  • JavaScript does not have built-in observables, but the library Rx.js can be used


The Recent Parts of JavaScript

Strings

  • Template strings (interpolated strings) introduced in ES6

    • Rather than concatenating variables with strings (imperative), integrate it instead by using replacement
    • This is a more declarative approach
    const name = "John";
    let msg = `Hello, ${name}!`;
    
  • Tagged templates, useful for formatting strings

    let amount = 12.3;
    let msg = formatCurrency`Total order: ${amount}`;
    
    function formatCurrency(strings, ...values) {
        let newString = "";
        for(let i = 0; i < strings.length; i++) {
            if (i > 0) {
                if (typeof values[i - 1] == "number") {
                    newString += `$${values[i - 1].toFixed(2)}`;
                }
                else {
                    newString += values[i - 1];
                }
            }
    
            newString += strings[i];
        }
    
        return newString;
    }
    
  • Padding and trimming

    • padStart takes in an integer (total number of characters) and optionally a string, which pads the start of the original string
    let str = "Hello";
    
    str.padStart(5);            // "Hello"
    str.padStart(8);            // "   Hello"
    str.padStart(8, "*");       // "***Hello"
    str.padStart(8, "12345");   // "123Hello"
    str.padStart(8, "ab");      // "abaHello"
    
    • padEnd is the same as padStart but pads at the end
    let str = "Hello";
    
    str.padEnd(5);            // "Hello"
    str.padEnd(8);            // "Hello   "
    str.padEnd(8, "*");       // "Hello***"
    str.padEnd(8, "12345");   // "Hello123"
    str.padEnd(8, "ab");      // "Helloaba"
    
    • The trim methods don't modify the original string, it returns a new string instead
    • trim removes whitespace from both ends
    • trimStart removes whitespace from the start of the string
    • trimEnd removes whitespace from the end of the string
    let str = "   Hello   ";
    
    str.trim();         // "Hello"
    str.trimStart();    // "Hello   "
    str.trimEnd();      // "   Hello"
    

Array destructuring

  • Decomposing an array structure into its individual elements

    • Imperative
    const person = ["John", "Smith", 10];
    
    const firstname = person[0];
    const surname = person[1];
    const age = person[2];
    
    • Declarative
    const person = ["John", "Smith", 10];
    
    const [firstname, surname] = person;
    
    • Could be other properties on the person array, such as age or address
    • Can specify the data we want to destructure
  • Can restructure the remaining elements into another array after destructuring certain elements into variables

    const data = [1, 2, 3, 4, 5];
    const [a, b, ...c] = data; // a = 1, b = 2, c = [3, 4, 5]
    
    • The following syntax would be invalid, the rest element must be last
    const [a, b, ...c, d] = data;
    
  • Can have empty positions indicated by commas, put comma on a new line for readability

    const data = [1, 2, 3, 4, 5];
    const [a, 
            , 
            ...c] = data; // a = 1, c = [3, 4, 5]
    
  • Swapping values are more convenient than having an intermediate variable

    let x = 10;
    let y = 20;
    [y, x] = [x, y];
    
  • Destructuring in function parameters

    function doSomething([a, b, c]) {
        ...
    }
    
  • Nested array destructuring

    const data = [1, [2, 3], 4, 5];
    const [a, [b, c], ...d] = data;     // a = 1, b = 2, c = 3, d = [4, 5]
    
    const data = [1, [2, 3], 4, 5];
    const [a, [b], ...c] = data;     // a = 1, b = 2, c = [4, 5]
    

Object destructuring

  • Decomposing an object structure into its individual elements

    • Imperative
    const person = {
        firstname: "John",
        surname: "Smith",
        age: 10
    };
    
    const firstname = person.firstname;
    const surname = person.surname;
    const age = person.age;
    
    • Declarative
    const person = {
        firstname: "John",
        surname: "Smith",
        age: 10
    };
    
    const { firstname, surname } = person;    // firstname = "John, surname = "Smith"
    
    • Could be other properties on the person object, such as age or address.
    • Can specify the data to destructure using the key name
  • Can restructure the remaining elements into another object after destructuring certain elements into variables

    const data = { a: 1, b: 2, c: 3, d: 4 };
    const { a, b, ...c } = data;    // a = 1, b = 2, c = { c: 3, d: 4 }
    
    • Similar to arrays, the rest element must be last otherwise an error occurs
    const { a, b, ...c, d } = data;
    
  • Destructuring in function parameters

    function doSomething({ a, b, c }) {
        ...
    }
    
  • Nested object destructuring

    const data = { a: 1, b: { c: 2, d: 3 }, e: 4, f: 5 };
    const { a, b: { c, d }, ...e } = data;    // a = 1, c = 2, d = 3, e = { e: 4, f: 5 }
    
    const data = { a: 1, b: { c: 2, d: 3 }, e: 4, f: 5 };
    const { a, b: { c }, ...e } = data;    // a = 1, c = 2, e = { e: 4, f: 5 }
    
  • Nested object and array destructuring

    const data = { a: 1, b: { c: 2, d: [3, 4] }, e: 5, f: 6 };
    const { a, b: { c, d: [e, f] }, ...g } = data;    // a = 1, c = 2, e = 3, f = 4, g = { e: 5, f: 6 }
    
    const data = { a: 1, b: { c: 2, d: [3, 4] }, e: 5, f: 6 };
    const { a, b: { c, d: [e, f] }, ...g } = data;    // a = 1, c = 2, e = 3, f = 4, g = { e: 5, f: 6 }
    
    const data = { a: 1, b: { c: 2, d: [3, 4] }, e: 5, f: 6 };
    const { a, b: { c, d }, ...e } = data;    // a = 1, c = 2, d = [3, 4], e = { e: 5, f: 6 }
    

Further destructuring

  • Named arguments

    function lookupRecord(store = "", id = -1) { } 
    function lookupRecord({ store = "", id = -1 }) { }
    
    lookupRecord({ id: 42 });
    
  • Destructuring and restructuring

    function ajaxOptions({
        url = "http://",
        method = "post",
        data,
        callback,
        headers: [
            header: "",
            ...otherHeaders
        ] = []
    } = {}) {
        return {
            url, method, data, callback,
            headers: [header, ...otherHeaders]
        };
    }
    

Array methods

  • Array.find

    • find takes in a function callback
    • Returns first instance of an element that matches a predicate in a function callback
    • If no elements match, then undefined is returned
  • Array.findIndex

    • Similar to Array.find but returns the index position of the element
    • If no element is found then -1 is returned
  • Array.indexOf

    • Similar to Array.findIndex but it doesn't take in a callback function, it takes in a search element
  • Array.includes

    • Similar to Array.find but returns true or false instead of the element
    • Can optionally take in a starting search index
  • Array.flat

    • Create a new array with sub-array elements concatenated up to the specified depth
    const arr = [1, [2, 3], [[]], [4, [5]], 6];
    
    arr.flat(0);     // [1, [2, 3], [[]], [4, [5]], 6]
    arr.flat();      // [1, 2, 3, [], 4, [5], 6]
    arr.flat(2);     // [1, 2, 3, 4, 5, 6]
    
  • Array.flatMap

    • Creates a new array formed by applying a callback function to each element then flattening the result by one level
    • Basically map followed by flat but more efficient than doing both operations individually
    const arr = [1, 2, 3];
    const newArr = arr.flatMap(n => [n, n]);    // [[1, 1], [2, 2], [3, 3]] --> [1, 1, 2, 2, 3, 3]
    

Iterators and generators

  • Iterate over an iterable data structure by calling next until no more elements exist

    let str = "Hello";
    let it = str[Symbol.iterator]();
    
    it.next();  // { value: "H", done: false }
    it.next();  // { value: "e", done: false }
    it.next();  // { value: "l", done: false }
    it.next();  // { value: "l", done: false }
    it.next();  // { value: "o", done: false }
    it.next();  // { value: undefined, done: true }
    
    • Can use a loop as well
    let str = "Hello";
    let it = str[Symbol.iterator]();
    
    for (let v of it) {
        ...
    }
    
  • Data structures without iterators, such as objects

    • Need to define our own iterator
    • Imperative approach
    let obj = {
        a: 1,
        b: 2,
        c: 3,
        [Symbol.iterator]: function() {
            let keys = Object.keys(this);
            let index = 0;
            return {
                next: () =>
                    (index < keys.length) ?
                        { done: false, value: this[keys[index++]] } :
                        { done: true, value: undefined }
            };
        }
    };
    
    • Declarative approach, using a generator to yield out values
    let obj = {
        a: 1,
        b: 2,
        c: 3,
        *[Symbol.iterator]() {
            for (let key of Object.keys(this)) {
                yield this[key];
            }
        }
    };
    

Regular expressions

  • Look ahead

    let msg = "Hello World";
    
    msg.match(/(l.)/g);         // ["ll", "ld"]
    msg.match(/(l.)$/g);        // ["ld"]
    msg.match(/(l.)(?=o)/g);    // ["ll"], positive look ahead, match "l." if followed by an "o"
    msg.match(/(l.)(?!o)/g);    // ["lo", "ld"] negative look ahead, match "l." if not followed by an "o"
    
  • Look behind

    let msg = "Hello World";
    
    msg.match(/(?<=e)(l.)/g);    // ["ll"], positive look behind, match "l." if preceded by an "e"
    msg.match(/(?<!e)(l.)/g);    // ["lo", "ld"] negative look ahead, match "l." if not preceded by an "e"
    
  • Named capture groups, organise regex and make them more readable

    let msg = "Hello World";
    
    msg.match(/(?<cap>l.)/).groups;     // { cap: "ll" }
    
  • dotall Mode, a flag s to add to the end of regex

    let msg = "
    The quick brown fox 
    jumps over the 
    lazy dog";
    
    msg.match(/brown.*over/);           // null, can't match over new lines
    msg.match(/brown.*over/s);          // ["brown fox\njumps over"]
    

async and await

  • Don't need promise chains

    • await must be used inside an async function
        async function main() {
            let user = await fetchCurrentUser();
            let [archivedOrders, currentOrders] = await Promise.all([
                    fetchArchivedOrders(user.id),
                    fetchCurrentOrders(user.id)
                ]);
    
            ...
        }
    
  • await can't be used within a function that's within an async function

    • Use another set of nested async and await keywords
    async function fetchFiles(files) {
        let promises = await FA.concurrent.map(getFiles, files);
        
        await FA.serialforEach(async function each(promise) {
            console.log(await promise);
        }, promises);
    }
    
  • Issues:

    • await Only Promises, not extensible to other functionality other than then
    • Scheduling (starvation), promises have priority in a microtask queue but there is a risk they keep generating more microtasks (infinite loop)
    • External cancelation, there is no mechanism for cancelling requests
  • Async generators with yield

    async function *fetchURLs(urls) {
        for(let url of urls) {
            let response = await fetch(url);
            if (response.status == 200) {
                let text = yield response.text();
                yield text.toUpperCase();
            }
            else {
                yield undefined;
            }
        }
    }
    
  • Iteration with async generators

    async function main(sites) {
        for await (let text of fetchURLs(sites)) {
            console.log(text);
        }
    }