Table of Contents
- JavaScript Foundations
- Rethinking Asynchronous JavaScript
- Functional-Light JavaScript
- 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
-
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); } }