DEV Community

loading...

A few handy JavaScript tricks

Andrew Nosenko
Dad, self-employed, problem solver at heart, async all the way down. Ex principal software engineer at Nuance Communications. Occasionally I tweet and answer my own questions on StackOverflow.
Updated on ・10 min read

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 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());
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
}(); 
Enter fullscreen mode Exit fullscreen mode

I believe it's more readable and mnemonic than wrapping the function with brackets:

(function debug() {
  if (confirm('stop?')) debugger;
})();
Enter fullscreen mode Exit fullscreen mode

If we do need the result:

const rgb = function getColor(color) {
  return { 
    red: '#FF0000',
    green: '#00FF00',
    blue: '#0000FF'
  }[color];
}(car.color); 
Enter fullscreen mode Exit fullscreen mode


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}`);
}();
Enter fullscreen mode Exit fullscreen mode


Destructuring of a function argument inline

function output ({firstName, lastName}) {
  console.log(firstName, lastName);
}

const person = {
  firstName: 'Jane',
  lastName: 'Doe'
};

output(person);
Enter fullscreen mode Exit fullscreen mode


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);
Enter fullscreen mode Exit fullscreen mode


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);
Enter fullscreen mode Exit fullscreen mode


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); 
Enter fullscreen mode Exit fullscreen mode


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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

We can also make it work across realms (an iframe or a popup):

const isString = s?.constructor.name === 'String';
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode


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 
}
Enter fullscreen mode Exit fullscreen mode

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, here is another way of performing the "nullish" check:

if (a?.constructor) {
  // a is neither null nor undefined
}

if (!a?.constructor) {
  // a is either null or undefined
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, we can check if the valueOf method exists:

if (a?.valueOf === undefined) {
  // a is either null or undefined
}
Enter fullscreen mode Exit fullscreen mode

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();
      }
    }());
  }
}
Enter fullscreen mode Exit fullscreen mode

Of course, now there's also the nullish coalescing operator. It allows for shortcuts like a ?? DEFAULT_VALUE, and 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 {}
*/
Enter fullscreen mode Exit fullscreen mode


A mnemonic way of ignoring promise errors (where applicable)

await promise.catch(e => void e); 
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode


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;
Enter fullscreen mode Exit fullscreen mode


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 }; 
}
Enter fullscreen mode Exit fullscreen mode


"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)));
Enter fullscreen mode Exit fullscreen mode

Hopefully, soon we'll be able to do:

const promise = Promise.try(() => ensureEven(1));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

It might be tempting to think about such kind of object reproduction this way:

const a2 = new a.constructor();
Enter fullscreen mode Exit fullscreen mode

In reality though, it's done a bit differently, more like this:

const constructor = a.constructor[Symbol.species] ?? a.constructor;
const a2 = new constructor();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (6)

Collapse
morganconrad profile image
Morgan Conrad

I seldom use IIFEs, but I like your idea of prefacing with void. I typically name the function "iife" as well. (or iife1. iife2...)

Collapse
noseratio profile image
Andrew Nosenko Author

I find myself using them more often for generators or async generators, like that getRacers from the article:

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);
    }
  }());
Enter fullscreen mode Exit fullscreen mode
Collapse
dvdty profile image
Pavel

You can replace
winner = winner ?? p;
with
winner ??= p;

Thread Thread
noseratio profile image
Andrew Nosenko Author

A good point, thanks. Though, the logical nullish assignment is fairly new (Chrome 85+, Node 15+).

Collapse
ogrotten profile image
ogrotten

I'm gonna have to read this several times to digest all the info. Great stuff.

Collapse
levitabris profile image
Wei Li

Extremely useful! Thanks.

Forem Open with the Forem app