Spread operator, Iterators and Arrow functions in ES6
This is the second installment of our ES6 tutorial series, where we will take a look at the spread operators, iterators, and fat arrow functions. We will focus on their peculiarities and go over where to use them.
Check out the first post in the series about ES6 Classes, where we gave you a practical example of game UI.
Spread operator
The spread operator allows an iterable to be expanded in places where zero or multiple arguments (for function calls) or elements (for array literals) are expected.
The syntax looks like this:
- for function calls:
1 |
myFunction(...iterableObj); |
- for array literals:
1 |
[...iterableObj, 4, 5, 6]; |
Let’s look at some examples where the spread operator becomes handy:
- Replace Apply when calling functions: when you want to pass the elements of an array as arguments to a function.
Before ES6, in this case, we had to use Function.prototype.apply
:
1 2 3 4 5 6 7 8 9 10 |
// ES5 function printSum(a, b) { console.log(a + b); } let args1 = [0, 1]; printSum.apply(null, args1); // 1 let args2 = [-1, 5]; printSum.apply(null, args2); // 4 |
You can simplify the code above and avoid using null
, thanks to the spread operator:
1 2 3 4 5 6 7 8 9 10 |
// ES6 function printSum(a, b) { console.log(a + b); } let args1 = [0, 1]; printSum(...args1); // 1 let args2 = [-1, 5]; printSum(...args2); // 4 |
- Easier array manipulation: use it when you want to expand an array with another one without nesting them or using a combination of
push
,splice
,concat
, etc. Spread syntax can be used anywhere in the array literal and it can be used multiple times:
1 2 3 4 5 |
let words = ['foo', 'bar', 'baz']; let arr = ['one', ...words, 'two', ...words]; console.log(arr); // ['one', 'foo', 'bar', 'baz', 'two', 'foo', 'bar', 'baz'] |
- Copy an array: spread syntax makes a copy by value, so that future manipulations will not affect the original array:
1 2 3 4 5 6 7 8 |
let arr1 = [1, 2, 3]; let arr2 = [...arr1]; arr2.push(4); console.log(arr1); console.log(arr2); // [1, 2, 3] // [1, 2, 3, 4] |
Note: objects within the array are still by reference, so not everything gets “copied”, per se.
- Concatenate arrays: it is an easier way to insert an array of values at the start of an existing array without using
unshift
andapply
:
1 2 3 4 5 6 7 |
// ES5 let arr1 = ['a', 'b', 'c']; let arr2 = ['d', 'e', 'f']; Array.prototype.unshift.apply(arr1, arr2); console.log(arr1); // ['d', 'e', 'f', 'a', 'b', 'c'] |
Thanks to the spread syntax, this operation is much easier:
1 2 3 4 5 6 |
//ES6 let arr1 = ['a', 'b', 'c']; let arr2 = ['d', 'e', 'f']; arr1 = [...arr2, ...arr1]; console.log(arr1); // ['d', 'e', 'f', 'a', 'b', 'c'] |
- Math functions: you can use them in conjuction:
1 2 |
let numbers = [3, 9, 6, 1]; Math.min(...numbers); // 1 |
- Converting string to array: this feature allows you can convert a string to an array of characters:
1 2 3 4 5 |
let str = "javascript"; let chars = [...str]; console.log(chars); // ['j', 'a', 'v', 'a', 's', 'c', 'r', 'i', 'p', 't'] |
Iterators
The for...of
statement creates a loop that iterates over iterable data structures such as Arrays, Strings, Maps, Sets, and more. It is the most concise and direct syntax for looping through array elements, as an alternative to both for...in
and forEach()
. For...of
loop works with break
, continue
, return
and throw
.
Let’s look at the syntax:
1 2 3 |
for (variable of iterable) { statement } |
where:
- variable: for each iteration, a value of a different property is assigned to a variable.
- iterable: an object which has enumerable properties and can be iterated upon.
The following examples show the different data structures where the for...of
loop can be used:
- Arrays: They are used to store multiple values in a single variable. Here is how we iterate over an Array:
1 2 3 4 5 6 7 8 |
let iterable = ['foo', 'bar', 'buz'] for (let value of iterable) { console.log(value); } // foo // bar // buz |
- Strings: They hold data that can be represented in text form:
1 2 3 4 5 6 7 8 |
let iterable = 'foo'; for (let value of iterable) { console.log(value); } // f // o // o |
- Map: The Map object holds key-value pairs, so the
for...of
loop will return an array of key-value pair for each iteration.
1 2 3 4 5 6 7 8 9 |
let iterable = new Map([['a', 1], ['b', 2]]); for (let [key, value] of iterable) { console.log(`Key: ${key} and Value: ${value}`); } // Output: // Key: a and Value: 1 // Key: b and Value: 2 |
- Set: The Set object allows you to store unique values of any type like primitive values or objects. If you create a Set that has the same element more than once, it is still considered as a single element.
1 2 3 4 5 6 7 8 |
let iterable = new Set([1, 1, 2, 2, 3, 3]); for (let the value be iterable) { console.log(value); } // 1 // 2 // 3 |
- Arguments object: It is an Array-like object corresponding to the arguments passed to a function. Here is a case where iterating is used over it:
1 2 3 4 5 6 7 8 9 10 |
function args() { for (const arg of arguments) { console.log(arg); } } args('a', 'b', 'c'); // a // b // c |
Fat arrow functions
The main benefits of fat arrow functions are that they provide shorter syntax and simplify function scoping by non-binding of the this
keyword. Their analogue is Lambdas in other languages like C# and Python.
Let’s compare the syntax of an ordinary function and this one of a fat arrow function:
1 2 3 4 5 6 7 |
// ES5 function multiply(x, y) { return x * y; } multiply(3, 2); // 6 |
The same function can be expressed as an arrow function with one line of code, which is more concise and easier to read:
1 2 3 4 5 |
// ES6 let multiply = (x, y) => x * y; multiply(3, 2); // 6 |
So, the basic syntax of a fat arrow function looks like:
1 |
(param1, param2, ..., paramN) => { statements } |
You can simplify the syntax even more:
- when there’s only one parameter, the opening parenthesis are optional:
1 |
oneParam => { statements } |
- when you are returning an expression, you remove the brackets and the
return
keyword:
1 2 3 |
(parameters) => { return expression; } // is equivalent to: (parameters) => expression |
- function without parameters should be written with a pair of parentheses:
1 |
() => { statements } |
As we said above the this
of arrow functions works differently.
Before we move on, let’s take a look at what is this
in JavaScript. It can refer to objects, variables and can we use it in context.
- Global context:
If this
is outside of any function in a global execution context, it refers to the global context (window object in browsers).
1 2 3 4 5 |
var myGlobVar = 5; console.log(this); // 5 console.log(window.myGlobVar); // 5 |
- Function context
this
refers to the object that the function is executing in. Look at this example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var myVar = 3; function foo() { return this; } function bar() { return this.myVar; } function baz() { return window.myVar; } console.log(foo() === window); // => True console.log(bar()); // => 3 console.log(baz()); // => 3 |
this
is determined by how a function is invoked. As you can see, all the above functions have been called in a global context.
So, when a function is called as a method of an object, this
is set to the object the method is called on:
1 2 3 4 5 6 7 8 9 |
var person = { name: "Jon Doe", logName: function() { console.log(this.name) } }; person.logName(); // Jon Doe |
And it doesn’t matter how the function was defined:
1 2 3 4 5 6 7 8 9 10 11 12 |
function foo() { return this; } var obj = { method: foo }; console.log(obj.method() === window); // False console.log(obj.method() === obj); // True |
Another example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
function foo() { return this; } var obj = { method: function() { return this; } } var outerObj = { nestedObj: { method: function() { return this; } } }; // We are still in the global context console.log(foo() === window); // True // Function is called as a method of an object console.log(obj.method() === window); // False // Function is called as a method of an object console.log(obj.method() === obj); // True // Function is called as a method of object nestedObj, so this is its context console.log(outerObj.nestedObj.method === outerObj); // False |
But in strict mode, rules are different. Context remains as whatever it was set to:
1 2 3 4 5 6 7 |
function bar() { 'use strict'; return this; } console.log(bar() === undefined); // True |
In the example above, this
was not defined, so it remained undefined
.
this
is dynamic, which means the value could change. Here’s a simple example to show that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var firstObj = { method: function() { return this; } }; var secondObj = { method: firstObj.method }; console.log(secondObj.method() === firstObj); // False console.log(secondObj.method() === secondObj); // True |
We can call car by this
and by object name:
1 2 3 4 5 6 7 8 9 |
var car = { model: "Toyota", logThis: function() { console.log('this, ' this.model); // this Toyota console.log('car', car.model); // car Toyota } } |
When we create an instance of an object with new
operator, the context of a function will be set to the created instance of the object:
1 2 3 4 5 6 7 8 9 10 |
var Foo = function() { this.bar = 'baz'; }; var foo = new Foo(); console.log(foo.bar); // baz console.log(window.bar) // undefined |
call
,apply
,bind
These methods allow us to manage the context of a function. Let’s see how they work, on examples:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var string = 'xo xo'; var obj = { string: 'lorem ipsum' }; function func() { return this.string; } console.log(func()); // xo xo // We call func in a global context console.log(func.call(obj)); // lorem ipsum // By using call, we call func in context of obj console.log(func.apply(obj)); // lorem ipsum // By using apply, we call func in context of obj |
The bind
method sets the context to the provided value. But remember that after using bind
, this
remains immutable, even by invoking call
, apply
or bind
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var foo = 7; function test() { return this.foo; } var bound = test.bind(document); // There is no foo variable in the document object console.log(bound()); // undefined // There is no foo variable in the document object. In this situation, call can’t change the context console.log(bound.call(window)); // undefined // We creted a new object and called test in this context var bound2 = test.bind({foo: 5}); console.log(bound2()); // 5 |
But when we use arrow functions, this
retains the value of the enclosing lexical context:
1 2 3 |
var foo = (() => this); console.log(foo() === window); // True |
Let’s notice the difference between the arrow and ordinary function. With an arrow function we are in the window context:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var objArrow = {method: () => this}; var objFunc = { method: function() { return this; } } console.log(objArrow.method() === objArrow); // False console.log(objArrow.method() === window); // True console.log(objFunc.method() === objFunc); // True |
We can say that:
x => this.y equals (x) {return this.y}.bind(this)
The arrow function always binds this
and so it can’t be used as a constructor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var a = 'global'; var obj = { method: function() { return { a: 'inside method', normalFunc: function() { return this.a; }, arrowFunc: () => this.a }; }, a: 'inside obj' }; console.log(obj.method().normalFunc()); // inside method console.log(obj.method().arrowFunc()); // inside obj |
Once you figure out the difference between the function dynamic and lexical this
, be careful before declaring a new function. If it is invoked as a method, use dynamic this
, if it is invoked as a subroutine, use the lexical this
.
For more interesting information regarding spread operator, iterators and arrow functions in ES6 follow @CoherentLabs on Twitter or start a discussion in our Forum.
Additional resources
6 Great Uses of the Spread Operator