My Profile Photo

rubycoloredglasses


I'm Jason, a web applications developer in the San Francisco Bay area.


ES2015 Notes

ES2015, formerly known as ES6, is the most extensive update to the JavaScript language since the publication of its first edition in 1997.

The committee decided to use the year of release, instead of version number.

Level 1 - Declarations

The exercises will focus on a forum web app. The first feature will be loading users profile into the sidebar of the site.

Declarations with let

The loadProfiles function takes an array of users and adds their profile to the sidebar.

<!DOCTYPE html>
   //...
   <script src="./load-profiles.js">
   <script>
     loadProfiles(["Sam", "Tyler", "Brook", "Jason"]);
   </script>
</html>

The loadProfiles Function

function loadProfiles(userNames) {
  if (userNames.length > 3) {
    var loadingMessage = "This might take a while...";
    _displaySpinner(loadingMessage);
  } else {
    var flashMessage = "Loading Profiles";
    _displayFlash(flashMessage);
  }
  console.log(flashMessage); // returns undefined
  // ... fetch names and build sidebar
}

Javascript detects undeclared variables and declares them at the top of the current scope. This is known as hoisting.

One way to avoid confusion is to use let.

let loadingMessage = "This might take a while...";

Variables declared with let are not hoisted to the top of the scope. Instead of the hoisting occurring, thus leading to an undeclared value for the references, you’ll instead get a ReferenceError informing you that the variable is not defined.

Declarations with let in for loops

When using var in for loops, there can be unexpected behavior.

function loadProfiles(userNames) {
  for (var i in userNames) {
    _fetchProfile("/users/" + userNames[i], function() {
      console.log("Fetched for ", userNames[i]);
    });
  }
}

This results in:

> Fetched for Alex
> Fetched for Alex
> Fetched for Alex
> Fetched for Alex

The last element of the array is outputted all 4 times because the _fetchProfile method is delayed in it’s execution due to an AJAX call, so when it references the variable i, the iterations have completed and the value of i is set to 3 as it’s final value. When the callbacks calls, upon completion of the AJAX request, it references the 3 and ends up outputting ‘Alex’ as the name.

This is because the var i is hoisted to the stop of the function and declared in that scope, and then other references to i. If this is replaced with let, a new i variable is created on each iteration.

for (let i in userNames) {
}

Variables declared with let can be reassigned, but cannot be redeclared within the same scope.

// no problem
let flashMessage = "Hello";
flashMessage = "Goodbye";

// problem
let flashMessage = "Hello";
let flashMessage = "Goodbye"; // results in a TypeError

// no error because defining in a different scope
let flashMessage = "Hello";
function loadProfiles(userNames) {
  let flashMessage = "Loading profiles";
  return flashMessage;
}; ## Declarations with const

Magic Number - A literal value without a clear meaning. If you end up using the number multiple times, it will lead to unnecessary duplication, which is bad code. People won’t know if these literal values are related or not.

By using const we can create a read-only named constant.

const MAX_USERS = 3;

if (userNames.length > MAX_USERS) {
   // ...
}

You cannot redefine a constant after it has been defined. Constants also require an initial value.

// will result in error
const MAX_USERS;
MAX_USERS = 10;

Block Scoped Constants are blocked scoped, which mean they are not hoisted to the top of the function. So if you define something within an if block, and try to access it from outside, it will return an error.

if (userNames.length > MAX_USERS) {
  const MAX_REPLIES = 15;
} else {
  // ...
}
console.log(MAX_REPLIES); // ReferenceError. MAX_REPLIES is not defined) # Level 2 - Functions ## Functions

Default Parameters Unexpected arguments might cause errors during function execution. This code runs just fine, because it’s passing an array to the function as expected.

loadProfiles(["Sam", "Tyler", "Brook"]);

However what if it was passed no arguments, or an undefined value.

function loadProfiles(userNames) {
  // TypeError: Cannot read property ‘length’ of undefined
  let namesLength =  userNames.length;
}

A common practice is to validate the presence of arguments in the beginning of the function.

let names = typeof userNames !== 'undefined' ? userNames : [];

We can make this code better by defining default values for the parameters.

function loadProfiles(userNames = []) {
  let namesLength = userNames.length;
  console.log(namesLength);
}

Named Parameters The options object is a widely used pattern that allows user-defined settings to be passed to a function in the form of properties on an object.

function setPageThread(name, options = {}) {
  let popular = options.popular;
  let expires = options.expires;
  let activeClass = options.activeClass;
};

setPageThread("New Version out Soon!", {
  popular: true,
  expires: 10000,
  activeClass: "is-page-thread"
});

