Learn ECMAScript - Second Edition

4 (1 reviews total)
By Mehul Mohan , Narayan Prusty
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Getting Started with ECMAScript

About this book

Learn ECMAScript explores implementation of the latest ECMAScript features to add to your developer toolbox, helping you to progress to an advanced level. Learn to add 1 to a variable andsafely access shared memory data within multiple threads to avoid race conditions.

You’ll start the book by building on your existing knowledge of JavaScript, covering performing arithmetic operations, using arrow functions and dealing with closures. Next, you will grasp the most commonly used ECMAScript skills such as reflection, proxies, and classes. Furthermore, you’ll learn modularizing the JS code base, implementing JS on the web and how the modern HTML5 + JS APIs provide power to developers on the web. Finally, you will learn the deeper parts of the language, which include making JavaScript multithreaded with dedicated and shared web workers, memory management, shared memory, and atomics. It doesn’t end here; this book is 100% compatible with ES.Next.

By the end of this book, you'll have fully mastered all the features of ECMAScript!

Publication date:
February 2018
Publisher
Packt
Pages
298
ISBN
9781788620062

 

Chapter 1. Getting Started with ECMAScript

ECMAScript 2017 (ES8) was released at the end of June 2017 by Technical Committee number 39 (TC39). It's part of ECMA, the institution that standardizes the JavaScript language under the ECMAScript specification. Currently, the standard aims to publish a new ES specification version once a year. ES6 was published in 2015 and ES7 was published in 2016. A lot changed when ES6 was released (arrow functions, classes, generators, module loaders, async programming, and so on) and even more interesting stuff keeps happening, as time goes by.

In this chapter, we'll be starting off with the fundamentals of JavaScript, starting off with ES6 basics and heading towards ES8 stuff. Furthermore, we'll be taking a look at some interesting aspects of traditional JS such as closures, and some new ones such as arrow functions.

As an autodidact, I highly recommend not only reading this book, but also trying to apply whatever you're learning here in some small but interesting projects. This will help you to retain a lot of stuff effortlessly.

In this chapter, we'll be covering:

  • Creating block-scoped variables using the let keyword
  • Creating constant variables using the const keyword
  • The spread operator and the rest parameter
  • Hoisting
  • Extracting data from iterables and objects using a destructuring assignment
  • Arrow functions
  • Closures and how to deal with them
  • Use of semicolons in JavaScript
  • Benchmarking let versus var versus const
  • The new syntaxes for creating object properties
 

The let keyword


The let keyword is used to declare a block-scoped variable (more on this later), optionally initializing it to a value. Programmers who come from a different programming language background, but are new to JavaScript, often end up writing error-prone JavaScript programs, believing that the JavaScript variables created using the traditional var keyword are block-scoped. Almost every popular programming language has the same set of rules when it comes to the variable scopes, but JavaScript acts a bit differently due to a lack of block-scoped variables. Due to the fact that JavaScript variables are not block-scoped, there are chances of memory leaks and JavaScript programs are harder to read and debug.

Declaring function-scoped variables

The JavaScript variables that are declared using the var keyword are called function-scoped variables. Function-scoped variables are accessible globally to the script, that is, throughout the script, if declared outside a function. Similarly, if the function scoped variables are declared inside a function, then they become accessible throughout the function, but not outside the function. Let's take a look at an example:

 var a = 12; // accessible everywhere
 function myFunction() {
   console.log(a); // alerts 12
   var b = 13;
   if(true) {
     var c = 14; // this is also accessible throughout the function!
     alert(b); // alerts 13
   }
   alert(c); // alerts 14
 }
 myFunction();
 alert(b); // alerts undefined

Clearly, variables initialized inside a function are restricted inside that function only. However, variables declared in a block scope (that is, inside curly braces { } that is not a function (that is, if statements)) can be used outside those blocks as well.

Declaring block-scoped variables

Variables that are declared using the let keyword are called block-scoped variables. Block-scoped variables behave the same way as function-scoped variables when declared outside a function, that is, they are accessible globally. But when block-scoped variables are declared inside a block, they are accessible inside the block that they are defined in (and also any sub-blocks) but not outside the block:

 let a = 12; // accessible everywhere
 function myFunction() {
   console.log(a); // alerts 12
   let b = 13;
   if(true) {
     let c = 14; // this is NOT accessible throughout the function!
     alert(b); // alerts 13
   }
alert(c); // alerts undefined
 }
 myFunction();
 alert(b); // alerts undefined

Study the code carefully. This is the same as the preceding example, but with  var replaced by let everywhere. Observe how C alerts undefined now (let makes it inaccessible outside if {}).

Re-declaring variables

When you declare a variable using the var keyword that is already declared using  the var keyword (in the same scope) then it's overwritten. Consider this example:

var a = 0;
var a = 1;
alert(a); // alerts 1
function myFunction() {
 var b = 2;
 var b = 3;
 alert(b); // alerts 3
}
myFunction();

