Chapter 2: Modern JavaScript

There have been many updates to the modern JavaScript language over the last few years, and you must understand some of the new features if you’re trying to learn React.

React encourages software developers to use modern JavaScript, so this post gives you an overview of the newest features of modern JavaScript.

So today, let’s look at some of these concepts which every JS dev must be aware of.

Let’s get started and dive into the things you need to know about JS.

Modern JavaScript

ES5 and ES6

The oversight of the JavaScript language specification is in the hands of ECMA, a non-profit entity, which has standardized this language as ECMAScript.

The terms “ES5” and “ES6” are often heard in reference to the 5th and 6th editions of the ECMAScript standard, unveiled in 2009 and 2015 respectively. ES5 serves as a foundational implementation with extensive support across various platforms, while ES6 heralds substantial enhancements over ES5, retaining backward compatibility. Following the launch of ES6, ECMA has embarked on yearly updates to the standard, progressively modernizing the language. In numerous settings, the label “ES6” broadly covers all advancements made post-ES5, not solely the ES6 specification.

Web browsers face a challenge in keeping up with such a swiftly evolving language and, in reality, they fall short. Features introduced in ES6 and subsequent updates aren’t universally supported across all browsers. To sidestep the risk of code failure due to unimplemented language features, modern JavaScript frameworks utilize a method known as transpiling. This technique transforms modern JavaScript code into its ES5 equivalent, ensuring functional consistency across all platforms. Thanks to transpiling, JavaScript developers are alleviated from the concern over varying levels of browser support for different JavaScript language features.

Modules (Import/Export)

Modern JavaScript allows developers to work with modules, making code organization and reuse much easier.

The import and export statements are used to handle modularization.

In the era of traditional JavaScript applications tailored for browsers, the concept of “importing” functions or objects from other modules might have seemed foreign. The practice was straightforward – you just embedded <script> tags to load the required dependencies, which then became available in the global scope, accessible to any JavaScript code operating within the current page’s context.

Transitioning to the modern landscape, contemporary JavaScript front-end frameworks have ushered in a more organized and rational dependency model, thanks to the integration of advanced tooling. This modern model thrives on the principles of imports and exports.

Now, let’s expand on this:

Import/Export in Modern JavaScript:

The import/export syntax is a hallmark of modern JavaScript, facilitating a modular programming approach. Here are some additional insights:

Modular Code: The import/export syntax enables developers to organize code into manageable modules, each with a specific purpose. This modularity promotes code reuse, maintainability, and testing.

// math.js
export function add(a, b) {
  return a + b;
}

// app.js
import { add } from './math.js';
console.log(add(2, 3));  // Output: 5

Named and Default Exports: JavaScript modules can have named exports (several per module) and a default export (one per module).

// utils.js
export function multiply(a, b) {
  return a * b;
}

export default function square(x) {
  return x * x;
}

// app.js
import square, { multiply } from './utils.js';
console.log(square(4));  // Output: 16
console.log(multiply(2, 3));  // Output: 6

Scope Management: Unlike the old method where dependencies were loaded into the global scope, the import/export syntax encapsulates scope within modules, preventing global namespace pollution.

Static Analysis and Tree Shaking: With import/export, tools can perform static analysis to determine which parts of modules are used. This enables tree shaking, a process that removes dead code, optimizing the application’s size.

Dynamic Imports: Besides static imports, modern JavaScript supports dynamic imports, allowing developers to load modules on demand, further optimizing performance.

// Dynamic import
import('./module.js')
  .then(module => {
    module.function();
  });

Variables and Constants

In older versions of JavaScript, declaring variables with the var keyword was a bit messy due to scoping issues. However, starting from ES6, Modern JavaScript introduced the let and const keywords making variable declaration much clearer and more predictable. The let keyword is now used for variables that may change over time, while const is used for variables that remain constant. Although var was common in older code, it’s better to use let now because it helps avoid some of the scoping problems that var has.

The let and const keywords provide block-scoped variable declarations, making code more readable and maintainable.

