Gone with the wind 🌬️🍃
Published on

Look ma, I know JS!

Introduction

JavaScript has many interesting behaviours and different ways of doing the same thing, with slight nuances in how the alternate syntaxes work.

This makes it flexible, so flexible that you may waste a significant amount of your life refactoring syntax just to comply to conventions of different tools/libraries you may use.

For example, certain tools only want the expected symbol (function,object etc) exported as the default not named export:

// Default export
export default async(){}


// named export. Won't work on certain react based tools 
// if its loader expects a default export

export async function func(){}

Which really sucks because there are more deadlier bugs that require our attention than silly export mismatches. That's one thing that made it so hard for me to learn JS because I was trying to learn the language via tools/frameworks.

Due to the subtle differences in behaviour in alternate syntaxes it's very easy to create bugs that will only popup in edge cases you didn't know exist.

For example, when you refactor variable declarations from var to const it means that the symbol can no longer be reassigned and is no longer being hoisted. What this ultimately means is that if you reassigned the symbol somewhere in your program, it will break.

But I'm not here to tutor JS lol.

In this post, I'll share some of my favourite and usually abused features of the language.

Array destructuring

Variable declarations allow us to tell our program to store values which we can retrieve later when needed in the program. One of my favourite way of doing this array destructuring. We can do funny things with this feature🙃:

var [a, b, c, [d, e, [f, g]]] = [1, 2, 3, [4, 5, [6, 7]]]

The variables a,b,c,d,e,f and g are assigned the values in the array on the right side of the assignment operator. The engine will recursively assign nested values to their respective nested declarations.

I sorta abuse this syntax and put literally anything in there (including functions, don't mention readability please).

I don't know if there's any perfomance tradeoffs with this.

Immediately Invoked Function Expression (IIFE)

As the name implies, this function is called (or invoked) as soon as it is defined. It's useful if you want to write a once off function that, for example computes the value of a key in an object.

const obj = {
  name: "ディーン・タリサイ",
  secret: (() => Math.random() * new Date().
    getMilliseconds())(),
};

console.log(obj);

In the above example, the secret key of the obj object will have a dynamic value that is unique everytime we access it. So one use case would be to create values that are computed 'on the fly'.

Disjunction and conjuction

Big words for logical OR || ,and AND && (pun intended) respectively, these symbols can act as a makeshift ternary operator or if..else statement if combined:

const func = (x)=> typeof x === 'number' && `The number is ${x}` || `The value is ${x}`

What the Javascript engine does is follow the order of precedence, that is:

  • !
  • &&
  • ||

In our case, this means that the AND expression is evaluated first so it would look like this:


const func = (x)=> (typeof x === 'number' && `The number is ${x}`) || `The value is ${x}`

TIP

We can create our desired order of precedence by wrapping the expression we want to be evaluated first in parentheses which then enforces the PEMDAS order of precedence.

Source: MDN

If we run our function it will work as expected:

console.log(func(5))

// The number is 5


console.log(func('ディーン・タリサイ'))


// The value is ディーン・タリサイ

CAUTION

However if one of your expressions throws (an error or exception, duh 🙄), it may break the program by immediately halting the execution and throwing the unhandled exception. Use this cautiously (or not at all).

Nullish coalescing

For the lazier ones, we can use the ?? operator which returns the expression on the right side if the one on the left is falsy.

Very useful for defining default values in a concise way.

const func = (x) => x ?? 'The default value'

console.log(func(void 0))

// The default value

console.log(func('This will be printed'))

// This will be printed

As you probably can see, handling falsy values is a very big fuss in the language.

Spread operator

The spread operator a.k.a ... allows us to expand elements of an iterable like an Array. This is useful if we don't know the exact length of our iterable or wish to just unpack the iterable into elements:

const arr = [0,1,2,3]


// In this example we'll spread the iterable to our variables using array destructuring assignment


const [zero,one,two,three] = [...arr]
console.log([zero,one,two,three])

// [0,1,2,3]

Of course, the above syntax is redundant because we could have just passed the array as it is and it would work. But then again, I'm not here to teach 'good practices' (whatever that is 👀).

TIP

Also check out rest parameters. The magic behind this language construct is similar to the spread operator but just a different context. In this case, rest parameters allow us to specify an indefinite number of arguments as an array.

The in operator

The in operator checks if a property exists in an object in which case it will return true or false if otherwise.

Let's say you wish to execute some code if a certain property exists in an object. The following example will only work properly if the extremum is a property of the Math object like min or max. Whichever argument proves falsy will log the error associated with it:

const fn = (extremum, nums) => extremum in Math ?
    nums.length ?
        Math[extremum](...nums)
        : 'Extremum must be either "min" | "max" !'
    : 'Nums must be an iterable !'


console.log(fn('max',[9,5,2,7,0]))

// 7... Just kidding its 9 😄

TIP

In case you hadn't guessed, it's also implemented un the for...in loop which iterates over an objects own enumerable keys.

async...await

When we mark a function as async it means we're expecting it to return our value wrapped in a Promise.

The await keyword tells JavaScript to wait for the function to return, in this case our function is fetch.

Mostly you'll find these keywords on operations that involve network requests or computationally intense routines that could take a while to complete. Because we can't afford to block the main thread, asynchronous functions can execute without halting the entire program (contrary to synchronous programming). Here's the basic syntax.

const fn = async (url) => {

const res = await fetch(url) 

return res.json()
}

CAUTION

At the time of writing, you can only use the await keyword inside an async function otherwise you'll get a syntax error.

Optional chaining with ?.

Remember when accessing an undefined property on an object used to throw a violent error ? Well, thanks to optional chaining we can now only attempt to access a property if it exists on our object.

This allows our program to fail gracefully by returning undefined instead of throwing an error which immediately halts our program (if the error is not handled, of course):

const obj = {
  value:'Hello',
}

// this fails gently by returning undefined
console.log(obj?.Value)

// this throws a tantrum
console.log(obj.Value)

TIP

This is useful when accessing deep nested objects in (like a JSON response for example) where certain properties may not be defined.

What instanceof an object is this ?

The instanceof operator allows us to check what instance of a constructor our object belongs to. It utilises the prototype chain to find the nearest ancestor of the object.

class SomeObject {

    constructor(opts) {
        this.opts = opts
    }

    getKeys() {
        return Object.keys(this.opts)
    }
};

const obj = new SomeObject(), x = obj instanceof SomeObject

console.log(x)

Conclusion

Well, that was a round up of the random features I like and often (overuse) in JavaScript.