JavaScript Tricks
Published on Thu, 16 Nov 2023|Updated on Mon, 20 Nov 2023|10 minute read

JavaScript (1) TypeScript (1) LeetCode (1) Books (1) YouTube (1)

After several friends and old coworkers mentioned using TypeScript in their work, and discovering online content creators who love and teach TypeScript, as well as finding cool job postings that use the language, a superset of JavaScript, I decided I finally better get my act together around its use.

Ok, so I typically turn first to books. A recent (Aug 2023) publication from O'Reilly is TypeScript Cookbook - which I'm working through as of last night. It's been a great High and Low level review of what's going on in the JavaScript world. And even though it seems some major projects are moving back to Vanilla (apparently, for performance reasons), I needed hands-on experience myself.

But I'm also pretty into LeetCode right now. I've got a small Daily Streak going (33 as of today!), and I typically save my solutions so I can review them quickly and easily later in my GitHub repo for LeetCode Solutions. So while I'm poking around the site, I stumble upon another user with a "JavaScript 30" badge... what' s this?

And so I end up running through LeetCode's 30 Days of JavaScript in an afternoon.

Fast Learning

Whenever you learn a bunch of new concepts in quick succession, or you're refreshing your memory on concepts you've learned in the past, it's so easy to end up in the following situation.

  1. You are reminded of something you used to know
  2. "Oh, cool. Yeah, I remember that."
  3. Wake up 3 days later...
  4. 90% of what you recalled is long gone.

I'll call this "Fast Learning" - we ingest, refresh, and move on. Without repetition, this stuff fades away so fast.

Ok, let's forestall Steps 3 and 4 with a little anticipatory action... writing stuff down!

Power of Note-Taking

Saving little memories and ideas for later is super useful. Think notecards, book highlights, visual queues - all of these things serve our feeble memory to convert convert convert... until we've finally got stuff digested into the long-term folds.

And long-term memory need not even be the goal here. Often times, I use notes and highlights to simply be the memory storage themselves. All I really need to remember is: "huh, I think this is something I saved for later", or "Oh, Dijkstra's! How does that go again?" and run off to my little stash of notes and pull the right one up.

Stashed Snippets from 30 Days of JavaScript

Alright, so I'm breezing through the course. Feeling really smart. But only while I can easily check the Editorials... let's copy stuff down so we have it for later! What follows are some of the things I took away from the course, that I was either refreshed on, that I learned, or that I thought cool.

Stuff About Arrays

Reduce Replacing .filter().map()

Replace .filter().map() with .reduce() to prevent one extra array copy (.filter() and .map() each create a copy of the input to return an array).

``` /* With filter and map / var neighborNames = homes .filter(home => home.proximityMiles < 2) .map(home => home.name)

/* Replace With Reduce / var neighborNames = homes.reduce((accumulator, home) => { if (home.proximityMiles < 2) { accumulator.push(home.name); } return accumulator; }, []); ```

Array.reduceRight() and Array.reverse()

Given an array of functions, apply them to x in reverse order:

let size = functions.length;
while (--size >= 0) {
    x = functions[size](x);
}
return x;

...or simply use .reduceRight()

(x: number) => functions.reduceRight((acc, f) => f(acc), x);

...or even .reverse() with for ... of

for (const fn of functions.reverse()) {
    x = fn(x);
}

Array.filter Performance

Because Array.filter needs to handle sparse arrays, it is usually slower than an optimal custom implementation that assumes arrays aren't sparse.

What's a sparse array? It's an array with many allocated spaces but few values, e.g.

let arr = Array(100); arr[1] = 10;

Useful Array Methods

Long list of Array Methods
  • length: Returns the number of elements in an array.
  • constructor: Specifies the function that creates an array's prototype.
  • prototype: Allows you to add properties and methods to an array's prototype object.
  • Symbol.iterator: Returns the iterator object for iterating over the elements of an array.
  • concat(): Joins two or more arrays and returns a new array.
  • join(): Joins all elements of an array into a string.
  • push(): Adds one or more elements to the end of an array and returns the new length of the array.
  • pop(): Removes the last element from an array and returns that element.
  • shift(): Removes the first element from an array and returns that element.
  • unshift(): Adds one or more elements to the beginning of an array and returns the new length of the array.
  • slice(): Returns a shallow copy of a portion of an array into a new array object selected from start to end (end not included).
  • splice(): Changes the contents of an array by removing, replacing, or adding elements at a specified index.
  • indexOf(): Returns the first index at which a given element can be found in the array, or -1 if it is not present.
  • lastIndexOf(): Returns the last index at which a given element can be found in the array, or -1 if it is not present.
  • forEach(): Executes a provided function once for each array element.
  • map(): Creates a new array with the results of calling a provided function on every element in the array.
  • filter(): Creates a new array with all elements that pass the test implemented by the provided function.
  • reduce(): Applies a function against an accumulator and each element in the array to reduce it to a single value.
  • reduceRight(): Applies a function against an accumulator and each element in the array (from right to left) to reduce it to a single value.
  • sort(): Sorts the elements of an array in place and returns the sorted array.
  • reverse(): Reverses the order of the elements in an array in place.
  • toString(): Returns a string representing the specified array and its elements.
  • toLocaleString(): Returns a localized string representing the elements of the array.
  • includes(): Determines whether an array includes a certain value.
  • some(): Checks if at least one element in the array satisfies a provided condition.
  • every(): Checks if all elements in the array satisfy a provided condition.
  • find(): Returns the first element in the array that satisfies a provided condition.
  • findIndex(): Returns the index of the first element in the array that satisfies a provided condition.
  • fill(): Fills all elements in an array with a static value from a start index to an end index.
  • copyWithin(): Copies a sequence of elements within the array to another position in the same array.
  • flat(): Creates a new array with all sub-array elements concatenated into it recursively up to a specified depth.
  • flatMap(): Maps each element using a mapping function and flattens the result into a new array.
  • from(): Creates a new, shallow-copied array from an iterable object or array-like object.
  • isArray(): Determines whether the passed value is an array.
  • of(): Creates a new array with a variable number of elements.
  • keys(): Returns a new array iterator that contains the keys of the array.
  • values(): Returns a new array iterator that contains the values of the array.
  • entries(): Returns a new array iterator that contains the key-value pairs of the array.