Prior to ES6, JavaScript relied on the var keyword, which only allowed for function and global scope. There was no scope at the block level. Modern JavaScript added block scoping with the addition of let and const.

To define a variable, simply prefix it with the let keyword or const keyword:

let a;

const a = someValue;

You can also declare a variable and give it an initial value simultaneously in Modern JavaScript.

Here’s how you can do it using let, and const:

let a = 10;

const a = 10;

If no initial value is specified, the variable is given a value as undefined.

Undefined Initialization: When you declare a variable without assigning a value to it, JavaScript initializes it with the special value undefined.

let a;
console.log(a);  // Output: undefined

In modern JavaScript, using const is a good practice when you have a variable whose value should not change over time. It helps make your code more readable and predictable, as developers can easily see which variables are meant to be constant.

const MAX_USERS = 100;

console.log(MAX_USERS);  // Output: 100
MAX_USERS = 101;  // Error: Assignment to constant variable.

In this example:

  1. We declare a constant named MAX_USERS and initialize it with a value of 100.
  2. We then log the value of MAX_USERS to the console, which outputs 100.
  3. Finally, we attempt to reassign a new value 101 to MAX_USERS, which results in an error because MAX_USERS is a constant and cannot be reassigned.

But, if you assign a mutable object (like an array or an object) to a constant, you can still modify the content of the object, but you cannot reassign a new object to the constant.

Here’s an example:

const userProfiles = [];

console.log(userProfiles);  // Output: []

// You can push items into the array
userProfiles.push({ name: 'John Doe', age: 25 });
console.log(userProfiles);  // Output: [{ name: 'John Doe', age: 25 }]

// You can also modify the objects within the array
userProfiles[0].age = 26;
console.log(userProfiles);  // Output: [{ name: 'John Doe', age: 26 }]

// However, you cannot reassign a new array or object to the constant
userProfiles = [{ name: 'Jane Doe', age: 24 }];  // Error: Assignment to constant variable.

In this example:

  1. A constant userProfiles is initialized as an empty array.
  2. We then push an object into the userProfiles array, and modify the object’s property within the array.
  3. Finally, we attempt to reassign a new array to userProfiles, which results in an error since userProfiles is a constant and cannot be reassigned.

When you create a constant with const, what you’re really doing is creating a constant reference to a value, not a constant value itself. This means that the constant userProfiles will always refer to the same object, but the object itself can change.

It may be confusing but let me try me explain this in more simple layman’s words.

Here’s a simplified analogy:

Imagine userProfiles as a box and const as a label with a name on it. Once you stick the label on the box, the label’s name (in this case, userProfiles) always points to that box and can’t be moved to another box. However, you can still open the box and change what’s inside of it. So, in JavaScript, you can change the content of the object that userProfiles refers to (like adding or modifying properties), but you can’t change which object userProfiles refers to.

In technical terms, objects and arrays in JavaScript are mutable, and when you perform actions like pushing items to an array or modifying object properties, you’re not changing the reference to the object or array—you’re changing the content of the object or array itself. Hence, it’s allowed even when the array or object is assigned to a constant.

Wrap up of let and const

  • The keywords let and const introduce block scoping in JavaScript.
  • Declaring a variable with let prevents us from re-defining or re-declaring another let variable with the same name within the same scope (function or block scope), though it allows us to re-assign a new value to it.
  • Declaring a variable with const disallows re-defining or re-declaring another const variable with the same name within the same scope (function or block scope). However, if the variable is of a reference type like an array or object, we can modify the values stored in that variable.

Explore more about let and const in the JavaScript reference documentation. It offers detailed information that will enhance your understanding and coding skills.


Equality and Inequality Comparisons

Modern JavaScript have introduced new comparison operators === and !==, providing a more predictable way to compare values.

In JavaScript, there are two primary types of comparisons: equality and inequality. These comparisons can be either strict or type-converting.

