Hello, my name is Seth Bergman. I am a

Full Stack Engineer

focused on helping companies scale. I love learning about software architecture, containers, open source programming and automation. I use technologies that drive innovation, speed up development and provide continuous delivery of awesome software.

ES6 Learning Notes - Functional JavaScript

Arrow Functions

Arrow functions introduce a concise syntax for defining a function.

Example:

let add = (x,y) => x + y;  
add(3,5); // 8  

On the left hand side of the arrow (=>) are the parameters, and on the right hand side is the function body. There is no need for braces or a return statement.

Example:

let add = (x,y) => x + y;  
let square = x => x * x;

square(add(2,3)); // 25  

In order to write an arrow function that takes no parameters, you must include empty parentheses:

let three = () => 3;  

If you need multiple lines of code for an arrow function, you must use braces and a return statement (but this isn't really best practice for an arrow function):

let add = (x,y) => {  
  let temp = x + y;
  return temp;
};

Arrow functions can be used as parameters to other functions.
Example of adding all numbers in an array:

var numbers = [1, 2, 3, 4];

var sum = 0;  
numbers.forEach(n => sum += n); // 10  

This code takes each number in the array numbers, assigns its value to n, then adds it to sum.

Example of creating a new array with each value doubled:

var doubled = numbers.map(n => n * 2);

doubled; //[2, 4, 6, 8]  

Arrows as Async Callbacks

One problem with callbacks is managing the this reference, since it is established by the context.
To illustrate:

e1.doWork()

....

doWork() {  
  this.name = "Taylor"; // this refers to e1

  ajaxCall("aUrl", function(response) {
    // this does not point to e1
    this.name = response.data;
  });
}

However, Arrow functions always capture the this value of the context they are in (it lexically binds to this).

  this.name = "Taylor";

  ajaxCall("aUrl", (response) => {
      this.name = response.data;
    });

Iterables and Iterators

If you have a collection of objects that is iterable, you can call an iterator. For example, an array has a next() function. The iterator holds the value and a boolean done.

You have to use the iterator to get the items in the iterable. There is no length attribute of an iterator.

Let's look at different ways to sum up all the numbers in an array.

let sum = 0;  
let numbers = [1, 2, 3, 4];

// for loop
sum = 0;  
for(let i = 0; i < numbers.length; i++){  
  sum += numbers[i];
}
sum; // 10

// "for in"
sum = 0;  
for(let i in numbers){  
  sum += numbers[i];
}

// iterator
sum = 0;  
let iterator = numbers.values(); // returns an iterator, not an array  
let next = iterator.next();  
while(!next.done){  
  sum += next.value;
  next = iterator.next();
}

but there's a new way...

for of

Sometimes we only want to iterate values and not worry about indexes or keys:

let numbers = [1, 2, 3, 4];

for(let i of numbers) {  
  console.log(i);
}

This type of loop works with iterators:

let sum = 0;  
let numbers = [1, 2, 3, 4];

for(let n of numbers){  
  sum += n;
}

"for of" gets to an iterator's value without having to use .values() like above because it implements Symbol.iterator.

Implementing Symbol.iterator

The "hard way":

class Company {  
  constructor() {
    this.employees = [];
  }

  addEmployees(...names) {
    this.employees = this.employees.concat(names);
  }

  [Symbol.iterator]() {
    return {
      next() {
        ... // This is the hard way
      }
    }
  }

  let count = 0;
  let company = new Company();
  company.addEmployees("Tim", "Sue", "Joy", "Tom");

  for(let employee of company) {
    count += 1;
  }
}

It is better to implement the iterator with a class, but it's still not the easiest way:

class Company {

  constructor() {
    this.employees = [];
  }

  addEmployees(...names) {
    this.employees = this.employees.concat(names);
  }

  [Symbol.iterator]() {
    return new ArrayIterator(this.employees);
  }
}

class ArrayIterator {  
  constructor(array) {
    this.array = array;
    this.index = 0;
  }

  next() {
    var result = { value: undefined, done: true };

    if(this.index < this.array.length) {
      result.value = this.array[this.index];
      result.done = false;
      this.index += 1;
    }
    return result;
  }
}

What's good about implementing an iterator like this is that it allows users to go through our employees without giving them access to the underlying data structure.

However, the better way to implement an iterator is by using a Generator.

Generators

A generator is a function that generates an iterator. It uses function*() and the yield keyword:

let numbers = function*() {  
  yield 1;
  yield 2;
  yield 3;
  yield 4;
};

yield returns multiple values (instead of returning just one). You can yield multiple values because the generator is a factory for iterators:

let sum = 0;  
let iterator = numbers();  
let next = iterator.next();  
while(!next.done){  
  sum += next.value;
  next = iterator.next();
}

sum; // 10  

Each time a generator uses yield, it yields the thread of execution back to the caller. The value is returned immediately, the caller can do whatever it needs to do, and then when the caller asks for the next value, the generator picks up where it left off.

Using a for of with a Generator

let numbers = function*(start, end) {  
  for(let i = start; i <= end; i++) {
    console.log(i);
    yield i;
  }
};

let sum = 0;

for(let n of numbers(1,5)){  
  sum += n;
  console.log("next");
}

sum; // 15  

Replacing the iterator we built in the Company example...

We will create a filter generator function to look for employees with a "T" in their name:

class Company {  
  constructor() {
    this.employees = [];
  }

  addEmployees(...names) {
    this.employees = this.employees.concat(names);
  }

  *[Symbol.iterator]() {
    for(let e of this.employees) {
      console.log(e);
      yield e;
    }
  }
}

let filter = function*(items, predicate) {  
  for(let item of items) {
    console.log("filter", item);

    // Only process items that match the filter
    if(predicate(item)) {
      yield item;
    }
  }
}

// Function that only matches a set number without iterating the entire collection
// (e.g. we only want 'x employees out of everyone')

let take = function*(items, number) {  
  let count = 0;
  if(number < 1) return; // Inside a generator, hitting a `return` sets the `done` flag to `true`

  for(let item of items) {
    console.log("take", item);
    yield item;
    count += 1;
    if(count >= number) {
      return;
    }
  }
}

let count = 0;  
let company = new Company();  
company.addEmployees("Tim", "Sue", "Joy", "Tom");

// Pass the filter function our company and an arrow function
// to look for names that start with T
for(let employee of filter(company, e => e[0] == 'T')) {  
  count += 1;
}

count; // 2 (Tim and Tom are the only employees starting with T)

// "take" only one employee whose name starts with T
for(let employee of take(filter(company, e => e[0] == 'T'), 1)) {  
  count += 1;
}

Range Function Example

let range = function*(start, end) {  
  let current = start;
  while(current <= end) {
    let delta = yield current;
    current += delta || 1;
  }
}

This is not a generator function, but returns an object that can be used as an iterator:

let range2 = function(start, end) {  
  let current = start;
  let first = true;
  return {
    next(delta = 1) {
      let result = { value: undefined, done: true };

      if(!first) {
        current += delta;
      }

      if(current <= end) {
        result.value = current;
        result.done = false;
      }
      first = false;
      return result;
    }
  }
}

let result = [];  
let iterator = range(1, 10);  
let next = iterator.next();  
while(!next.done) {  
  result.push(next.value);
  next = iterator.next(2);
}

result; // [1, 3, 5, 7, 9]  

Comprehensions

Comprehensions are a terse syntax for building arrays and generators, similar to what's found in Python.

Array Comprehension

We know we are building an array from the square brackets.

var numbers = [for (n of [1, 2, 3]) n * n];  
numbers; // [1, 4, 9]  

This comprehension is the same as:

let numbers = [];  
for(let n of [1, 2, 3]) {  
  numbers.push(n * n);
}
numbers; // [1, 4, 9]  

We can also use a filter predicate to look for numbers greater than 1:

var numbers = [for (n of [1, 2, 3]) if (n > 1) n * n];  

Using this syntax, an array will be yielded. In order to yield the members of the array, a * is used.
Example:

yield [for (item of items) item] // is like `yield ["Tim", "Sue", "Joy", "Tom"]`

yield* [for (item of items) item] // is like `yield "Tim"; yield "Sue"; yield "Joy"; yield "Tom"`  

Array comprehensions are a greedy syntax. In the Company example, we had a filter that would filter the list using a predicate. Rewriting it using an array comprehension:

let filter = function*(items, predicate) {  
  yield* [for (item of items) if(predicate(item)) item];
}

The problem with this is that the entire array is iterated before returning just the desired item. In order to stop iterating as soon as we've "taken" what we wanted, use a generator comprehension:

Generator Comprehension

Use () instead of []

var numbers = (for (n of [1, 2, 3]) n * n);  

So to rewrite our filter function to stop iterating when we've got what we wanted (using the take function in the Company example):

yield* (for (item of items) if(predicate(item)) item);  

Essentially, use the Generator Comprehension when you want to be lazy and do as little work as possible, or use the Array Comprehension when you want to have everything in a data structure from the start.