The result is as expected. But the variables created using the let keyword don't behave in the same way.

When you declare a variable using the let keyword that is already declared using the let keyword in the same scope, then it throws a SyntaxError exception. Consider this example:

let a = 0;
let a = 1; // SyntaxError
function myFunction() {
 let b = 2;
 let b = 3; // SyntaxError
 if(true) {
    let c = 4;
    let c = 5; // SyntaxError
 }
}
myFunction();

When you declare a variable with a name that's already accessible in a function (or inner function), or is a sub-block using var or the let keyword respectively, then it's a different variable. Here, is an example this shows the behavior:

 var a = 1;
 let b = 2;
 function myFunction() {
   var a = 3; // different variable
   let b = 4; // different variable
   if(true) {
     var a = 5; // overwritten
     let b = 6; // different variable
     console.log(a); // 5
     console.log(b); // 6
 }
     console.log(a); // 5
     console.log(b); // 4
 }
 myFunction();
 console.log(a);
 console.log(b);

Closures and let keyword

Congratulations on making it to here! Let's face it, JavaScript has got some weird (and some bad) sides. Closures are on the weird side of JavaScript. Let's see what the term closure actually means.

When you declare a local variable, that variable has a restricted scope, that is, it cannot be used outside that particular scope within which it is declared (depends on var and let). As discussed earlier, local variables are not available outside the block (as in the case of let) or function scope (as in the case of var or let).

Let's take a look at the following example to understand what the preceding paragraph states:

function() {
  var a = 1;
  console.log(a); // 1
} 
console.log(a); // Error

When a function is fully executed, that is, has returned its value, its local variables are no longer required and cleaned from memory. However, a closure is a persistent local variable scope. 

Consider the following example:

function counter () {
  var count = 0;
  return function () {
    count += 1;
    return count;
  }
}

Clearly, the returned function makes use of the local variable to the counter() function. What happens when you call counter?

let myCounter = counter(); // returns a function (with count = 1)
myCounter(); // now returns 2
myCounter(); // now returns 3

Look carefully, we are not executing counter() again and again. We stored the returned value of the counter in the myCounter variable and then kept calling the returned function.

The returned myCounter function will count up by one each time it's called. When you call myCounter(), you are executing a function that contains a reference to a variable (count), which exists in a parent function and technically should've been destroyed after its complete execution. However, JavaScript preserves used variables inside a returned function in a kind of different stack. This property is called a closure.

Closures have been around for a long time, so what's different? Using it with the let keyword. Have a look at this one:

for(var i=0;i<5;i++){
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

The output will be:

 5 5 5 5 5 

Why? Because till the time setTimeout fires, the loop has already ended and the i variable was already 5. But this does not happen with let:

for(let i=0;i<5;i++){
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

Output:

0 1 2 3 4

The fact that let binds variables to the block (thus, in this case, the for loop) means that it binds the variable to every iteration. So, when the loop is finished, you have five setTimeout functions (with i = 0, 1, 2, 3, 4) waiting to fire one after another.

let achieves this by creating a closure of its own in every iteration. This happens behind the scenes with let, so you do not need to code that aspect.

To fix this code without let, we'll need to create an Immediately Invoked Function Expression (IIFE), which looks something like this:

for(var i=0;i<5;i++){
  (function(arg) {
    setTimeout(function() {
      console.log(arg); 
    }, 1000);
  }(i));
}

This is more or less what let does behind the scenes. So what happened here?

We created an anonymous function that is immediately invoked at every loop cycle with the correct i value associated with it. Now, this function has the correct i value passed as arg in the function argument. Finally, we use console.log after a second to get the correct output as 0 1 2 3 4.

So you can observe, a simple let statement can simplify the code a lot in such cases.

 

The const keyword


Using the const keyword, you can create variables that cannot change their values (hence they're  called constants) once they're initialized, that is, you cannot reinitialize them with another value later in your code.

If you try to reinitialize a const variable, a read-only exception is thrown. Furthermore, you cannot just declare and not initialize a const variable. It'll also throw an exception.

For instance, you might want your JavaScript to crash if someone tries to change a particular constant, say pi, in your calculator. Here's how to achieve that:

const pi = 3.141;
pi = 4; // not possible in this universe, or in other terms, 
        // throws Read-only error

The scope of const variables

The const variables are block-scoped variables, that is, they follow the same scoping rules as the variables that are declared using the let keyword. The following example demonstrates the scope of the constant variables:

const a = 12; // accessible globally
function myFunction() {
  console.log(a);
  const b = 13; // accessible throughout function
  if(true) {
    const c = 14; // accessible throughout the "if" statement
    console.log(b);
  }
console.log(c);
}
myFunction();

The output of the preceding code is:

12
13
ReferenceError Exception

Here, we can see that constant variables behave in the same way as block-scoped variables when it comes to scoping rules.

Referencing objects using constant variables

When we assign an object to a variable, the reference of the object is what the variable holds and not the object itself. So, when assigning an object to a constant variable, the reference of the object becomes constant to that variable and not to the object itself. Therefore, the object is mutable. Consider this example:

const a = {
  name: "Mehul"
};
console.log(a.name);
a.name = "Mohan";
console.log(a.name);
a = {}; //throws read-only exception

The output of the preceding code is:

Mehul
Mohan
<Error thrown>

In this example, the a variable stores the address (that is, reference) of the object. So the address of the object is the value of the a variable, and it cannot be changed. But the object is mutable. So when we tried to assign another object to the a variable, we got an exception as we were trying to change the value of the a variable.

When to use var/let/const 

The difference between const and let is that const makes sure that rebinding will not happen. That means you cannot reinitialize a const variable, but a let variable can be reinitialized (but not redeclared).

Within a particular scope, a const variable always refers to the same object. Because let can change its value at runtime, there is no guarantee that a let variable always refers to the same value. Therefore, as a rule of thumb, you can (not strictly) follow these:

  • Use const by default if you know that you'll not change the value (max performance boost)
  • Only use let if you think reassignment is required/can happen somewhere in your code (modern syntax)
  • Avoid using var (let does not create global variables when defined in a block scope; this makes it less confusing for you if you come from a C, C++, Java, or any similar background)

Let versus var versus const performance benchmarks

Currently, running a benchmark test on my own laptop (MacBook Air, Google Chrome Version 61.0.3163.100 (official build) (64-bit)) produces the following result:

Clearly, performance-wise on Chrome, let on the global scope is slowest, while let inside a block is fastest, and so is const.

First of all, the aforementioned benchmark tests are performed by running a loop 1000 x 30 times and the operation performed in the loop was appending a value to an array. That is, the array starts from [1], then becomes [1,2] in the next iteration, then [1,2,3], and so on.

What do the results mean? One inference we can draw from these results is that let is slower in a for loop when used inside the declaration: for(let i=0;i<1000;i++).

This is because let is redeclared every time for each iteration (relate this to the closure section you read earlier), whereas for(var i=0;i<1000;i++) declares the i variable for the whole block of code. This makes let a bit slower when used in a loop definition.

However, when let is not used inside the loop body but declared outside the loop, it performs quite well. For example:

let myArr = [];
for(var i = 0;i<1000;i++) {
   myArr.append(i);
}

This will give you the best results. However, if you're not performing tens of hundreds of iterations, it should not matter.

 

Immutability in JavaScript


Immutability, defined in a single line, means that once that value is assigned, then it can never be changed: 

var string1 = "I am an immutable";
var string2 = string1.slice(4, 8);

string1.slice does not change the value of string1. In fact, no string methods change the string they operate on, they all return new strings. The reason is that strings are immutable—they cannot change.

Strings are not the only immutable entity in JavaScript. Numbers, too, are immutable.

Object.freeze versus const

Earlier, we saw that even if you create objects with const in front of them, a programmer is still able to modify its properties. This is because const creates an immutable binding, that is, you cannot assign a new value to the binding.

Therefore, in order to truly make objects constants (that is, unmodifiable properties), we have to use something called Object.freeze. However, Object.freeze is, again, a shallow method, that is, you need to recursively apply it on nested objects to protect them. Let's clear this up with a simple example.

Consider this example:

var ob1 = {
   prop1 : 1,
    prop2 : {
        prop2_1 : 2 
    }
};
Object.freeze( ob1 );

const ob2 = {
   prop1 : 1,
    prop2 : {
        prop2_1 : 2 
    }
}

ob1.prop1 = 4; // (frozen) ob1.prop1 is not modified 
ob2.prop1 = 4; // (const) ob2.foo gets modified

ob1.prop2.prop2_1 = 4; // (frozen) modified, because ob1.prop2.prop2_1 is nested
ob2.bar.value = 4; // (const) modified 

ob1.prop2 = 4; // (frozen) not modified, bar is a key of obj1
ob2.prop2 = 4; // (const) modified

ob1 = {}; // (frozen) ob1 redeclared (ob1's declaration is not frozen)
ob2 = {}; // (const) ob2 not redeclared (used const)

We froze ob1 so all of its first-level hierarchical properties got frozen (that is, cannot be modified). A frozen object will not throw an error when attempted to be modified, but rather it'll simply ignore the modification done.

However, as we go deeper, you'll observe that ob1.bar.value got modified because it's 2 levels down and is not frozen. So, you'll need to recursively freeze nested objects in order to make them constant.

Finally, if we look at the last two lines, you'll realize when to use Object.freeze and when to use const. The const declaration is not declared again, whereas ob1 is redeclared because it's not constant (it's var). Object.freeze does not freeze the original variable binding and hence is not a replacement for const. Similarly, const does not freeze properties and is not a replacement for Object.freeze.

Note

Also, once an object is frozen, you can no longer add properties to it. However, you can add properties to nested objects (if present).

 

Default parameter values


In JavaScript, there is no defined way to assign default values to function parameters that are not passed. So programmers usually check for parameters with the undefined value (as it is the default value for missing parameters) and assign the default values to them. The following example demonstrates how to do this:

 function myFunction(x, y, z) {
 x = x === undefined ? 1 : x;
 y = y === undefined ? 2 : y;
 z = z === undefined ? 3 : z;
 console.log(x, y, z); //Output "6 7 3"
 }
 myFunction(6, 7);

This can be done in an easier way by providing a default value to function arguments. Here is the code that demonstrates how to do this:

function myFunction(x = 1, y = 2, z = 3) {
 console.log(x, y, z); 
}
myFunction(6,7); // Outputs 6 7 3

In the preceding code block, since we've passed first two arguments in the function calling statement, the default values (that is x = 1 and y = 2) will be overwritten with our passed values (that is x = 6 and y = 7). The third argument is not passed, hence its default value (that is z =3) is used.

Also, passing undefined is considered as missing an argument. The following example demonstrates this:

function myFunction(x = 1, y = 2, z = 3) {
   console.log(x, y, z); // Outputs "1 7 9"
 }
 myFunction(undefined,7,9);

A similar thing happens here. If you want to omit the first argument, just pass undefined in that.

Defaults can also be expressions. The following example demonstrates this:

 function myFunction(x = 1, y = 2, z = x + y) {
   console.log(x, y, z); // Output "6 7 13"
 }
 myFunction(6,7);

Here, we're making use of the argument variables themselves inside a default argument value! That is, whatever you pass as the first two arguments, if the third argument is not passed it'll take the value of the sum of the first two arguments. Since we passed 6 and 7 to the first and second argument, z becomes 6 + 7 = 13.

 

The spread operator


A spread operator is represented by the ... token. A spread operator splits an iterable object into its individual values.

Note

An iterable is an object that contains a group of values and implements the ES6 iterable protocol to let us iterate through its values. An array is an example of a built-in iterable object.

A spread operator can be placed wherever multiple function arguments or multiple elements (for array literals) are expected in code.

The spread operator is commonly used to spread the values of an iterable object into the arguments of a function. Let's take the example of an array and see how to split it into the arguments of a function.

To provide the values of an array as a function argument, you can use the apply()method of Function. This method is available to every function. The following example demonstrates:

function myFunction(a, b) {
 return a + b;
}
var data = [1, 4];
var result = myFunction.apply(null, data);
console.log(result); //Output "5"

Here, the apply method takes an array, extracts the values, passes them as individual arguments to the function, and then calls it.

Here's an example using the modern way, that is, with the spread operator:

function myFunction(a, b) {
    return a + b; 
}
let data = [1, 4];
let result = myFunction(...data);
console.log(result); //Output "5"

During runtime, before the JavaScript interpreter calls the myFunction function, it replaces ...data with the 1,4 expression:

let result = myFunction(...data);

The previous code is replaced with:

let result = myFunction(1,4);

After this, the function is called.

Other uses of the spread operator

The spread operator is not just limited to spreading an iterable object into function arguments, but it can be used wherever multiple elements (for example, array literals) are expected in code. So it has many uses. Let's see some other use cases of the spread operator for arrays.

Making array values a part of another array

The spread operator can also be used to make array values a part of another array. The following example code that demonstrates how to make the values of an existing array a part of another array while creating it:

let array1 = [2,3,4];
let array2 = [1, ...array1, 5, 6, 7];
console.log(array2); //Output "1, 2, 3, 4, 5, 6, 7"

Consider the following code:

 let array2 = [1, ...array1, 5, 6, 7];

This previous code is equivalent to:

 let array2 = [1, 2, 3, 4, 5, 6, 7];

Pushing the values of an array into another array

Sometimes, we may need to push the values of an existing array into the end of another existing array. This is how programmers used to do it:

var array1 = [2,3,4];
var array2 = [1];
Array.prototype.push.apply(array2, array1);
console.log(array2); //Output "1, 2, 3, 4"

But from ES6 onward we have a much cleaner way to do it, which is as follows:

let array1 = [2,3,4];
let array2 = [1];
array2.push(...array1);
console.log(array2); //Output "1, 2, 3, 4"

Here the push method takes a series of variables and adds them to the end of the array on which it is called. See the following line:

array2.push(...array1);

This will be replaced with the following line:

array2.push(2, 3, 4);

Spreading multiple arrays

Multiple arrays can be spread on a single-line expression. For example, take the following code:

let array1 = [1];
let array2 = [2];
let array3 = [...array1, ...array2, ...[3, 4]];//multi arrayspread
let array4 = [5];
function myFunction(a, b, c, d, e) {
  return a+b+c+d+e;
}
let result = myFunction(...array3, ...array4); //multi array spread
console.log(result); //Output "15"
 

The rest parameter


The rest parameter is also represented by the ... token. The last parameter of a function with ... is called a rest parameter. The rest parameter is an array type and contains the rest of the parameters of a function when the number of arguments exceeds the number of named parameters. The rest parameter is used to capture a variable number of function arguments from within a function. The arguments object can also be used to access all arguments passed. The argument object is not strictly an array, but it provides some interfaces that are similar to an array. The following example code shows how to use the arguments object to retrieve the extra arguments:

function myFunction(a, b) {
const args = Array.prototype.slice.call(arguments, myFunction.length);
    console.log(args);
}
myFunction(1, 2, 3, 4, 5); //Output "3, 4, 5"

This can be done in a much easier and cleaner way, by using the rest parameter. The following example demonstrates to use the rest parameter:

function myFunction(a, b, ...args) {
    console.log(args); //Output "3, 4, 5"
}
myFunction(1, 2, 3, 4, 5);

The arguments object is not an array object. Therefore, to do array operations on the arguments object, you need to convert it to an array. The rest parameter is easy to work with.

Note

What is the ... token called? The ... token is called the spread operator or rest parameter, depending on where and how it's used.

 

Hoisting


Hoisting is JavaScript's default behavior: moving declarations to the top. That means the following code will work in JavaScript:

bookName("ES8 Concepts");
function bookName(name) {
   console.log("I'm reading " + name);
}

If you're coming from a C/C++ background, this might seem a little weird at first because those languages do not allow you to call a function before at least declaring its prototype. But JavaScript, behind the scenes, hoists the function, that is, all function declarations are moved to the top of the context. So, essentially, the preceding code is the same as the following:

function bookName(name) {
   console.log("I'm reading " + name);
}

bookName("ES8 Concepts");

Hoisting only moves the declarations to the top, not the initializations. Therefore, although the preceding code works, the following code won't work:

bookName("ES8 Concepts"); // bookName is not a function
var bookName = function(name) {
   console.log("I'm reading " + name);
}

This is because, as we said earlier, only declarations are hoisted. Therefore, what a browser sees is something like this:

var bookName; // hoisted above
bookName("ES8 Concepts"); // bookName is not function 
                          // because bookName is undefined
bookName = function(name) { // initalization is not hoisted
   console.log("I'm reading " + name);
}

Guess the output of the following code:

function foo(a) {
   a();
   function a() {
      console.log("Mehul");
   }
}

foo(); // ??
foo( undefined ); // ??
foo( function(){ console.log("Not Mehul"); } ); // ??

Ready to find out? Your possible answers are:

  • Mehul
    undefined
    Not Mehul
  • Program throws error
  • Mehul
    Mehul
    Mehul

The output will be :

Mehul
Mehul
Mehul

Why? Because this is how your browser see this code (after applying the hoisting thing):

function foo(a) { 
   // the moment below function is declared, 
   //the argument 'a' passed is overwritten.
   function a() {
      console.log("Mehul");
   }
   a();
}

foo();
foo( undefined );
foo( function(){ console.log("Not Mehul"); } );

Once the function is hoisted, it doesn't matter what you pass in that function. It is always overwritten with the function defined inside the foo function.

Therefore, the output is just Mehul written three times. 

 

Destructuring assignments


A destructuring assignment is an expression that allows you to assign the values or properties of an iterable or object to variables, using a syntax that looks similar to the array or object construction literals respectively.

A destructuring assignment makes it easy to extract data from iterables or objects by providing a shorter syntax. A destructuring assignment is already present in programming languages such as Perl and Python, and works the same way everywhere.

There are two kinds of destructuring assignment expressions: array and object. Let's see each of them in detail.

The array destructuring assignment

An array destructuring assignment is used to extract the values of an iterable object and assign them to the variables. It's called an array destructuring assignment because the expression is similar to an array construction literal.

Programmers used to do it this way to assign the values of an array to the variables:

 var myArray = [1, 2, 3];
 var a = myArray[0];
 var b = myArray[1];
 var c = myArray[2];

Here, we are extracting the values of an array and assigning them to the a, b, c variables respectively.

With an array destructuring assignment we can do this in a one-line statement:

let myArray = [1, 2, 3];
let a, b, c;
[a, b, c] = myArray; //array destructuring assignment syntax

As you can see, [a, b, c] is an array destructuring expression.

On the left-hand side of the array destructuring statement, we need to place the variables to which we want to assign the array values, using a syntax similar to an array literal. On the right-hand side, we need to place an array (actually any iterable object) whose values we want to extract.

The previous example code can be made even shorter in this way:

let [a, b, c] = [1, 2, 3];

Here, we create the variables on the same statement, and instead of providing the array variable, we provide the array with a construction literal.

If there are fewer variables than items in the array, then only the first items are considered.

Note

If you place a non-iterable object on the right-hand side of the array destructuring assignment syntax, then a TypeError exception is thrown.

Ignoring values

We can also ignore some of the values of the iterable. Here is example code that shows how to do this:

 let [a, , b] = [1, 2, 3]; // notice -->, ,<-- (2 commas)
 console.log(a);
 console.log(b);

The output is as follows:

1 3

Using the rest operator in an array destructuring assignment

We can prefix the last variable of an array destructuring expression using the ... token. In this case, the variable is always converted into an array object that holds the rest of the values of the iterable object, if the number of other variables is less than the values in the iterable object.

Consider this example to understand it:

   let [a, ...b] = [1, 2, 3, 4, 5, 6];
   console.log(a);
   console.log(Array.isArray(b));
   console.log(b);

The output is as follows:

   1
   true
   2,3,4,5,6

In the previous example code, you can see that the b variable is converted into an array, and it holds all the other values of the right-hand side array.

Here the ... token is called the rest operator. We can also ignore the values while using the rest operator. The following example demonstrates this:

   let [a, , ,...b] = [1, 2, 3, 4, 5, 6];
   console.log(a);
   console.log(b);

The output is as follows:

1 4,5,6

Here, we ignored the 2, 3 values.

Default values for variables

While destructuring, you can also provide default values for the variables if an array index is undefined. The following example demonstrates this:

   let [a, b, c = 3] = [1, 2];
   console.log(c); //Output "3"

Nested array destructuring

We can also extract the values from a multidimensional array and assign them to variables. The following example demonstrates this:

   let [a, b, [c, d]] = [1, 2, [3, 4]];

Using a destructuring assignment as a parameter

We can also use an array destructuring expression as the function parameter for extracting the values of an iterable object, passed as an argument into the function parameters. The following example demonstrates this:

   function myFunction([a, b, c = 3]) {
     console.log(a, b, c); //Output "1 2 3"
   }
   myFunction([1, 2]);

Earlier in this chapter, we saw that if we pass undefined as an argument to a function call, then JavaScript checks for the default parameter value. So, we can provide a default array here too, which will be used if the argument is undefined. The following example demonstrates this:

   function myFunction([a, b, c = 3] = [1, 2, 3]) {
     console.log(a, b, c);  //Output "1 2 3"
   }
    myFunction(undefined);

Here, we passed undefined as an argument and therefore the default array, which is [1, 2, 3], was used to extract the values.

Object destructuring assignments

An object destructuring assignment is used to the extract property values of an object and assign them to the variables.

This is a traditional (and still useful) way of assigning property values to an object:

   var object = {"name" : "John", "age" : 23};
   var name = object.name;
   var age = object.age;

We can do this in a one-line statement, using the object destructuring assignment:

   let object = {"name" : "John", "age" : 23};
   let name, age;
   ({name, age} = object); //object destructuring assignment syntax

On the left-hand side of the object destructuring statement, we need to place the variables to which we want to assign the object property values using a syntax similar to that of an object literal. On the right-hand side, we need to place an object whose property values we want to extract. The statement is finally closed using the ( ) token.

Here the variable names must be the same as the object property names. If you want to assign different variable names, then you can do it this way:

   let object = {"name" : "John", "age" : 23};
   let x, y;
   ({name: x, age: y} = object);

The previous code can be made even shorter this way:

   let {name: x, age: y} = {"name" : "John", "age" : 23};

Here we are creating the variables and object on the same line. We don't need to close the statement using the ( ) token, as we are creating the variables on the same statement.

Default values for variables

You can also provide default values for the variables if the object property is undefined while destructuring. The following example demonstrates this:

   let {a, b, c = 3} = {a: "1", b: "2"};
   console.log(c); //Output "3"

Some property names are constructed dynamically using expressions. In this case, to extract the property values, we can use the [ ] token to provide the property name with an expression. The following example demonstrates this:

   let {["first"+"Name"]: x} = { firstName: "Eden" };
   console.log(x); //Output "Eden"

Destructuring nested objects

We can also extract property values from nested objects, that is, objects within objects. The following example demonstrates this:

   var {name, otherInfo: {age}} = {name: "Eden", otherInfo: {age:
   23}};
   console.log(name, age); //Eden 23

Using the object destructuring assignment as a parameter

Just like the array destructuring assignment, we can also use the object destructuring assignment as a function parameter. The following example demonstrates this:

   function myFunction({name = 'Eden', age = 23, profession = 
                       "Designer"} = {})   {
     console.log(name, age, profession); // Outputs "John 23 Designer"
   }
   myFunction({name: "John", age: 23});

Here, we passed an empty object as a default parameter value, which will be used as a default object if undefined is passed as a function argument.

 

Arrow functions


An arrow function is, at first glance, just a fancy way to create regular JavaScript functions (however, there are some surprises). Using arrow functions, you can create concise one-liner functions that actually work!

The following example demonstrates how to create an arrow function:

let circumference = (pi, r) => {
  let ans = 2 * pi * r;
  return ans;
}
let result = circumference(3.141592, 3);
console.log(result); // Outputs 18.849552

Here, circumference is a variable, referencing to the anonymous arrow function. The previous code is similar to the following code in ES5:

var circumference = function(pi, r) {
  var area = 2 * pi * r;
  return area;
}
var result = circumference(3.141592, 3);
console.log(result); //Output 18.849552

If your function contains just a single statement (and you want to return the result of that statement), then you don't have to use the {} brackets to wrap the code. This makes it a one-liner. The following example demonstrates this:

let circumference = (pi, r) => 2 * pi * r;
let result = circumference(3.141592, 3);
console.log(result); //Output 18.849552

When {} brackets are not used then the value of the statement in the body is automatically returned. The preceding code is equivalent to the following:

let circumference = function(pi, r) { return 2 * pi * r; }
let result = circumference(3.14, 3);
console.log(result); //Output 18.84

Also, if there's only a single argument, you can omit the brackets to make the code even shorter. Consider the following example:

let areaOfSquare = side => side * side;
let result = areaOfSquare(10);
console.log(result); //Output 100

Since there is only one argument, side, we can omit the circular brackets for this.

The value of "this" in an arrow function

In arrow functions, the value of the this keyword is the same as the value of the this keyword of the enclosing scope (the global or function scope, whichever the arrow function is defined inside). That means, instead of referring to the context object (that is, the object inside which the function is a property), which is the value of this in traditional functions, this instead refers to global or function scope, in which the function is called. Consider this example to understand the difference between the traditional functions and the arrow functions, this value:

var car = {
  name: 'Bugatti',
  fuel: 0,
  // site A
  addFuel: function() {
             // site B
             setInterval(function() {
              // site C
              this.fuel++;
              console.log("The fuel is now " + this.fuel);
             }, 1000)
           }
}


What do you think will happen when you call the car.addFuel() method? If you guessed The fuel is now undefined will appear forever, then you are right! But why?!

When you define the addFuel method inside the function() {} (above site B), your this keyword refers to the current object. However, once you go another level deeper into functions (site C), your this now points to that particular function and its prototypes. Hence, you cannot access the parent object's property with the this keyword.

How do we fix this? Take a look at these arrow functions!

var car = {
  name: 'Bugatti',
  fuel: 0,
  // site A
  addFuel: function() {
             // site B
             setInterval(() => { // notice!
              // site C
              this.fuel++;
              console.log("The fuel is now " + this.fuel);
             }, 1000)
           }
}

Now, inside site C, the this keyword refers to the parent object. Hence, we're able to access the fuel property using the this keyword only.

Other differences between arrow and traditional functions

Arrow functions cannot be used as object constructors, that is, the new operator cannot be applied to them. Apart from syntax, the value, and the new operator, everything else is the same between arrow and traditional functions, that is, they are both instances of the Function constructor.

 

Enhanced object literals


Once, JavaScript required developers to write complete function names, property names, even when the function name / property name values matched each other (example: var a = { obj: obj }). However, ES6/ES7/ES8 and beyond relaxes this and allows the minification and readability of code in a number of ways. Let us see how.

Defining properties

ES6 brought in a shorter syntax for assigning object properties to the values of variables that have the same name as the properties. Traditionally, you would've done this:

var x = 1, y = 2;
var object = {
 x: x,
 y: y 
};
console.log(object.x); //output "1"

But now, you can do it this way:

let x = 1, y = 2;
let object = { x, y };
console.log(object.x); //output "1"

Defining methods

ES6 onwards provides a new syntax for defining the methods on an object. The following example demonstrates the new syntax:

let object = {
    myFunction(){
        console.log("Hello World!!!"); //Output "Hello World!!!"
    }
}
object.myFunction();

This concise function allows the use of super in them, whereas traditional object methods don't allow the use of super. We will learn more about this later in the book.

Computed property names

Property names that are evaluated during runtime are called computed property names. An expression is usually resolved to find the property name dynamically. Computed properties were once defined in this way:

var object = {};
object["first"+"Name"] = "Eden";//"firstName" is the property name
//extract
console.log(object["first"+"Name"]); //Output "Eden"

Here, after creating the object, we attach the properties to the object. But in ES6, we can add the properties with the computed name while creating the object. The following example demonstrates this:

let object = {
["first" + "Name"]: "Eden",
};
//extract
console.log(object["first" + "Name"]); //Output "Eden"
 

Trailing commas and JavaScript


Trailing commas are those commas found at the end of an array list, object, or function arguments. They can be useful when adding new elements, parameters, or properties to JavaScript code. It just makes it a little more convenient for developers that they can choose to write an array as [1,2,3] or [1,2,3,] (notice the comma in the second example)

JavaScript has allowed trailing commas in arrays and objects for a long time. Finally, in ECMAScript 2017 (ES8), the standard now allows you to add trailing commas to function parameters as well.

That means all the following examples are valid JavaScript code:

Arrays:

var arr = [1, 2, 3,,,];
arr.length; // 5
arr[3]; // undefined

var arr2 = [1, 2, 3,];
arr2.length; // 3

The preceding example is clearly valid JavaScript code and arr is created as [1, 2, 3, undefined, undefined]

Let us now explore how objects behave with trailing commas.

Objects:

var book = { 
  name: "Learning ES8", 
  chapter: "1",
  reader: "awesome", // trailing comma allowed here
};

It can be seen that the code does not throw any error even after putting a comma after the last property name. Let's move on to functions now.

Functions:

function myFunc(arg) {
   console.log(arg);
}

function myFunc2(arg,) {
   console.log(arg)
} 

let myFunc3 = (arg) => {
   console.log(arg);
};

let myFunc4 = (arg,) => {
   console.log(arg);
}

All the aforementioned function definitions are valid from the ES2017 (ES8) spec.

 

The semicolon dilemma


You must've seen a lot of JavaScript code with semicolons, and a lot without semicolons as well. And surprisingly, both work fine! While languages such as C, C++, Java, and so on are strict about the use of semicolons, and on the other hand languages such as Python are strict about not using semicolons (only indentations), there is no such fixed rule for JavaScript.

So let's see when is semicolon required in JavaScript.

Automatic semicolon insertion in JavaScript

The ECMAScript Language specification (http://www.ecma-international.org/ecma-262/5.1/#sec-7.9) states that:

"Certain ECMAScript statements must be terminated with semicolons. Such semicolons may always appear explicitly in the source text"

But the spec also says:

"For convenience, however, such semicolons may be omitted from the source text in certain situations."

Therefore, the specification states that JavaScript is able to handle automatic semicolon insertion by its own judgment. However, it is extremely error-prone in some cases and not intuitive at all.

Consider this example:

vara=1
varb=2
varc=3

JavaScript automatically inserts semicolon to make code look like:

vara=1;
varb=2;
varc=3;

So far so good.

Where to insert semicolons in JavaScript?

At times, you will find yourself skipping semicolons somewhere and you'll see that your code still works! This is strictly opposite to what you find in languages such as C or C++. Let us take a look at a scenario where you can get trapped by not using semicolons properly. 

Consider this code:

var fn = function (arg) {
    console.log(arg);
} // Semicolon missing

// self invoking function
(function () {
    alert(5);
})() // semicolon missing

fn(7)

Take a good look and guess what possible alerts might be, with their orders as well. When you're ready with your answer, look at the following, the code to which JavaScript compiles (not really, just the code after inserting automatic semicolons):

var fn = function (arg) {
    alert(arg);
}(function () { // <-- semicolon was missing here, 
                // this made it an argument for the function
    alert(5);
})();

fn(7);

So instead of invoking that self-invoking function, what you do apparently is, pass that whole function as an argument to the first one. Therefore, try to use semicolons to avoid ambiguity in your code. You can always use JavaScript compressors later on, which will take care of necessary places to leave semicolons intact. The takeaway from here is use semicolons.

 

Summary


In this chapter, we learned about variable scopes, read-only variables, splitting arrays into individual values, passing indefinite parameters to a function, extracting data from objects and arrays, arrow functions, and new syntaxes for creating object properties, hoisting, IIFE, semicolon usage, and more. In the next chapter, we will learn about built-in objects and symbols, and we will discover tons of fundamental tools JavaScript natively provides us with out-of-the-box.

About the Authors

  • Mehul Mohan

    Mehul Mohan is an independent developer and a security researcher. Currently, he is pursuing his bachelor’s degree in computer science at BITS Pilani. He aims to provide free technical knowledge to all through his website “codedamn”. He loves reading about techy stuff and you’ll often find him creating programming tutorials on his YouTube channel, codedamn; he has over 50,000 subscribers on his YouTube channel. He has been acknowledged by companies such as Google, Microsoft, Sony, Envoy, InVision, and such for his contribution as a security researcher.

    Browse publications by this author
  • Narayan Prusty

    Narayan Prusty is a full-stack developer. He works as a consultant for various start-ups around the world. He has worked on various technologies and programming languages but is very passionate about JavaScript, WordPress, Ethereum, Solr, React, Cordova, MongoDB, and AWS. Apart from consulting for various start-ups, he also runs a blog titled QNimate and a video tutorial site titled QScutter, where he shares information about a lot of the technologies he works on.

    Browse publications by this author

Latest Reviews

(1 reviews total)
I wass look this book once to get it...

Recommended For You

The JavaScript Workshop

Cut through the noise and get real results with a step-by-step approach to beginner JavaScript development

By Joseph Labrecque and 7 more
ECMAScript Cookbook

Become a better web programmer by writing efficient and modular code using ES6 and ES8

By Ross Harrison
The HTML and CSS Workshop

Cut through the noise and get real results with a step-by-step approach to learning HTML and CSS programming

By Lewis Coulson and 4 more