This approach doesn’t make it very clear which options the function expects, and it requires the definition of boilerplate code to handle the option assignment.

Using named parameters for optional settings makes it easier to understand how a function should be invoked.

function setPageThread(name, {popular, expires, activeClass}) {
  console.log("Name: ", name);
  console.log("Popular: ", popular);
  console.log("Expires: ", expires);
  console.log("Active: ", activeClass);
}

Now we know which arguments are available. The function call remains the same. Each property of the parameter is mapped to the argument appropriately.

It’s NOT okay to omit the options argument altogether when invoking a function with named parameters when no default value is set for them.

// results in TypeError: Cannot read property 'popular' of undefined
setPageThread("New Version out Soon!");

// sets default value, resulting in all becoming undefined
function setPageThread(name, {popular, expires, activeClass} = {}) {
  console.log("Name: ", name);
  console.log("Popular: ", popular);
  console.log("Expires: ", expires);
  console.log("Active: ", activeClass);
}

// defaults the value of popular to an empty string
// while still defaulting the entire options hash if not provided
function setPageThread(name, {popular = '', expires, activeClass} = {}) {
  console.log("Name: ", name);
  console.log("Popular: ", popular);
  console.log("Expires: ", expires);
  console.log("Active: ", activeClass);
} ## Rest Parameter, Spread Operator, and Arrow Functions

Tags are a useful feature in web applications that have lots of read content. It helps filter results down to specific topics. Let’s add these to the forum.

We want our displayTags function to operate as follows:

// variadic functions can accept any number of arguments
displayTags("songs");
displayTags("songs", "lyrics");
displayTags("songs", "lyrics", "bands");

Arguments Object In classic Javascript we could have used the arguments object, which is a build-in Array-like object that corresponds to the arguments of a function.

This is not ideal because it’s hard to tell which parameters this function expects to be called with. Developers might not know where the arguments reference comes from (outside the scope of the function??).

function displayTags() {
  for(let i in arguments) {
    let tag = arguments[i];
    _addToTopic(tag);
  }
}

If we change the function signature, it will break our code also.

function displayTags(targetElement) {
  let target = _findElement(targetElement);

  for(let i in arguments) {
    let tag = arguments[i]; // becomes broken because the
                            // first argument is no longer a tag
    _addToTopic(tag);
  }
}

Rest Parameter The new rest parameter syntax allows us to represent an indefinite number of arguments as an Array. This way, changes to function signature are less likely to break code.

The three dots make tags a rest parameter.

function displayTags(...tags) {
  // tags in an array object
  for(let i in tags) {
    let tag = tags[i];
    _addToTopic(tag);
  }
}

function displayTags(targetElement, ...tags) {
  // ...
}

The rest parameter must always go last in the function signature.

Spread Operator We need a way to convert an Array into individual arguments upon a function call.

getRequest("/topics/17/tags", function(data){
  let tags = data.tags;
  displayTags(tags); // tags is an Array
});

Our function is expecting to be called with individual arguments, not a single argument that is an Array. How can we convert the Array argument into individual elements on the function call?

getRequest("/topics/17/tags", function(data){
  let tags = data.tags;
  displayTags(...tags); // tags is an Array
});

Prefixing the tags array with the spread operator makes it so that the call is the same as calling displayTags(tag, tag, tag).

The syntax for rest parameters and the spread operator look the same, but the former is used in function definitions and the later in function invocations.

JavaScript Objects JavaScript objects can help us with the encapsulation, organization, and testability of our code.

Functions like getRequest and displayTags should not be exposed to caller code.

getRequest("/topics/17/tags", function(data){
  let tags = data.tags;
  displayTags(...tags);
});

We want to convert code like above, into code like this:

let tagComponent = new TagComponent(targetDiv, "/topics/17/tags");
tagComponent.render();

The TagComponent object encapsulates the code for fetching tags and adding them to a page.

function TagComponent(target, urlPath) {
  this.targetElement = target;
  this.urlPath = urlPath;
}

TagComponent.prototype.render = function() {
  getRequest(this.urlPath, function(data) {
    // ...
  });
}

Properties set on the constructor function can be accessed from other instance methods. This is why the reference to this.urlPath works within the render() method.

Issues with Scope in Callback Functions Anonymous functions passed as callbacks to other functions create their own scope.

function TagComponent(target, urlPath) {
  // this scope within the component object is not the same
  // as the anonymous function assigned to 'render' below
  this.targetElement = target;
  this.urlPath = urlPath;
}

