Refactoring to Modern JavaScript

October 12, 2023
Brian Salazar
Refactoring to Modern JavaScript

Refactoring to Modern JavaScript 

It’s increasingly common for people learning JavaScript to start with the newest features without knowing that they are not in the JavaScript code base. If you want to better understand the programming language, it’s important to know why these new features were added, what problems they solve, and the advantages they offer.

Arrow Functions

The arrow functions were introduced as an alternative way to write functions. This new method of writing functions is very beneficial in some contexts. For example, in callback functions, using the full structure of a function definition creates a lot of noise, making the code very complicated to read. Arrow functions help simplify the code in this case.

One of the disadvantages of arrow functions is that the context of “this” is different from common functions. It’s recommended that if you need to use “this” it is better to use the conventional structure of functions.

Example

 The example below illustrates how using arrow functions in callbacks can simplify the code and make it more readable and easier to understand.

Vanilla JS:

const result = [10, 20, 30, 40]
.map(function (n) {
  return n * 2;
})
.filter(function (n) {
  return n > 25;
})

ES6

const result = [10, 20, 30, 40]
.map(n => n * 2)
.filter(n => n > 25);
**​​​**

Destructuring

Destructuring was added as a way to get data from the arrays and objects more conveniently. Symmetrical structure is used between how the array or objects are created and how the data is obtained.

Example

Below is an example of how destructuring and the use of the symmetric structure before and after the equal sign makes it easier to understand.

Vanilla JS

const array = [10, 20, 30, 40];

const val1 = array[0];
const val2 = array[1];

ES6

const array = [10, 20, 30, 40];
const [val1, val2, ...rest] = array;

Template Strings

Template strings are incredibly beneficial because they can, use any JavaScript expression within the strings, greatly simplifying what previously had to be done through concatenations.

Example

Vanilla JS

const firstName = 'Brian';
const lastName = 'Salazar';

const msg = 'Hello ' + firstName.toUpperCase() + ' ' + lastName.toUpperCase() + '!';

ES6

const firstName = 'Brian';
const lastName = 'Salazar';

const msg = `Hello ${firstName.toUpperCase()} ${lastName}!`;****

Default Parameters

Default parameters are used to avoid undefined parameters. While it’s possible to validate parameter values within a function, default parameters keep the code simpler.

Example

In this example, we have a function to print the sides of a rectangle receiving two of its sides as a parameter.  The results that can be obtained without using default parameters are pictured below.

function printRect(x, y) {
console.log([x, y, x, y]);
}


printRect();                // [undefined, undefined, undefined, undefined]
printRect(10);              // [10, undefined, 10, undefined]
printRect(10, 20);          // [10, 20, 10, 20]
printRect(10, 20, 30, 40);  // [10, 20, 10, 20]

By validating the parameters and adding a default value you can avoid undefined results. Using default params you can avoid the code inside the function to validate those parameters

Vanilla JS

function printRect(x, y) {
if (!x) x = 1;
if (!y) y = 2;
console.log([x, y, x, y]);
}


printRect();                // [1, 2, 1, 2]
printRect(10);              // [10, 2, 10, 2]
printRect(10, 20);          // [10, 20, 10, 20]
printRect(10, 20, 30, 40);  // [10, 20, 10, 20]
**​​​​​​​​​​​​​**

ES6

function printRect(x = 1, y = 2) {
console.log([x, y, x, y]);
}

printRect();                // [1, 2, 1, 2]
printRect(10);              // [10, 2, 10, 2]
printRect(10, 20);          // [10, 20, 10, 20]
printRect(10, 20, 30, 40);  // [10, 20, 10, 20]
**​​​​​​​​​**

Rest Operator

In our examples of default parameters, the printRect function expected two parameters, but in some cases a function with more parameters was called. What happens with these parameters?

JavaScript will accept all the parameters that you want to send in a function even if that function does not expect them, but it will make the parameters previously defined available. In JavaScript base code, a variable inside each function called “arguments” takes all the variables received in the function, even those that are not previously defined.

