Declarations

Variables can also be declared with let and const.

let

let variables are scoped to the nearest block - curly braces- and are not hoisted. Variables declared with let can be reassigned, but cannot be redeclared within the same scope (curly braces scope).

let in for loops

Var in for loops with callbacks can gotcha.  The full loop cycle is run first which registers then callbacks, and after the loop the callbacks are executed.  Because the var is hoisted and each loops re-assigns the value of the loop counter (i), all the callback are called with the final value of i.

Let is not hoisted.  Using let, a new i variable is created with each iteration of the for loop and each callback receives its own version of i:

for (let i in usernames) {
  _fetchProfiles('/users/' + usernames[i], function () {
    console.log("fetched ", usernames[i]);
  });
}

const

const keyword creates a read-only named constants.

This is helpful for instance where you're using a magic number through conditionals, and you want the reader to understand the number is important throughout the conditionals.

Once assigned, constants cannot be assigned a new value.  Variables declared with const must be assigned an initial value.  Constants are block scoped like let and not hoisted to the top of the function.

const and let are similar.  Semantic difference is const can signify that value will not ever change over time.

Functions

Default function values

Flexible function arguments may cause problems.  JS will allow us to call a function with no arguments or specific argument of undefined.  Whereas a function my expect to be passed an array and errors calling .length on nothing or undefined.

Manual argument checks don't scale well checking multiple arguments.  Common practice is to check for existence of arguments in first line of function:

function loadProfiles (userNames) {
  let names = typeof userNames !== 'undefined' ? userNames || [];
  ...// use names safely
}

Can now move default function values from the function body to the function signature:

function loadProfiles (userNames = []) {
  ...// use namesNames safely
}

Default value will be used if function called without the argument, or explicitly with undefined.

Names parameters

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

Now we know which optional arguments are available. Each option is available from the function body as a local variable.

function setPageThread(name, { popular, expires, activeClass } = {}) {
  console.log(popular);
}

Function is called as would usually call with an option object:

setPageThread("new version soon", {
  popular: true,
  expires: 1000,
  activeClass: 'is-page-thread
});

Options unspecified in the options object will be undefined.  If no options object passed at all, code will error unless default blank option object passed.

Rest parameters

Variadic functions are function that take any number of arguments.

Using the arguments object for variadic functions is problematic however. Makes function look like takes no arguments, when in fact  it takes infinite arguments.

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.

...dots make arguments a rest parameter, which will group all arguments into an actual array.  The rest parameters must be the last parameter in the function signature.

function displayTags(targetElement, ...tags) {
  for (let i in tags) {
    _addToTopic(tags[i]);
  }
}

Spread Operator

The spread operator allows us to split an Array argument into individual elements (often as expected by the rest parameters of the variadic function).

let tags = [... really long array.. ];
displayTags(someElement, ...tags);

Arrow Functions

Objects help keep code encapsulated, organised and easier to write tests for.

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

TagComponent.prototype.render = function () {
  getRequest(this.urlPath, function(data){
    //cannot access this.targetElement in callback 
  });
}

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

Issue here with callback scope of getRequest.  Scope of the TagComponent object is not the same as the scope of the callback anonymous function.