1. Equality Comparisons:

  • Strict Equality (===): This checks whether two values are equal in type and value without attempting to convert the types.
3 === 3;  // true
'3' === 3;  // false
  • Loose Equality (==): This checks whether two values are equal in value, and it attempts to convert the types if they are not.
3 == 3;  // true
'3' == 3;  // true

2. Inequality Comparisons:

  • Strict Inequality (!==): This checks whether two values are not equal in type or value without attempting to convert the types.
3 !== 3;  // false
'3' !== 3;  // true

Loose Inequality (!=): This checks whether two values are not equal in value, and it attempts to convert the types if they are not.

3 != 3;  // false
'3' != 3;  // false

It’s advisable to use the newer operators === and !== for all equality and inequality comparisons. This practice ensures clearer and more reliable comparison outcomes, making your code easier to read and debug.

Additional Considerations:

  • Besides these, Modern JavaScript also supports relational comparisons (<, >, <=, >=) which are useful for comparing numeric values.
  • When comparing objects, arrays, or functions, JavaScript compares the references, not the actual content.

Recap:

Understanding the nuances between strict and loose comparisons, and when to use each, is crucial for writing accurate and bug-free code. By adhering to best practices, like preferring strict comparisons, you set a solid foundation for robust JavaScript code.

Refer to the Strict equality and Strict inequality operators section in the JavaScript reference documentation for further insights.


Arrow Functions

Arrow functions offer a shorter syntax for writing function expressions and automatically bind this to the surrounding code’s context.

const myFunction = (a, b) => a + b;

The expression const myFunction = (a, b) => a + b; is a concise way to define a function in JavaScript using the arrow function syntax introduced in ES6.

Let’s break down the expression:

  1. const myFunction:
    • This part declares a constant named myFunction. In JavaScript, functions act as first-class objects, allowing assignment to variables, passage as arguments, and return from other functions.
  2. = (a, b) =>:
    • This part is the arrow function syntax. The parameters a and b are enclosed in parentheses, followed by the arrow =>. This syntax is a shorter alternative to the traditional function expression.
  3. a + b:
    • This is the body of the arrow function. In this concise form, the expression a + b is automatically returned. This is equivalent to { return a + b; } in a traditional function expression.

Putting it together, const myFunction = (a, b) => a + b; declares a constant named myFunction that references an arrow function. This function accepts two parameters, a and b, and returns the sum of their values.

How you can use this function:

You can now use myFunction to add two numbers together.

const result = myFunction(5, 3);
console.log(result);  // Output: 8

Benefits of Arrow Functions:

  • Conciseness: Arrow functions are more concise than traditional function expressions.
  • Lexical this: Arrow functions capture the this value of the enclosing context, which can be beneficial in certain scenarios.

Okay, before going ahead let’s do a comparison with traditional function expression.

For comparison, here’s how the same function would be written using a traditional function expression:

const myFunction = function(a, b) {
  return a + b;
};

Arrow functions provide a modern and concise way to define functions in JavaScript, making your code more readable and maintainable.

So, should I always use Arrow Functions?

While arrow functions are a great addition to modern JavaScript, they are not always the best choice. Let’s see some of the issues which we can face while using them.

No Named Functions:

  • Arrow functions are anonymous, which can make debugging more difficult. Named functions provide better stack traces in error messages.
function add(a, b) {  // Named function
  return a + b;
}

No this Binding:

  • Arrow functions capture the this value from their surrounding context, which can be problematic in object-oriented programming or when working with event handlers
const obj = {
  value: 10,
  getValue: function() {  // Traditional function to access object's properties
    return this.value;
  }
};

const obj = {
  value: 'hello',
  createArrowFunction: function() {
    return () => console.log(this.value);
  },
  createRegularFunction: function() {
    return function() { console.log(this.value); };
  }
};
const arrowFunc = obj.createArrowFunction();
arrowFunc();  // Output: 'hello'
const regularFunc = obj.createRegularFunction();
regularFunc();  // Output: undefined (or throws a TypeError in strict mode)

