Modern JavaScript

February 3, 2023 (1 year ago)

Table of contents:

Falsy, Truthy & Nullish values

JavaScript implicitly converts values when they are in a boolean context (such as in conditional operations)

Falsy values

Falsy values, when implicitly converted, are equivalent to false:

  • false
  • ''
  • 0, -0, 0n, -0n, 0.0, -0.0
  • null
  • undefined
  • NaN

Truthy values

Truthy values, when implicitly converted, are equivalent to true:

  • true
  • Any non-empty string
  • Any number less than 0 ou greater than 0 (including floating-point numbers)
  • Infinity, -Infinity
  • [] (Array, empty or non-empty)
  • {} (Object, empty or non-empty)

Nullish values

Nullish values are always falsy and there are two possible values:

  • null
  • undefined

Numeric separators

Numeric separators improve the readability of literal numbers by adding a visual separator between groups, using an underscore ( _ )

const oneMillion = 1_000_000; // 1000000
const oneBillion = 1_000_000_000; // 1000000000
const oneMillionFiftyCents = 1_000_000.5; // 1000000.50

Template literals

Template literals (or template strings) is a syntactic sugar to build strings.

Don't do this:

const fullname = "My name is " + username + " " + lastname;

Do this instead:

const fullname = `My name is ${username} ${lastname}`;

It's good practice to use template literals for concatenation and + for mathematical operations.

Shorthand property names

We can use a shorter syntax for property names in object literals to simplify object creation.

Don't do this:

const person = {
  username: username,
  lastname: lastname,
  getFullname: getFullname,
};

Do this instead:

const person = {
  username,
  lastname,
  getFullname,
};

Don't do this:

const person = {
  person.name,
};

This way will throw an Uncaught SyntaxError: Unexpected token '.'

Do this instead:

const person = {
  name: person.name,
};

Bonus:

const person = {
  fullname: "Lucas Bittencourt",
  getFullname() {
    return this.fullname;
  },
};

Learn more about here

Logical operator AND

The logical operator AND ( && ) returns the right-hand value if the left-hand value is truthy.

console.log(false && "Lucas Bittencourt"); // false
console.log(undefined && "Lucas Bittencourt"); // undefined
console.log(null && "Lucas Bittencourt"); // null
console.log(NaN && "Lucas Bittencourt"); // NaN

console.log(true && "Lucas Bittencourt"); // Lucas Bittencourt
console.log(1 && "Lucas Bittencourt"); // Lucas Bittencourt

console.log(1 && true && "Lucas Bittencourt"); // 'Lucas Bittencourt'
console.log(1 && false && "Lucas Bittencourt"); // false

JSX:

{
  hasMinimumAge && <SomeComponent />;
}

Logical operator OR

The logical operator OR ( || ) returns the right-hand value if the left-hand value is falsy.

console.log(false || "Lucas Bittencourt"); // Lucas Bittencourt
console.log(undefined || "Lucas Bittencourt"); // Lucas Bittencourt
console.log(null || "Lucas Bittencourt"); // Lucas Bittencourt
console.log(NaN || "Lucas Bittencourt"); // Lucas Bittencourt

console.log(true || "Lucas Bittencourt"); // true
console.log(1 || "Lucas Bittencourt"); // 1

console.log(0 || false || "Lucas Bittencourt"); // Lucas Bittencourt
console.log(0 || true || "Lucas Bittencourt"); // true
console.log(1 || true || "Lucas Bittencourt"); // 1

Nullish Coalescing operator

The Nullish coalescing operator ( ?? ) returns the right-hand value if the left-hand value is nullish.

console.log("" ?? "Lucas Bittencourt"); // ''
console.log(0 ?? "Lucas Bittencourt"); // 0
console.log(true ?? "Lucas Bittencourt"); // true

console.log(null ?? "Lucas Bittencourt"); // "Lucas Bittencourt"
console.log(undefined ?? "Lucas Bittencourt"); // "Lucas Bittencourt"

console.log(undefined ?? null ?? "Lucas Bittencourt"); // 'Lucas Bittencourt'
console.log(undefined ?? false ?? "Lucas Bittencourt"); // false

Logical Assignment operator

We can now assign values conditionally using logical operators.

AND equals (&&=)

Only evaluates the right operand and assigns to the left if the left operand is truthy.

Don't do this:

let intersectionObserver = new IntersectionObserver();

function resetIntersectionObserver() {
  if (intersectionObserver) {
    intersectionObserver = new IntersectionObserver();
  }

  return intersectionObserver;
}

Do this instead:

let intersectionObserver = new IntersectionObserver();

function resetIntersectionObserver() {
  intersectionObserver &&= new IntersectionObserver();
  return intersectionObserver;
}

OR equals (||=)

Only evaluates the right operand and assigns to the left if the left operand is falsy.

Don't do this:

let intersectionObserver;

function getIntersectionObserver() {
  if (!intersectionObserver) {
    intersectionObserver = new IntersectionObserver();
  }

  return intersectionObserver;
}

Do this instead:

let intersectionObserver = null;

function getIntersectionObserver() {
  intersectionObserver ||= new IntersectionObserver();
  return intersectionObserver;
}

