10 popular modern JavaScript features for front-end devsA beginner's look at features typically used with React, Vue and other frameworks

chevron_leftAll Articles
Samer Buna

Just because you can do this doesn’t mean you should

Don’t try to figure out the output of the above, but can you identify which modern JavaScript features it uses?

JavaScript is a very different language than it used to be a few years ago. ECMAScript, which is the official specification that JavaScript conforms to, has improved a lot in the past few years after a rather long period without any updates to the language at all.

Today, the ECMAScript Technical Committee (TC39) makes yearly releases of ECMAScript and modern browsers shortly follow by implementing the new features introduced in each year. This has started with ECMAScript 2015 (or its other commonly known name, ES6) and since then we’ve had yearly releases named ES+CurrentYear. Some of these releases were big and others were very small but the language now has a continuous update cycle that drives more innovative features and phases out the famous problems JavaScript had over the years.

In this article, I’ll go over some of the features that are commonly used in modern front-end applications. Please note that this is a guide for beginners. Not everything “modern” is explained in this article and not everything explained in this article is “modern”. This is a grouping of important and related concepts to help you understand some of the modern features in JavaScript.

1 — Variables and Block Scopes

Here’s one of my favorite JavaScript trick questions. Is the following a valid JavaScript line of code?

{{{}}}

If you open up your dev-tools console and paste the line above, what will happen?

Why is this valid??

Nothing. No error. It’s valid!

So now the question is: What did it do? (It did not create any objects 😎)

Block Scopes

These braces are just nested block scopes. We could write code in them:

JavaScript does not care about spacing

A block scope is created with a pair of braces. This happens every time you create an if-statement, for-statement, while-statement, etc. The only exception is the braces you use with functions. These create a function scope, not a block scope.

{
  **// Block Scope**
}

if (true) {
  **// Block Scope**
}

for (var i = 1; i <= 10; i++) {
  **// Block Scope**
}

function doSomething() {
  **// Function Scope**
}

So what exactly is the different between a block scope and a functions scope you ask?

var vs let

Function scopes are created for each function (like doSomething above). One difference they have from block scopes is obvious when using the var keyword.

Variables defined with var inside a function scope are okay; they don’t leak out of that scope. If you try to access them outside of the scope, you can’t:

As expected

However, when you define variables with var in a block scope you can totally access them outside that scope afterward, which is a bit problematic.

For example, in the for-loop above, v is defined with var. So guess what you can do AFTER the loop?

A famous WTF-JavaScript

This is why the more recommended way to declare variables in modern JavaScript is by using the let keyword instead of the var keyword. When defining variables with let, we won’t have this weird out-of-scope access problem.

Much Better

Take-away: Use the let keyword to define variables. BUT, only when the variable NEEDS to CHANGE its value. This should not be a common thing in your code.

Here’s what you can mark as a better practice:

Always use the const keyword to define variables. Only use the let keyword when you absolutely need it. Never use the var keyword.

Okay… What exactly is const?

const all the things

In JavaScript, a variable is basically a label we put on a certain space in the computer’s memory.

let V = {id: 42}; // create a memory unit and label it as V

When you change the value of the variable V, you’re not changing the content of the memory space that was initially associated with V. You’re creating a new memory space and changing the V label to be associated with this new space.

V = []; // No errors

// Discard current memory unit (and its current label)
// Create new memory unit and label it as V

When you use const to define a variable you are instructing the computer to not only label a space in memory but to also NEVER CHANGE that label. The label will be forever associated with its same space in memory.

const V = { id: 42 };

**// Create a memory unit and label it as V
// This label cannot be discarded or reused**

// Later in the program
V = []; // TypeError: Assignment to constant variable.

Note that the constant part here is just the label. The value of what’s in the memory space can still change (if it’s mutable). For example, objects in JavaScript are mutable, so for the V above:

// You can do:
V.id = 37; // No errors
console.log(V.id) // 37

// But you still can't do:
V = { id: 37 }; // TypeError: Assignment to constant variable.

This applies to arrays too (because they are mutable as well).

