A few handy JavaScript tricks
Andrew Nosenko
Posted on December 1, 2020
I'd like to document a few neat JavaScript tricks and patterns I've recently learnt from Twitter and other online resources (which I sadly haven't kept track of). All the credits go to the online JavaScript community.
Table Of Contents
class
is an expression, what it extends is also an expressionthis
in static class methods- Invoking an IIFE without extra brackets
- Invoking an async IIFE without extra brackets
- Destructuring of a function argument inline
- Partial destructuring of a function argument inline
- Using expressions in
switch
- Passing a non-function object as event handler to
addEventListener
- Checking if a variable is of specific type
- Checking if a variable is
nullish
(i.e.,null
orundefined
) - Converting to primitive types with
Symbol.toPrimitive
- A mnemonic way of ignoring promise errors (where applicable)
- Thenables can be useful side-by-side with promises
- Telling which promise has settled first in
Promise.race
- "Promisifying" a synchronous function call to defer exception handling
Symbol.species
can be useful when extending standard classes-
await
can be used inside string templates
class
is an expression, what it extends is also an expression
Similar to function funcName() { ... }
, class className { ... }
is an expression which can be assigned to a variable or passed over as a function argument. And className
here can be optional as well, like with anonymous functions. More so, the base class is also an expression. For example, the following is possible:
class Base1 {
whatAmI() {
return 'Base1';
}
}
class Base2 {
whatAmI() {
return 'Base2';
}
}
const createDerivedClass = base => class extends base {
whatAmI() {
return `Derived from ${super.whatAmI()}`;
}
};
const Derived1 = createDerivedClass(Base1);
// output: Derived from Base1
console.log(new Derived1().whatAmI());
const Derived2 = createDerivedClass(Base2);
// output: Derived from Base2
console.log(new Derived2().whatAmI());
This can be useful for dynamic compositions of class inheritance trees, including mixins. I've learnt about it from Justin Fagnani's excellent "Mixins and Javascript: The Good, the Bad, and the Ugly."
Conveniently, this
in static class methods refers to the class itself
Thus, polymorphism is possible for static methods, like with oncreate
method below:
// Base
class Base {
static create() {
const instance = new this();
this.oncreate(instance);
return instance;
}
static oncreate(instance) {
console.log(`Something of the base class ${
Base.name} has been created.`);
}
}
// Derived
class Derived extends Base {
static oncreate(instance) {
console.log(`It's a new instance of ${
Derived.name}, all right!`);
}
}
// output: Something of the base class Base has been created.
const base = Base.create();
// output: It's a new instance of Derived, all right!
const derived = Derived.create();
// output: true
console.log(derived instanceof Derived);
I learnt about new this()
when I stumbled upon this tweet.
Invoking an IIFE (Immediately Invoked Function Expression) without extra brackets
We can use the void
operator for that, where void
clearly indicates we want to discard the result of an expression (which an IIFE itself is):
void function debug() {
if (confirm('stop?')) debugger;
}();
I believe it's more readable and mnemonic than wrapping the function with brackets:
(function debug() {
if (confirm('stop?')) debugger;
})();
If we do need the result:
const rgb = function getColor(color) {
return {
red: '#FF0000',
green: '#00FF00',
blue: '#0000FF'
}[color];
}(car.color);
Invoking an async
IIFE (Immediately Invoked Function Expression)
Similarly to the above, we don't need the wrapping brackets:
await async function delay() {
const start = performance.now();
await new Promise(r => setTimeout(r, 1000));
console.log(`elapsed: ${performance.now() - start}`);
}();
Destructuring of a function argument inline
function output ({firstName, lastName}) {
console.log(firstName, lastName);
}
const person = {
firstName: 'Jane',
lastName: 'Doe'
};
output(person);
Partial destructuring of a function argument inline
function output ({firstName, ...rest}) {
console.log(firstName, rest.lastName, rest.age);
}
const person = {
firstName: 'John',
lastName: 'Doe',
age: 33
};
output(person);
Using expressions in switch
const category = function getCategory(temp) {
// the first `case` which expression is `true` wins
switch(true) {
case temp < 0: return 'freezing';
case temp < 10: return 'cold';
case temp < 24: return 'cool';
default: return 'unknown';
}
}(10);
Passing a non-function object as event handler to addEventListener
The trick is to implement EventListener.handleEvent
:
const listener = Object.freeze({
state: { message: 'Hello' },
handleEvent: event => {
alert(`${event.type} : ${listener.state.message}`);
}
});
button.addEventListener('click', listener);
Checking if a variable is of specific type
This method works for both primitive value types and their wrapper classes: String
, Number
, Boolean
, Object
.
Could you predict which console output is common for s1
and s2
snippets below?
const s1 = 's';
console.log(s1 instanceof String);
console.log(typeof s1);
console.log(s1.constructor === String);
const s2 = new String('s');
console.log(s2 instanceof String);
console.log(typeof s2);
console.log(s2.constructor === String);
I could not, so I've made a RunKit:
s1 instanceof String: false
typeof s1: string
s1.constructor === String: true
s2 instanceof String: true
typeof s2: object
s2.constructor === String: true
Interestingly, it's only s1.constructor === String
and s2.constructor === String
which are consistently true
for both s1
(a primitive string value) and s2
(an instance of String
class).
It's even more fun in TypeScript, which may feel odd for a person coming to JavaScript with C# or Java background.
So, to check if variable s
represents a string, the following works equally well for primitive values and their wrapping class types:
const isString = s?.constructor === String;
We can also make it work across realms (an iframe
or a popup):
const isString = s?.constructor.name === 'String';
Some may argue that we shouldn't be using class wrappers for primitive values at all. Indeed, we should not. But we have an option to make our own code behave correctly when it is called by a 3rd party, no matter if it's given a primitive value or a wrapper class object as an argument.
For example, the following works consistently for all three cases (note the use of valueOf
):
takeBool(false);
takeBool(Boolean(false));
takeBool(new Boolean(false));
function takeBool(b) {
if (b?.constructor !== Boolean) throw new TypeError();
console.log(b.valueOf() === false? "is false": "is true");
}
Checking if a variable is nullish
(i.e., null
or undefined
)
Traditionally, this is done with loose equality operator ==
, for example:
if (a == null) {
// a is either null or undefined
console.log((a == null) && (a == undefined)); // both true
}
This might arguably be the only meaningful use of the loose equality ==
operator (as opposed to the strict equality operator ===
).
If however you want to avoid using ==
and !=
operators by all means, below are some alternative ways of performing the "nullish" check:
// The nullish coalescing (??) operator returns
// its right-hand side operand when its left-hand side operand
// is null or undefined, and otherwise returns
// its left-hand side operand.
if ((a ?? null) === null) {
// a is either null or undefined
}
if (Object.is(a ?? null, null)) {
// a is either null or undefined
}
if (Object.is(a ?? undefined, undefined)) {
// a is either null or undefined
}
// all standard or properly derived custom JavaScript objects
// have standard properties like these:
// `constructor`, `valueOf`, `toString`.
// Note though the below doesn't work for exotic cases,
// e.g. where a = Object.create(null):
if (a?.constructor) {
// a is neither null nor undefined
}
if (!a?.constructor) {
// a is either null or undefined
}
if (a?.valueOf === undefined) {
// a is either null or undefined
}
What's nice about the optional chaining operator is that the result is unambiguously undefined
when a
is either null
or undefined
. This allows for some fancy expressions like this:
class Derived extends Base {
constructor(numericArg) {
// make sure the argument we pass to the base class'
// constructor is either a Number or DEFAULT_VALUE
super(function() {
switch (numericArg?.constructor) {
case undefined: return DEFAULT_VALUE;
case Number: return numericArg.valueOf();
default: throw new TypeError();
}
}());
}
}
One notable thing about the nullish coalescing operator is that in a ?? DEFAULT_VALUE
it will pick DEFAULT_VALUE
when a
is either null
or undefined
(as opposed to a || DEFAULT_VALUE
, which picks DEFAULT_VALUE
when a
is falsy
).
Converting to primitive types with Symbol.toPrimitive
The well-know symbol Symbol.toPrimitive
defines how an object can be converted to primitive types, as in the example below. Note also the use of Symbol.toStringTag
:
class Item {
#item;
constructor(item) {
if (item?.constructor !== Number) throw new TypeError();
this.#item = item.valueOf();
}
[Symbol.toPrimitive](hint) {
// hint can be "number", "string", and "default"
switch (hint) {
case 'number':
return this.#item;
case 'string':
case 'default':
return `Item: ${this.#item}`;
default:
return null;
}
}
get [Symbol.toStringTag]() {
return this.constructor.name;
}
}
const item = new Item(42);
console.log(Number(item));
console.log(String(item));
console.log(item.toString());
console.log(item);
/* Output:
42
Item: 42
[object Item]
Item {}
*/
A mnemonic way of ignoring promise errors (where applicable)
await promise.catch(e => void e);
This literally says: "void that error" and it is ESLint-friedly. I see it becoming increasingly useful, to avoid potential troubles with unhandled promise rejections in Node v15+. For example:
// • we may want to start workflow1 before workflow2
const promise1 = workflow1();
const promise2 = workflow2();
// • and we may need workflow2 results first
// • if it fails, we don't care about the results of workflow1
// • therefore, we want to prevent
// unwanted unhandled rejection for promise1
promise1.catch(e => void e);
// • observe workflow2 results first
await promise2;
// • if the above didn't throw, now observe workflow1 results
await promise1;
Thenables can be useful side-by-side with promises
I've previously blogged about thenables. In a nutshell, here's how to create a jQuery.Deferred
-like thenable object that can be awaited:
function createDeferred() {
let resolve, reject;
const promise = new Promise((...args) =>
[resolve, reject] = args);
return Object.freeze({
resolve,
reject,
then: (...args) => promise.then(...args)
});
}
const deferred = createDeferred();
// resolve the deferred in 2s
setTimeout(deferred.resolve, 2000);
await deferred;
Telling which promise has settled first in Promise.race
Sometimes we need to know which promise became resolved or rejected first and thus won the race with Promise.race
, similarly to Task.WhenAny
in .NET. Linking my SO answer:
/**
* When any promise is resolved or rejected,
* returns that promise as the result.
* @param {Iterable.<Promise>} iterablePromises An iterable of promises.
* @return {{winner: Promise}} The winner promise.
*/
async function whenAny(iterablePromises) {
let winner;
await Promise.race(function* getRacers() {
for (const p of iterablePromises) {
if (!p?.then) throw new TypeError();
const settle = () => winner = winner ?? p;
yield p.then(settle, settle);
}
}());
// return the winner promise as an object property,
// to prevent automatic promise "unwrapping"
return { winner };
}
"Promisifying" a synchronous function call to defer exception handling
Credits: tc39-proposal-promise-try.
function ensureEven(a) {
if (a % 2 !== 0) throw new Error('Uneven!');
return a;
}
// • this throws:
const n = ensureEven(1);
// • this doesn't throw:
const promise = Promise.resolve().then(() => ensureEven(1));
// • until it is awaited
const n = await promise;
// • alternatively:
const promise = Promise(r => r(ensureEven(1)));
Hopefully, soon we'll be able to do:
const promise = Promise.try(() => ensureEven(1));
Until then, we can also use a polyfill like this one.
Symbol.species
can be useful when extending standard classes
The well-know symbol Symbol.species
was definitely little-known to me. MDN describes it as symbol that specifies a function-valued property that the constructor function uses to create derived objects.
What it means in reality is that sometimes JavaScript needs to create a fresh instance of an object, i.e., to reproduce an object without cloning. For example, Array.prototype.map
creates a new array instance before doing any mapping:
class UltraArray extends Array {}
const a = new UltraArray(1, 2, 3);
const a2 = a.map(n => n/2);
console.log(a2 instanceof UltraArray); // true
It might be tempting to think about such kind of object reproduction this way:
const a2 = new a.constructor();
In reality though, it's done a bit differently, more like this:
const constructor = a.constructor[Symbol.species] ?? a.constructor;
const a2 = new constructor();
Thus, if we want map
to use the base class Array
for a new mapped instance, when map
is invoked on an object of our custom class UltraArray
, we can do this:
class UltraArray extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new UltraArray(1, 2, 3);
const a2 = a.map(n => n/2);
console.log(a2 instanceof UltraArray); // false
console.log(a2.constructor.name); // Array
When could this feature (seemingly not so useful) still be important? My answer would be: for deriving from and extending the standard Promise
class with added functionality like DeferredPromise
, AbortablePromise
, etc. This probably deserves a separate blog post, which I plan to publish soon.
I hope you find these tips helpful
I plan to keep this post updated as I discover more fun JavaScript bits and pieces. Consider following me on Twitter if interested in these updates.
Posted on December 1, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 6, 2024
October 24, 2024