Nullish Coalescing equals (??=)

Only evaluates the right operand and assigns to the left if the left operand is nullish.

Don't do this:

let intersectionObserver = null;

function getIntersectionObserver() {
  if (intersectionObserver === null || intersectionObserver === undefined) {
    intersectionObserver = new IntersectionObserver();
  }

  return intersectionObserver;
}

Do this instead:

let intersectionObserver = null;

function getIntersectionObserver() {
  intersectionObserver ??= new IntersectionObserver();
  return intersectionObserver;
}

Optional Chaining

Optional Chaining is a safe way to access object properties that may be nullish.

Don't do this:

console.log(object.nullishReference.name);

This can lead to a runtime error: Uncaught TypeError: Cannot read properties of undefined (reading 'name')

Do this instead:

console.log(object.nullishReference?.name);

We also can use optional chaining in conditionals.

if (object.nullishReference?.name) {
  console.log("Hello, world");
}

We also can use optional chaining in arrays.

console.log(userList?.[0]?.name);

We also can use optional chaining in functions.

getUserData?.();
getUserData?.()?.name;

Destructuring assignment

The destructuring assignment syntax is a JavaScript expression that makes it possible to unpack values from arrays, or properties from objects, into distinct variables.

Don't do this:

const username = user.username;
const lastname = user.lastname;

Do this instead:

const { username, lastname } = user;

We can destructure from functions:

const { username, lastname } = getUser();

We can set a default value:

const { username = "Lucas", lastname } = user;

We can destructure nested objects:

const {
  username,
  lastname,
  information: { email, age },
} = user;

We can rename object properties when destructuring:

const { username: name } = user;

We can create another object using the rest operator when destructuring:

const { username, lastname, ...userInfo } = user;

We can destructure function parameters:

function getUsername({ name, lastname }) {
  return `${name} ${lastname}`;
}

Don't do this:

const user1 = userList[0];
const user2 = userList[1];

Do this instead:

const [user1, user2] = userList;

We can skip array elements when destructuring:

const [user1, user2, , user4] = userList;

We can destructure multidimensional arrays:

const [[a, , c], , [, h, i]] = myArray;

We can set a default value:

const [a = 1, b, c] = numberList;

We can destructure objects within arrays:

const [{ username, lastname }, user2] = userList;

We can destructure arrays as if they were objects:

const {
  0: user1,
  1: user2,
  2: { username, lastname },
} = userList;

We can create another array using the rest operator when destructuring:

const [user1, user2, ...moreUsers] = userList;

We can destructure function parameters:

function sumNumbers([a, b, c]) {
  return a + b + c;
}

Spread operator

Spread operator "expands" an array or object into its elements.

const newUser = { ...user, age: 25 };

const newUserList = [...userList, newUser];

We can spread nested object elements:

const newUser = {
  information: {
    ...user.information,
    username: "Lucas",
  },
  age: 25,
};

We can spread array elements as function arguments:

createUserList(argument1, argument2, ...arguments);

Rest operator

Rest operator collects multiple elements and "condenses" them into a single element.

const { username, lastname, ...otherInfo } = user;

const [a, b, c, ...otherNumbers] = numberList;

Rest parameters

Rest parameters represents variadic functions and provide a way to accept an indeterminate number of parameters.

function total(...numbers) {
  return sum(numbers);
}

Default function parameters

We can define default function parameters for optional arguments.

function getUsername(username = "Lucas Bittencourt") {
  return username;
}

We can set an object as default parameter:

function getUsername(user = { username: "Lucas Bittencourt" }) {
  return user.username;
}

Arrow functions expressions

Arrow functions are compact functions.

const getUser = (username, lastname) => {
  return {
    username,
    lastname,
  };
};

When an arrow function has only one statement, we can omit the return keyword and its brackets.

const getUsername = (username) => username.trim();

When returning a literal object, we need to ensure it is enclosed in parentheses:

const getUserInfo = (name) => ({
  name: name.trim(),
});

We can also return a literal array:

const getNumbers = () => [1, 2, 3];

We can have an asynchronus arrow function:

const getUsername = async (username) => Promise.resolve(username);

async/await

JavaScript async/await is syntactic sugar for promises.

Don't do this:

function fetchUser() {
  getUser().then((user) => console.log(user));
}

Do this instead:

async function fetchUser() {
  const user = await getUser();
  console.log(user);
}

top-level await

We can use a top-level await without the need for an asynchronus function.

const username = await Promise.resolve("Lucas Bittencourt");

try...catch...finally

The try...catch statement is comprised of a try block and either a catch block, a finally block, or both. The code in the try block is executed first, and if it throws an exception, the code in the catch block will be executed. The code in the finally block will always be executed before control flow exits the entire construct.

Don't do this:

function fetchUser() {
  getUser()
    .then((user) => console.log(user))
    .catch((error) => console.error(error))
    .finally(() => console.info("Código executado!"));
}

Do this instead:

async function fetchUser() {
  try {
    const user = await getUser();
    console.info(user);
  } catch (error) {
    console.error(error);
  } finally {
    console.log("Código executado!");
  }
}