Strings and Integers are immutable in JavaScript so the only way to change a string or integer value in JavaScript is to discard current memory space and re-label another one. That’s why if you have a numeric counter you need to increment in your program you would need to use let:

// Can't use const for this case:
let counter = 0;
counter = counter + 1 // Discard and re-label

If you need a variable to hold a changing scalar value (like the counter example above) then using let is okay. However, for most other cases it’s probably much better for you to stick with using const for all your variables.

2 — Arrow Functions

Arrow functions are probably the most used feature in Modern JavaScript.

But JavaScript has SO many ways to define functions. Why do we need another way?

// The arrow function syntax:

const doSomething = **() => {**
  // Function Scope**
};**

This new “shorter” syntax to define functions is popular not only because it’s shorter but also because it behaves more predictably with closures. This latter point is really what’s important so let’s learn what that’s all about.

Arrow functions give access to their defining environment while regular functions give access to their calling environment. This access is possible through the special this keyword:

  • The value of the this keyword inside a regular function depends on HOW the function was CALLED.

  • The value of the this keyword inside an arrow function depends on WHERE the function was DEFINED.

Here is a code example to expand on the explanation. Try to figure out what will be printed in Output #1 through #4 (last 4 lines):

this.whoIsThis = 'TOP'; // Identify this scope

**// 1) Defining
**const fancyObj {
  whoIsThis: 'FANCY', // Identify this object

  regularF: function () {
    console.log('regularF', this.whoIsThis);
  },

  arrowF: () => {
    console.log('arrowF', this.whoIsThis);
  },
};

**// 2) Calling
**console.log('TOP-LEVEL', this.whoIsThis); // It's "TOP" here


fancyObj.regularF(); **// Output #1 (Fancy)**
fancyObj.arrowF();   **// Output #2 (Top)**

fancyObj.regularF.call({whoIsThis: 'FAKE'}); **// Output #3 (Fake)**
fancyObj.arrowF.call({whoIsThis: 'FAKE'});   **// Output #4 (Top)**

You have a regular function (regularF) and an arrow function (arrowF) defined in the same environment, and called by the same caller. Here’s the explanation of the outputs in the last 4 lines:

  1. The regular function will always use its this to represent who called it. In the example above, the caller of both functions was the fancyObj itself. That’s why Output #1 was "FANCY".
  2. The arrow function will always print the this scope that was available at the time it was defined. That’s why Output #2 was "TOP".
  3. This holds true even if the caller was changed using .call, .apply, or .bind (which are functions that can be used to change the calling environment). The first argument to these functions becomes the new "caller". That’s why Output #3 will be "FAKE".
  4. The arrow function does not care about this caller change. It’ll still output "TOP".

You can experiment with this example at jsdrops.com/arrow-functions.

One other cool thing about arrow functions is that if the function only has a single return line (like square1 below) you can make it even more concise by removing the braces and the return keyword altogether (as in square2). You can also remove the parentheses around the argument if the function receives a single argument:

const square1 = (a) => {
  return a * a;
};

const square2 = (a) => a * a;

const square3 = a => a * a;

The last syntax is usually popular for functions that get passed to array methods like map, reduce, filter, and other functional programming methods:

console.log([1, 2, 3, 4].map(a => a * a));

Note that if you want to use the arrow-function one-liner version to make a function that returns an object, you’ll have to enclose the object in parenthesis because otherwise, the braces you think are for object literals are actually the scope of the function.

// Wrong
const objMaker = () => { answer: 42 };