TagComponent.prototype.render = function() {
  getRequest(this.urlPath, function(data) {
    let tags = data.tags;
    // this.targetElement returns undefined
    displayTags(this.targetElement, ...tags);
  });
}

Arrow Functions Arrow functions bind to the scope of where they are defined, not where they are used. This is also known as lexical binding.

function TagComponent(target, urlPath) {
  this.targetElement = target;
  this.urlPath = urlPath;
}

TagComponent.prototype.render = function() {
  // arrow functions bind to the lexical scope
  getRequest(this.urlPath, (data) => {
    let tags = data.tags;
    displayTags(this.targetElement, ...tags);
  });
} # Level 3 - Objects, Strings, and Object.assign ## Objects and Strings

The buildUser function returns an object with the first, last, and fullName properties.

function buildUser(first, last) {
  let fullName = first + " " + last;
  return {
    first: first,
    last: last,
    fullName: fullName
  }
}

let user = buildUser("Sam", "Williams");

As you can see, we end up repeating the same thing as the key and value here in the return statement.

Object Initializer We can shorten this by using the object initializer shorthand, which removes duplicate variable names.

return {first, last, fullName}; // way cleaner

This only works when the properties and values use the same name. It works anywhere a new object is returned, not just from functions.

let name = "Sam";
let age = 45;
let friends = ["Brook", "Tyler"];

let user = {name, age, friends};

Object Destructuring

// generates 3 separate variables based on the object returned
let { first, last, fullName } = buildUser(“Sam”, “Williams”);
console.log(first);     // > Sam
console.log(last);      // > Williams
console.log(fullName);  // > Sam Williams

Not all of the properties have to be destructured all the time. We can explicitly select the ones we want.

let { fullName } = buildUser("Sam", "Williams");
console.log(fullName);

In previous versions of JavaScript, adding a function to an object required specifying the property name and then the full function definition (including the function keyword);