The arguments variable has several disadvantages. The new rest operator was created to improve this functionality. The distinction between these two methods is explained here:

Arguments

Rest Operator

Looks like an array but doesn't have all the array methods like filter, find, slice, splice

Is a real array; you can use any method of a normal array so it has many more advantages 

Will take all the params, even the ones you defined

Will only take the rest params from the function,  making  it easy to work with these variables

Example console.log, it takes all the parameters that are sent and prints them no matter how many there are

Example console.log, it takes all the parameters that are sent and prints them no matter how many there are

N/A

Can't be defaulted, is always an array

N/A

Has to be last parameter of the function

Example

In the first image using arguments, you must transform the arguments variable into an array to be able to use the slice function.  One advantage of the rest operator is that it is not necessary to convert it into an array,

Vanilla JS

function multiplyWithFactor(factor) {
if (!factor) factor = 0;
var nums = Array.prototype.slice.call(arguments);
var result = factor;
for (var i = 1; i < nums.length; i++) {
  result *= nums[i];
}
return result;
}**​​​​​​​​​​​**

ES6

function multiplyWithFactor(factor = 1, ...args) {
let result = factor;
for (const n of args) {
  result *= n;
}
return result;
}

Spread Operator

Spread operator looks very similar to the rest operator, but it is used in arrays and objects. Unlike the rest operator, it does not create an array. The spread operator returns the separated data, making it  very useful to clone or create new arrays and objects from existing ones.

Example

An example that identifies all the benefits of the spread operator is the functions to clone or merge objects. Doing this with JavaScript base code can be very complicated, but it is greatly simplified using spread operator.

Vanilla JS

function objectMerge(obj1, obj2) {
var ret = {};
for (var key in obj1) {
  if (Object.prototype.hasOwnProperty.call(obj1, key)) {
    ret[key] = obj1[key];
  }
}
for (var key in obj2) {
  if (Object.prototype.hasOwnProperty.call(obj2, key)) {
    ret[key] = obj2[key];
  }
}
return ret;
}
**​​​​​​​​​​​​**

ES6

function objectMerge(obj1, obj2) {
return {
  ...obj1,
  ...obj2
}
}

Let & Const

JavaScript is a highly flexible language, but this flexibility means that we can do things that are imprecise or that generate undesirable results.

The use of variables is a clear example due to the hoisting of a JavaScript feature that allows to use a variables’ value in its scope before the line it is declared Variables can be declared within a scope and accessed within a previous scope as seen in the image, as it tries to access the variable before the assignment will return undefined but not an error.

function printNum() {
console.log(num);        // undefined
if (true) {
  var num = 10;
  console.log(num);      // 10
}
console.log(num);        // 10
}

Let and const were introduced to replace vars to have more control over the variables and ensure that they are used only in the scope in which they are declared. 

 The let and const variable is said to be in a “temporal dead zone” (TDZ) from the start of the block until the code execution reaches the line where the variable is declared and initialized.

While inside the TDZ, the variable has not been initialized with a value, and any attempt to access it will result in a ReferenceError. The variable is initialized with a value when execution reaches the line of code where it was declared. If no initial value was specified with the variable declaration, it will be initialized with a value of undefined.

We can see the difference between let/const and vars in the next example:

ES6

{ // TDZ starts at the beginning of scope
console.log(bar);    // undefined
console.log(foo);    // ReferenceError
var bar = 1;
let foo = 2;          // TDZ ends with initialization
}

Bottom line

Migrating your codebase from vanilla JavaScript to ES6 unlocks a range of modern language features and improvements that can greatly enhance your development process. From cleaner syntax to improved code organization and better error handling, the benefits of refactoring to ES6 are substantial. Embracing ES6 allows you to write more elegant and efficient code, improving the overall quality of your JavaScript applications. 

With its enhanced readability, maintainability, and advanced features, refactoring to ES6 is a valuable investment in the future of your codebase. Make sure to take advantage of the power and simplicity offered by ES6 in your JavaScript projects.