Not Suitable for Object Methods:

  • Arrow functions are not ideal for methods on objects since they don’t have their own this context.
const obj = {
  value: 10,
  getValue() {  // Short method syntax
    return this.value;
  }
};

Less Readable in Complex Scenarios:

  • In complex scenarios, or when the function body has multiple statements, arrow functions can make code less readable.
const complexFunction = (a, b) => {  // Might be less readable
  const c = a * b;
  const d = c + a;
  return d + b;
};

Not Suitable for Constructors:

  • Arrow functions cannot be used as constructors. Traditional functions can be used to create new objects using the new keyword.
const ArrowFunction = () => {};
// Attempting to use ArrowFunction as a constructor
const instance = new ArrowFunction();  // TypeError: ArrowFunction is not a constructor

No Access to new.target:

  • The new.target meta-property helps identify if a constructor was called using the new keyword. Arrow functions do not have access to new.target since they can’t be used as constructors.
function RegularFunction() {
  console.log(new.target);
}

const arrowFunction = () => console.log(new.target);

new RegularFunction();  // Output: function RegularFunction()
arrowFunction();  // Output: undefined

No Generator Functions:

  • Arrow functions cannot act as generator functions; hence, they can’t use the yield keyword within their body. Generator functions are defined using the function* syntax and are used to work with iterators and generators.
// Attempting to create a generator function using arrow syntax
const genFunc = *() => {
  yield 1;
};  // SyntaxError: Unexpected token '*'

// Correct way to create a generator function
function* correctGenFunc() {
  yield 1;
}

These examples demonstrate the different behaviours and limitations of arrow functions compared to traditional function expressions, particularly in the context of this binding, constructor usage, accessing new.target, and creating generator functions.

Understanding these nuances is crucial for writing robust and bug-free JavaScript code. It’s always about choosing the right tool for the job.

Refer to the Arrow function expressions section in the JavaScript reference documentation for further insights.


Template Literals

Template literals allow for easier string interpolation and multi-line strings.

Often, there’s a need to construct a string that incorporates both fixed text and variables.

ES6 uses template literals for this:

const name = 'Twitter';
console.log(`Hello, ${name}!`);  // Outputs: Hello, Twitter

Template literals are a new kind of string literals that allow for string formatting and multi-line strings.

They are enclosed by backticks (“) instead of single or double quotes.

const greeting = `Hello, World!`;

Multi-Line Strings:

  • You can create multi-line strings without needing to use the newline character (\n) or string concatenation.
const poem = `Roses are red,
Violets are blue,
Sugar is sweet,
And so are you.`;

Expression Interpolation:

  • You can embed expressions within template literals using ${expression} syntax.
const name = 'Ankur';
const greeting = `Hello, ${name}!`;
console.log(greeting);  // Output: Hello, Ankur!

Tagged Templates:

  • Template literals can be tagged, allowing you to parse template literals with a function.
function highlight(strings, ...values) {
  // Some processing...
}

const age = 25;
const highlighted = highlight`I am ${age} years old.`;

String Interpolation:

String interpolation is a feature provided by template literals, allowing you to insert values into strings in a more readable and concise manner.

const age = 36;
const text = `I am ${age} years old.`;
console.log(text);  // Output: I am 36 years old.

In this example, the value of the variable age is inserted into the string at the location of ${age}.

Advantages:

  • Readability: Template literals and string interpolation make the code more readable and easier to understand.
  • Ease of Use: They simplify the syntax for complex string manipulations and formatting.
  • Flexibility: Tagged templates provide a way to customize string parsing and processing.

These features significantly enhance the ease and efficiency of string handling in JavaScript, making code more clean and maintainable.

Refer to the Template literals (Template strings) section in the JavaScript reference documentation for further insights.


Ready to continue? Start reading A Complete Guide on How to Create a Project with Vite, Component Lifecycles, Props, Styling & More

If you like this blog and want to read more about ReactJS and JavaScript then start reading some of recent articles.