Arrow functions fix this issue.  Arrow functions bind to the scope of where they are defined, not where they are used. Known as lexical binding.
(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression // equivalent to:  => { return expression; }

Parentheses are optional when there's only one parameter:
(singleParam) => { statements }
singleParam => { statements }

A function with no parameters requires parentheses or an underscore:
() => { statements }
_ => { statements }

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

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

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

Objects and Strings

Object initialiser shorthand - from variables to object properties

We can remove duplicate variable names from object properties when those properties have the same name as the variables being assigned to them.

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

Can be used anywhere.

Object destructuring - from object properties assign to variables

We can use shorthand to assign properties from objects to local variables with the same name.

Same names as properties from the return object:
let { first, last, fullName } = buildUser("Sam", "Williams");

We can explicitly select the properties to destructure.  I.e. only select first in example above.

Adding a method function to an object

No need to specify the property name, and the full function definition including function keyword anymore.

Old way:

function buildUser () {
  return {
    isActive: function() {
      return true;
    }
  }
};

New way, include name of property and remove function keyword:

function buildUser () {
  return {
    isActive() {
      return true;
    }
  }
};

Template strings

Template strings are string literals embedded in expressions. This allows for better string interpolation.

Wrap string in backticks rather than quotes, and include ${} for embedding:

function buildUser(first, last) {
  let fullName = `${first} ${last}`;
  //...
}

Object.assign

Object.assign is great for writing flexible and reusable functions.  Object.assign copies properties from one or more source objects to a target object specified as the first argument.

A common use case is merging a passed in options object with an internal defaults object.  If there are duplicate properties, those from the options object must override properties from the defaults object.

function countDown (target, timeLeft, options = {}) {
  let defaults = {
    //...
  };
  // target object is first argument. As many source objects as necessary
  let settings = Object.assign({}, defaults, options);
  // ...
}

In the case of duplicate properties, the value from the last object on the chain will always prevail (last passed in).

Its good to preserve source objects and assign to blank object. Particularly in case where want to check against further.

Arrays, Maps and Sets

Arrays

Array destructuring (much like object destructuring) allows us to assign multiple values from an array to local variables.

let users = ["sam", "tom", "peter"];
let [a, b, c] = users
console.log(a, b, c) // logs 'sam tom peter'

A blank space is also allowed to skip elements. Can be used with functions that return arrays.

For off loop to loop over arrays

The for off loop iterates over property values, and is better to loop over arrays and other iterable objects.

For off reads each element directly from the array and assigns to the variable:

let users = ["sam", "tom", "peter"];
for (let user of users) {
  console.log(user); // prints sam tom peter
}

With a for in loop, would be using the index: console.log(user[index]);

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 if the object is iterable.

Returns 'function' if iterable:
console.log( typeof array[Symbol.iterator]);

Array.find

Array.find returns the first element of an array that satisfies a provided testing function (evaluates to true).

let userAdmin = userArray.find( (user) => {
  return user.admin; // returns first object for which user.admin is true
});

Maps

Maps are a data structure composed of a collection of key/value pairs. They are very useful to store simple data, such as property values.

Each key is associated with one and only one value.  When use objects on a map, they're not converted to strings.

We use get and set to access values of a Map.

let user = { name: "Sam" };
let replies = new Map();
replies.set(user, 5);
replies.get(user); // returns 5

Use maps when keys are unknown until runtime.  For instance, creating a new post the author is unknown until submitted at run-time.

User maps where the keys and the values are the same type.  For keys and values of different types, best to use objects.

Maps are iterable, so they can be used with for of loops. Each cycle of the loop returns a [key, value] pair from an entry in the Map.

The WeakMap is a type of Map where only objects can be passed as keys. Primitive data types such as strings, numbers and booleans are not allowed.  All available methods on  a WeakMap require access to an object used as a key.  WeakMaps are not iterable, and cannot be used with for of loops.

WeakMaps are better with memory.  They hold a weak reference to the objects used as keys.  Therefore, individual entries can be garbage collected while the WeakMap itself still exists.

Sets

Arrays don't enforce unique items: duplicate entries are allowed.

Sets store unique values of any type, whether primitive values or object references.

Set Objects are iterable, therefore can be used with for of loops and destructuring.

WeakSets are a type of set that only allow objects.  They're more memory efficient like WeakMaps.  WeakSets cannot be used with for of loops, and they offer no methods for reading values from it.

WeakSets allows us to create special groups from existing objects without mutating them.  Favouring immutable objects allows for much simpler code with no expected side effects.  Later, we check for presence of object in the group using has method.

Classes and Modules

Classes

New class syntax for writing object orientated javascript.  Syntax sugar, doesn't change underlying affect of prototypical inheritance, and can still use old constructor and prototypical approach.

Constructor method is a special method for creating and initialising an object.  It runs every time a new instance is created with the new operator.

class nameOfClass {

  constructor(name, description url) {
    // Assigning to instance variables makes them accessible by other instance methods
    this.name = name;
    this.description = description;
    this.url = url;
  } 

  render() {
  // instance methods
  }   

  _privateMethod() {       
  // private methods
  }
}

Prefixing a method with an underscore is a convention for indicating that the method should not be invoked from the public API.

Invocation of the object is the same:

let instance = new nameOfClass(name, description, url);
instance.render();

Class inheritance

To reduce code repetition.  Child classes inherit and specialise behaviour defined in parent classes.

Extends keyword is used to create a class that inherits methods and properties from another class.

The super method runs the constructor function from the parent class.  Child classes can invoke methods from their parent classes via the super object.

class Child extends Parent {

  constructor() {
    super();
  }

  parse() {
    let parentParse = super.parse();
    // vary the parse method in the child
  }

  render() {
    // can access inherited methods and properties of the parent
  }
}

Static methods

The static keyword defines a static method for a class. Static methods are called without instantiating their class and are also not callable when the class is instantiated. Static methods are often used to create utility functions for an application.

Modules

Not adding to the global namespace.

flash-message.js:

export default function(message) {
  alert(message);
}

app.js:

import module from './flash-message';
module('Hello');

The default type export limits the number of functions we can export from a module.  In order to export multiple functions from a single module, we use named exports (remove defaults and name function/s).

We need to use the same names as the functions on the import, and wrap in curly braces.

Alternatively, can import entire module as object, and call each individual function as a property on that object:

import * as flash from './myModule';
flash.myMethod();

Can also export once at the end:

function myFunction () {
...
}
export { myFunction }

Extracting hardcoded constants

Cannot redefine constants within the same scope.

Place constants in their own module allows them to be reused across other modules and hides implementation details (aka encapsulation) - their values.

Exporting Class Modules

Classes can also be exported and imported from modules using the same syntax. Only difference is, must create an instance of the class using new keyword (also import with Capital starting letter).

Promises, Iterators and Generators

Promises

function promise(pollName) {

  return new Promise( (resolve, reject) => {
     //...
     resolve(someValue);
     // ...
     reject(someValue);
   });
}

Creating a new promise automatically sets it to pending state. Then, it can either become fulfilled (resolved) or rejected.

A promise represents a future value, such as the eventual result of an asynchronous operation.

Then method reads results from promise once it is resolved. This method takes a function that will only be invoked once the promise is resolved.

We can also chain calls to then.  The return value from one call is passed as the argument to the next.

Iterators

Iterables are objects which can return a special iterator object.  This object can access items from a collection 1 at a time, while keeping track of its current position within the sequence.  Standard objects are not iterable out-of-the-box.

An iterator is an object with the next property, returned by the result of calling the Symbol.iterator method.  We can use Object.keys to build an array with property names from our object.

let post = {
  title: 'Some title',
  replies: 19
};
post[Symbol.iterator] = function () {
  let properties = Object.keys(this);
  let count = 0;
  let idDone = false;  

  let next = () => {
    if(count <= properties.length){
      isDone = true; 
    }
      return { done: isDone, value: this[properties[count++]] };
   };

  return { next };
};

Generators

Generator functions are special functions from which we can use the yield keyword to return iterator objects.

The function * declaration defines generator functions:

function * nameList() {
  yield "Sam";
  yield "Tyler";
}

Calling the function returns a generator object:

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

Can replace iterator code above using generators.

Include * in function signature and yield keyword:

post[Symbol.iterator] = function * () {
  let properties = Object.keys(this);

  for(let p of properties){
    yield this[p];
  }
};