function buildUser(first, last, postCount) {
  let fullName = first + " " + last;
  const ACTIVE_POST_COUNT = 10;
  
  return {
    first,
    last,
    fullName,
    isActive: function() {
      return postCount >= ACTIVE_POST_COUNT;
    }
}

A new shorthand notation is available for adding a method to an object where the keyword function is no longer necessary.

return {
  first,
  last,
  fullName,
  isActive() {
    return postCount >= ACTIVE_POST_COUNT;
  }

Template Strings Template strings are string literals allowing embedded expressions. This allows for a much better way to do string interpolation.

function buildUser(first, last, postCount) {
  let fullName = first + " " + last;
  const ACTIVE_POST_COUNT = 10;
  // ...
}

You can instead use back ticks, with a dollar sign and curly brace syntax for interpolated variables.

function buildUser(first, last, postCount) {
  let fullName = `${first} ${last}`; // back-ticks, NOT single quotes
  const ACTIVE_POST_COUNT = 10;
  // ...
}

Template strings offer a new - and much better- way to write multi-line strings.

let userName = "Sam";
let admin = { fullName: "Alex Williams" };
let veryLongText = `Hi ${userName},

this is a very
very
long text.

Regards,
  ${admin.FullName}
`;

console.log(veryLongText); ## Object.assign

In this example we’ll add a count-down timer to a forum. The countdown timer displays the time left for users to undo their posts after they’ve been created. Once the time is up, they cannot undo it anymore.

We want to make our timer function reusable so that it can be used by other applications and domains.

// simple example
countdownTimer($('.btn-undo'), 60);

// container class specified
countdownTimer($('.btn-undo', 60, { container: '.new-post-options'});

// container class and time units
countdownTimer($('.btn-undo', 60, {
  container: '.new-post-options',
  timeUnit: 'minutes',
  timeoutClass: '.time-is-up'
});

For functions that need to be used across different applications, it’s okay to accept an options object instead of using named parameters.

// too many options, difficult to interpret calls to this function
function countdownTimer(target, timeLeft, {container, timeUnit, clonedDataAttribute, timeoutClass, timeoutSoonClass, timeoutSoonSeconds} = {}) {
  // ...
}

// easier to customize to different applications
function countdownTimer(target, timeLeft, options = {}) {
  // ...
}

Some options might not be specified by the caller, so we need to have default values.

function countdownTimer(target, timeLeft, options = {}) {
  let container = options.container || ".timer-display";
  let timeUnit = options.timeUnit || "seconds";
  let clonedDataAttribute = options.clonedDataAttribute || "cloned";
  let timeoutClass = options.timeoutClass || ".is-timeout";
  let timeoutSoonClass = options.timeoutSoonClass || ".is-timeout-soon";
  let timeoutSoonTime = options.timeoutSoonSeconds || 10;
}

This works, but the default strings and numbers are all over the place, which makes the code hard to understand and difficult to maintain.

Using a local object to group default values for user options is a common practice and can help write more idiomatic JavaScript. We want to merge options and defaults. Upon duplicate properties, those from options must override properties from defaults.

The Object.assign method copies properties from one or more source objects to a target object specified as the first argument.

function countdownTimer(target, timeLeft, options = {}) {
  let defaults = {
    container: ".timer-display",
    timeUnit: "seconds",
    clonedDataAttribute: "cloned",
    timeoutClass: ".is-timeout",
    timeoutSoonClass: ".is-timeout-soon",
    timeoutSoonTime: 10
  };
  // we pass a {} because the target object is modified
  // and used as return value
  // Source objects remain unchanged
  let settings = Object.assign({}, defaults, options);
}

In case of duplicate properties on source objects, the value from the last object on the chain always prevails. Properties in options3 will override options2, and options2 will override options.

function countdownTimer(target, timeLeft, options = {}) {
  let defaults = {
    // ...
  };
  let settings = Object.assign({}, defaults, options, options2, options3);
}

Because the target of Object.assign is mutated, we would not be able to go back and access the original default values after the merge if we used it as the target

// bad idea
Object.assign(defaults, options);

// Okay alternative approach
let settings = {};
Object.assign(settings, defaults, options);

We want to preserve the original default values because it gives us the ability to compare them with the options passed, and act accordingly when necessary.

function countdownTimer(target, timeLeft, options = {}) {
  let defaults = {
    // ...
  };
  let settings = Object.assign({}, defaults, options);
  
  // this wouldn't be possible without knowing if the argument
  // is different than the default
  if (settings.timeUnit !== defaults.timeUnit) {
    _conversionFunction(timeLeft, settings.timeUnit);
  }
}

Let’s run countdownTimer() passing the value for container as argument…

countdownTimer($('.btn-undo'), 60, {container: '.new-post-options'});


function countdownTimer(target, timeLeft, options = {}) {
  let defaults = {
    container: ".timer-display",
    timeUnit: "seconds",
    // ...
  };
  let settings = Object.assign({}, defaults, options);
  console.log( settings.container ); // .new-post-options
  console.log( settings.timeUnit ); // seconds
} # Level 4 - Arrays, Maps, and Sets ## Arrays

Destructuring

We typically access array elements by their index, but doing so for more than just a couple of elements can quickly turn into a repetitive task.

let users = ["Sam", "Tyler", "Brook"];

// this will keep getting longer as we need to extract more elements
let a = users[0];
let b = users[1];
let c = users[2];

console.log(a, b, c); // Sam Tyler Brook

We can use Array Destructuring to assign multiple values from an array to local variables.

let users = ["Sam", "Tyler", "Brook"];
let [a, b, c] = users; // still easy to understand, AND less code
console.log(a, b, c); // Sam Tyler Brook

Values can be discarded if desired.

let [a, , b] = users; // discarding "Tyler" value
console.log(a, b); // Sam Brook

We can combine destructuring with rest parameters to group values into other arrays.

let users = ["Sam", "Tyler", "Brook"];
let [first, ...rest] = users; // groups remaining argument in an array
console.log(first, rest); // Sam ["Tyler","Brook"]

When returning arrays from functions, we can assign to multiple variables at once.

function activeUsers() {
  let users = ["Sam", "Alex", "Brook"];
  return users;
}

let active = activeUsers();
console.log(active); // ["Sam", "Alex", "Brook"]

let [a, b, c] = activeUsers();
console.log(a, b, c); // Sam Alex Brook

Using for…of

The for…of statement iterates over property values, and it’s a better way to loop over arrays and other iterable objects.

let names = ["Sam", "Tyler", "Brook"];

for (let index in names) {
  console.log( names[index] );
}

for (let name of names) {
  console.log( name );
}

For for..of statement cannot be used to iterate over properties in plain JavaScript objects out-of-the-box.

let post = {
  title: "New Features in JS",
  replies: 19,
  lastReplyFrom: "Sam"
};

// this will not work
// TypeError: post[Symbol.iterator] is not a function
for (let property of post) {
  console.log("Value: ", property);
}

In order to work with for…of, objects need a special function assigned to the Symbol.iterator property. The presence of this property allows us to know whether an object is iterable.

Maps

Sets

Level 5 - Classes and Modules

Classes

Modules - Part 1

Modules - Part 2

Level 6 - Promises, Iterators, and Generators

Promises

Iterators

Generators