// Right
const objMaker = () => ({ answer: 42 };

The above is actually one of the most common mistakes beginners do when working with libraries like React.

Guess what, the confusion about curly braces are NOT only about scopes and object literals. There are 2 more things you can do with curly braces. Keep reading.

3— Object Literals

You can create a JavaScript object in a few different ways but the most common way is to use an object literal:

const obj = {
  // key: value
};

This is easier than doing something like new Object(), which you can do if you want to.

Literal notation (AKA initializer notation) is very common. We use it for objects, arrays, strings, numbers, and even things like regular expressions:

const obj = {};     **// Object()**
const arr = [];     **// Array()**
const str = "";     **// String()**
const num = 0;      **// Number()**
const reg = /(?:)/; **// RegExp()**

Here are a few things you can with object literals:

const mystery = 'answer';
const InverseOfPI = 1 / Math.PI;

const obj = {
  p1: 10,        // Plain old object property (don't abbreviate)

  f1() {},       // Define a shorthand function property

  InverseOfPI,   // Define a shorthand regular property

  f2: () => {},  // Define an arrow function property

  [mystery]: 42, // Define a dynamic property
};

Did you notice that [mystery] thing? That is not an array or a destructuring thing (destructuring is explained next). It is how you define a dynamic property.

Interview Question: Given the code above, what is the value of obj.mystery?

42 is NOT the answer to this interview question

When you use the dynamic property syntax, JavaScript will first evaluate the expression inside [] and whatever that expression evaluates to becomes the object’s property.

For the example above, the obj object will have a property “answer” with the value of 42.

Another widely popular feature about object literals is available to you when you need to define an object with property names to hold values that exist in the current scope with the exact same names. You can use the shorthand property name syntax for that.

That’s what we did for the InverseOfPI variable above. That part of the object is equivalent to:

const obj = {
  InverseOfPI: InverseOfPI,
};

Objects are very popular in JavaScript. They are used to manage and communicate data and using these features will make the code a bit shorter and easier to read.

4 — Destructuring

The destructuring syntax uses the same braces and brackets you use with object/array literals, which makes it confusing sometimes! You need to inspect the context to know whether a set of braces ({}) or brackets ([]) are used as literal initializing or destructuring assignment.

Embedded content: https://twitter.com/samerbuna/status/1093902499059298306

In the tweet above, the { PI } on line 3 is an object literal while the {PI} on line 4 is used as a destructuring assignment.

It can really get a lot more complicated than that, but here are 2 simple general rules to identify what’s what:

  • If braces or brackets are on the left-hand side (LHS) of an assignment, they are for destructuring.

  • If braces or brackets are inside the parenthesis used to define a function they are for destructuring.

Example of destructuring:

**// 1) Destructure array items**
const [first, second,, forth] = [10, 20, 30, 40];

**// 2) Destructure object properties**
const { PI, E, SQRT2 } = Math;

These are both destructing because the brackets are on the LHS of the assignment.

Destructuring simply extracts named items out of an array (using their position) or properties out of an object (using their names) and into local variables in the enclosing scope. The 2 lines above are equivalent to:

**// 1) assuming arr is [10, 20, 30, 40]**
const first = arr[0];
const second = arr[1];
// third element skipped
const forth = arr[3];

**// 2)**
const PI = Math.PI;
const E = Math.E;
const SQRT2 = Math.SQRT2;

This is useful when you need to use a few properties out of a bigger object. For example, here’s a line to destructure Component, Fragment, and useState out of the React’s API.

const {Component, Fragment, useState} = React;

After this line, you can use these React API objects directly:

const [state, setState] = useState();

When designing a function to receive objects and arrays as arguments, you can use destructuring as well to extract named items or properties out of them and into local variables in the function scope. Here’s an example:

const circle = {
  label: 'circleX',
  radius: 2,
};

const circleArea = ({radius}, [precision = 2]) =>
  (Math.PI * radius * radius).toFixed(precision);

console.log(
  circleArea(circle, [5]) // 12.56637
);

I don’t really use arrays in function calls like this. This example is to only show how you can destructure both objects and arrays in function arguments.

The circleArea function is defined to receive an object first argument and an array second argument. These arguments are not named and not used directly in the function’s scope. Instead, their properties and items are destructured and used in the function scope. You can even give destructured element default values (as it’s done for the precision item).

In JavaScript, using destructuring with a single object as the argument of a function is an alternative to named arguments (which is available in other languages). It is much better than relying on positional arguments.

5 — Rest and Spread

Destructuring gets more interesting (and useful) when combined with the REST syntax and the SPREAD syntax, which are both done using the 3 dots (...) syntax but they do different things.

If you’ve worked with React components, you might have used these 3 same dots to spread “props”. The JavaScript spread syntax was inspired by React (and others), but the meaning of the 3 dots in React/JSX and in JavaScript is different.

The REST syntax is what you use with destructuring. The SPREAD syntax is what you use in object/array literals.

Here’s an example:

const [first, ...restOfItems] = [10, 20, 30, 40];

The 3-dots here, because they are in a destructuring call, represent a REST syntax. We are asking JavaScript here to destructure only 1 item out of this array (the first one) and then create a new array under the name restOfItems to hold the rest of the items (after removing the first one).

This is powerful for splitting the array and it’s even more powerful when working with objects to filter out certain properties from an object. For example, given this object:

const obj1 = {
  temp1: '001',
  temp2: '002',
**  firstName: 'John',
  lastName: 'Doe',
  // 98 other properties
**};

If you need to create a new object that has all the properties of obj1 except for temp1 and temp2, what would you do?

You can simply destructure temp1 and temp2 (and ignore them) and then use the rest operator to capture the remaining properties into a new object:

const { temp1, temp2, **...obj2** } = obj1;

How freaking cool is that?

REST syntax has nothing to do with REST APIs. Incidentally, when using REST APIs you end up with a lot of object properties that you do not need so you can use the REST syntax to pick the ones that you do. Have you looked at GRAPHQL yet??

The SPREAD syntax uses the same 3-dots to shallow-copy an array or an object into a new array or an object. This has nothing to do with destructuring but it’s commonly used to merge partial data structures into existing ones. It replaces the need to use the Object.assign method.

const array2 = [newItem0, **...array1,** newItem1, newItem2];

const object2 = {
  **...object2,**
  newP1: 1,
  newP2: 2,
};

When used in objects, property-name conflicts will resolve to taking the value of the last property.

What is shallow-copy?? Simple put, any nested arrays or objects will be shared between the copies. This is a similar story to memory-spaces and their labels, except here labels are cloned and made to label the exact same memory spaces.

7—Template Strings

You can define strings in JavaScript using either single quotes or double quotes:

const greeting = "Hello World";

const answer = 'Forty Two';

These 2 ways to define string literals in JavaScript are equivalent. Modern JavaScript has a third way to define strings and that’s using the backtick character.

const html = `
  <div>
    ${Math.random()}
  </div>
`;

Strings defined with the backtick character are called template strings because they can be used as a template with dynamic values. They support expressions interpolation. You can inject any JavaScript expression within the ${} syntax.

With template strings you can also have multiple lines in the string, something that was not possible with the regular-quoted strings. You can also “tag” templates strings with a function and have JavaScript execute that function before returning the string which is a handy way to attach logic to them. This tagging feature is used in the popular styled-components library (for React).

Backticks look very similar to single quotes, so make sure to train your eyes to spot template strings when they are used, and know when they’re needed.

8— Classes

JavaScript is flexible to a fault. It can be whatever you need it to be (object-oriented, functional, procedural, imperative, declarative, event-driven, etc).

Plain Object Oriented Programing (also, don’t abbreviate) was always possible with JavaScript but Modern JavaScript also added support for the Class syntax.

A class is the concept for a template or blueprint for you to define shared structure and behavior among similar objects. You can define new classes, make them extend other classes, and instantiate objects out of them using the new keyword. You can customize the construction of every object and define shared functions between these objects. Most importantly, you can do all that without bothering to understand the concept of prototype-based delegation!

You can tell I am not a fan of Classes. In fact, I get so much pleasure in removing them from code. It’s why I am a big fan of the upcoming React Hooks.

Speaking of React Hooks, guess who recorded a 4 hour video course that covers them! I love how much simpler they make the code. I will not miss classes.

You’ll find classes in many projects and examples and to be fair, some of their usage is actually not bad. You, on the other hand, should only use them when you fully understand why you think you need them.

Here’s a standard class example that demonstrates the magical things you can do with classes:

class Person {
  constructor(name) {
    this.name = name;
  }
  greet() {
    console.log(`Hello ${this.name}!`);
  }
}

class Student extends Person {
  constructor(name, level) {
    super(name);
    this.level = level;
  }
  greet() {
    console.log(`Hello ${this.name} from ${this.level}!`);
  }
}

const o1 = new Person('Max');
const o2 = new Student('Tina', '1st Grade');
const o3 = new Student('Mary', '2nd Grade');
o3.greet = () => console.log('I am special!');

o1.greet(); // Hello Max!
o2.greet(); // Hello Tina from 1st Grade!
o3.greet(); // I am special!

In this example, you have a Person class and a Student class that extends the Person class, because every student is a person, d’oh!.

Both classes define a constructor function. The constructor function is a special one that gets called every time you instantiate an object out of the class, which you do using the new keyword (used above to instantiate o1, o2, o3).

The arguments you pass to the new call are accessible in the constructor function of the class. The Person class expects a name argument and it stores that value on the instance using the this keyword. The Student class expects a name and a level arguments. It stores the level value on its instance and since it extends the Person class it can call the super method with the name argument, which will invoke the Person class constructor function and store the name as well. Both classes define a greet function that uses the values they store on each instance. A greet method was also directly defined on o3.

When all 3 greet methods are invoked, o1 will use the method from its class, the Person class, o2 will use the method from the Student class, and o3 will use its own directly defined method.

9 — Async/Await

When you need to work with asynchronous operations, you usually have to deal with promise objects. A promise is an object that might deliver data at a later point in the program, or it might crash and deliver an error instead.

An example of an async function that returns a promise is the Web fetch API that’s natively available in some browsers.

const fetchData = () => {
  fetch('[https://api.github.com').then(resp](https://api.github.com').then(resp) => {
    resp.json().then(data => {
      console.log(data);
    });
  });
};