Random Stuff

Pure Functions... who cares?

Pure functions are functions that, given the same inputs, return the same outputs. Their idempotency is their purity. What's so great about this aspect? Well, for one thing, you can cache the result and skip the entire computation if it's already been computed. This is called memoization, and it's a quality that drives much of the web's efficiency. In a classic example, calculating FIbonacci numbers without memoization for non-trivial values (e.g. 99) takes years.

Promises

For a nice walkthrough of Promises in Javascript, try https://javascript.info/. There one can find tutorials on a number of topics, including Promises: : https://javascript.info/async.

Combining Promises Concurrently:

/**
 * For promises resolving with numbers, return a promise resolving their sum.
 * @param {Promise} promise1
 * @param {Promise} promise2
 * @return {Promise}
 */
var addTwoPromises = async function(promise1, promise2) {
    return Promise.all([promise1, promise2]).then((values) => {
        return values.reduce((v1,v2)=>v1+v2,0);
    });
};

Promise Constructor

See Mozilla Promise Constructor documentation. Here's an example:

new Promise((resolve, reject) => {
  // invoke resolve or reject with values
});

Timeouts

Dude, don't forget that setTimeout/setInterval returns an identifier which can be cleared with clearTimeout:

var cancellable = function(fn, args, t) {
    const callbackId = setTimeout(() => fn(...args), t);
    const cancelFn = () => clearTimeout(callbackId);
    return cancelFn;
};

Note, from the docs:

It's worth noting that the pool of IDs used by setInterval() and setTimeout() are shared, which means you can technically use clearInterval() and clearTimeout() interchangeably. However, for clarity, you should avoid doing so.

Fun with Objects

LeetCode's 2705. Compact Object:

Given an object or array obj, return a compact object. A compact object is the same as the original object, except with keys containing falsy values removed. This operation applies to the object and any nested objects. Arrays are considered objects where the indices are keys. A value is considered falsy when Boolean(value) returns false.

You may assume the obj is the output of JSON.parse. In other words, it is valid JSON.

A nice recursive solution, with some small annotations! ``` /* * @param {Object|Array} obj * @return {Object|Array} / function compactObject(obj) { if (!obj) return obj;

if (Array.isArray(obj)) {
    return obj.reduce((arr, item) => {
         // prevent another call deeper if item is primitive
        const value = typeof item === 'object' ? compactObject(item) : item;
        if (value) {
            arr.push(value);
        }
        return arr;
    }, []);
}

// handle objects
const newObj = {};
for (let key of Object.keys(obj)) {
    // prevent another call deeper if item is primitive
    let value = typeof obj[key] === 'object' ? compactObject(obj[key]) : obj[key];
    if (value) {
        newObj[key] = value
    }
}

return newObj

}; ```

Operators

Chaining and Coalescing!

  1. Optional Chaining (?.): This operator allows you to read the value of a property located deep within a chain of connected objects without having to explicitly validate that each reference in the chain is valid. If a reference in the chain is null or undefined, the entire chain will gracefully return undefined instead of throwing an error.
  2. Nullish Coalescing (??): This operator returns the right-hand operand when the left-hand operand is null or undefined. It's a way to provide a default value when the left-hand operand is nullish.

Consider the usefulness of the Optional Chaining operator in this example, which creates a "pool" of n functions that execute concurrently, then pull the next one when any function in the pool frees up. See LeetCode's 2636. Promise Pool for the full problem. The Optional Chaining operator ensures that we don't attempt to invoke a function that doesn't exist in functions, when n is greater than functions.length.

var promisePool = async function(functions, n) {
    const evaluateNext = () => functions[n++]?.().then(evaluateNext);
    return Promise.all(functions.slice(0, n).map(f => f().then(evaluateNext)));
};

Spread (...)

  • Is useful for passing an unspecified number of parameters into a function.
  • Is an "array-like" object, which does expose some array methods, like push(), pop(), index notation args[index] = value, and .length.
  • Can be converted to a proper array easily with [...args] or Array.from(args)

There is one gotcha I encountered with this operator, and the importance of its use. Consider a basic subscription/callback environment, where we need to send an event to a bunch of subscribers (i.e. callbacks). We grab the callback out of a map, and for each one, invoke it... but we need to pass args correctly, using the Spread operator!

emit(eventName, args = []) {
    const cbs = this.<span class="tag"><a href="/allposts.html?tag=map"><span class="tag-name">map (1)</span></a></span>.get(eventName);
    let res = [];
    for (let cbKey in cbs) {
        cbs[cbKey](...args);
    }
    return res;
}