The fetchData function fetches information from the top-level GitHub API. Since fetch returns a promise, to consume that promise we do a .then call on the result of fetch and supply a callback function in there. The callback function will receive the raw response from the API. If you need to parse the data as JSON, you need to call the json() method on the raw response object. That json() method is also an asynchronous one, so it returns a promise as well. To get to the data, you need another .then call on the result of the json() method and in the callback of that you can access the parsed data.

As you can see, this syntax might get complicated with more nesting of asynchronous operations or when you need to combine this with any looping logic. You can simplify the nesting above by making each promise callback return the promise object, but the whole .then syntax is a bit less readable than the modern way to consume promises in JavaScript which is using async/await:

const fetchData = **async** () => {
  const resp = **await** fetch('[https://api.github.com'](https://api.github.com'));
  const data = **await** resp.json();
  console.log(data);
};

You just await on the async call (the one that returns a promise) and that will give you back the response object directly. Then, you can await on the json() method to access the parsed JSON data. To make await calls work, you need to label the function as async.

The async/await syntax is just another way for you to consume promises (but without having to deal with .then calls). It’s a bit simpler to read but keep in mind that once you await on anything in a function that function itself becomes asynchronous and it will return a promise object (even if you don’t return anything from it).

Note: no return statement in the IIFE but a promise was returned anywayNote: no return statement in the IIFE but a promise was returned anyway

For error-handling (when promises reject, for example) you can combine the async/await syntax with the plain-old try/catch statement (and you should do that all the time).

10 — Import/Export

This is not about international trade. Modern JavaScript introduced these 2 statements to provide a solution for “module dependency management”.

Module dependency management is just the fancy term to describe JavaScript files that need each other.

A file X that needs to use a function from file Y can use the import statement to declare this dependency. The function has to be exported first in order for any other files to import it. For that, you can use the export keyword:

// Y.js

export const functionY() {}

Now any file can import this named functionY export:

// X.js

import {functionY} from './Y'; // Assuming X & Y are same-level

// functionY();

The {functionY} syntax is not destructuring! It’s importing of a named export. You can also export without names using this other syntax:

// Y.js

export default function () {}

When you import this default Y export, you can give it any name you want:

// X.js

import funcion42 from './Y';

// function42();

Thanks for reading.

StratusUpdate

Keep your head in the cloud with Stratus Update

With our new weekly newsletter we’ll keep you up to date with a curated selection of the the latest cloud services, projects and best practices.
Click here to read the latest issue.