Javascript – Objects, Functions, and Classes

Objects

Syntactically speaking, there are several ways to create an object in JavaScript. But no matter how you create an object, it’s actually abstracting an underlying API called Object.create().  

Object Literal Notation

const bike = {
  gears: 10,
  currentGear: 3,
  changeGear: function(direction, changeBy) {
    if (direction === 'up') {
      this.currentGear += changeBy;
    } else {
      this.currentGear -= changeBy;
    }
  }
}
console.log(bike.gears); // 10
console.log(bike.currentGear); //3
bike.changeGear('up', 1);
console.log(bike.currentGear); //4

Literal objects are great for one-off objects. But if you want to create two or more objects of the same type, they are not practical

Constructors

Another way to create objects is to use a constructor. A constructor is a function that contains instructions for establishing the properties of an object when that object is created and assigned. This has an advantage over object literal as you can create many instances of objects that have the same properties. In JavaScript, what this points to is not determined by where a function is defined, but rather where that function is called

function Bike(gears, startGear) {
  this.gears = gears;
  this.currentGear = startGear;
}
Bike.prototype.changeGear = function(direction,changeBy){
  if(direction === 'up') {
    this.currentGear += changeBy;
  } else {
    this.currentGear -= changeBy;
  }
}
const bike = new Bike(10, 3);
console.log(bike.gears); // 10
console.log(bike.currentGear); //3
bike.changeGear('up', 1);
console.log(bike.currentGear); //4

In this example Bike is a normal JavaScript function that defines the object. We follow JavaScript convention and capitalize the first word to signal that this function is a constructor.

The new keyword is critical. Without new, the this pointer will not point to the object you expect and causes unintended behavior. 

Notice the assignment of the changeGear function is done using something called prototype. This ensures the function is defined once and shared by all instances created from this constructor.

Assigning Properties and Functions to Objects

Properties come in three basic shapes:

  • Primitives
  • Objects
  • Arrays

There are six primitive types in JavaScript: string, number, Boolean, nullundefined, and symbol. When a variable is a primitive type, it’s passed by value when assigned. That is to say, each time a primitive is assigned, a copy of the value is made and assigned to the new variable. 

Pretty much anything that isn’t a primitive in JavaScript is an object. In object literal notation, object properties are denoted by curly brackets. 

Arrays themselves are also implemented as objects in JavaScript. Arrays can be created with the Array() constructor function or with literal notation denoted by square brackets. 

Referencing Properties by Bracket Syntax

Referencing an object member (properties and functions) is most commonly done with dot notation

bike.frontGearIndex
bike.transmission.frontGearTeeth
bike.calculateGearRatio()

In dot notation, there are strict rules for the names of properties. However, JavaScript also allows for another syntax called bracket notation.

bike["frontGearIndex"]
bike["transmission"]["frontGearTeeth"]
bike["calculateGearRatio"]()

Object Mutability

part from the different syntaxes for defining objects, there’s another key principle of JavaScript objects: mutability. 

Objects in JavaScript are mutable, which means that if you want to modify the shape of an object, you can. 

Let’s take the bike object we created. We could, for instance, add a new property or function. 

bike.isTandem = true;
bike.popAWheelie = function() {
…
};

Even though you may not have access to the code where the object is initially defined, you can modify the shape of your object once it’s in memory. The important point, though, is that only one instance of the object changes. Let’s look back at our Bike constructor:

const bike1 = new Bike();
const bike2 = new Bike();
bike1.isTandem = true;
console.log(bike1.isTandem); // true
console.log(bike2.isTandem); // undefined

If you want several objects to share the same properties or methods, then there’s an inheritance model.

Objects and Inheritance

Despite not having classes as defined by classical languages, JavaScript still has an inheritance model, called prototype inheritance

A prototype is, in fact, another object. It sits in memory, and defines properties or functions that other objects inherit if they share the same prototype. 

Traditionally in JavaScript objects share the same prototype by sharing the same constructor function. Remember the Bike constructor. We assign the changeGear function to something called prototype

function Bike(gears, startGear) {
  this.gears = gears;
  this.currentGear = startGear;
}
Bike.prototype.changeGear = function(direction, changeBy) {
  if (direction === 'up') {
    this.currentGear += changeBy;
  } else {
    this.currentGear -= changeBy;
  }
}

This way every object created from Bike inherits the changeGear function. 

You can also implement multilevel inheritance with prototypes. It is referred to as a prototype chain.

Classes and JavaScript

Under the covers, the engine is still using Object.create and there is still no class (in the object-oriented sense), just that in-memory prototype object that is the actual source of inheritance. 

class Bike {
    constructor(gears, startGear){
        this.gears = gears;
        this.currentGear = startGear;
    }
    changeGear(direction, changeBy) {
        if (direction === 'up') {
            this.currentGear += changeBy;
        } else {
            this.currentGear -= changeBy;
        }
    }
}
const bike = new Bike(10, 5);
console.log(bike.currentGear); // 5
bike.changeGear('up', 2);
console.log(bike.currentGear); // 7

The constructor function is always named constructor. An important feature is that functions and attributes automatically belong to the prototype chain without having to directly reference Object.prototype. This also simplifies creating multilevel prototype inheritance.

What is important to know is that even though the class keyword is used, the underlying object created is still a function. Executing the following code would show “function” and not “class” in the console as the type.

console.log(typeof Bike);  // Display "function"

Another difference is that function declarations can be hoisted. In other words, you can call a function that has yet to be declared. Classes do not allow this sort of thing. A class can only be accessed after its definition is evaluated. 

Classes can contain the following kinds of members.

Member
Description
Example
Constructor
The constructor is executed automatically when creating a new instance of the class. It guarantees that an initialization function is always called. This helps maintain a valid state for the class. But you don’t have to create a constructor. If one is not included, then the JavaScript engine creates an empty one for you.
constructor(name) {
  this.name = name;
}

Static Methods
Static methods are not part of any instance of the class, meaning that you can refer to these methods without referring to an instance. The concept of static class members is not new to ES6, but the static keyword is. Prior to ES6, you had to put any methods you wanted static in the constructor. Now you can put them wherever you want in the class and just use the static keyword.
static methodName() {
  return ‘something’;
}

Prototype Methods
These methods do not include the static keyword and must be referenced with an instance.
printName() {
 console.log(this.name);
}

Getters and Setters
These accessor functions work just like object literals and work the same as they did in ES5. Essentially you just put the get and set keywords in front of the property name. If you define a getter without a setter, then the property becomes read-only.
get area() {
  return this.height * this.width;
 }

set area(value) {
  this.area = value;
}

Class inheritance

Classes come in two flavors: base classes and derived classes. The difference is the extends keyword. Derived classes (also known as subclasses) have them, and base classes don’t. 

class Parent {
  constructor(name) {
    this.name = name;
  }
      
  getName() {
    return this.name;
  }
}  
class Child extends Parent {
  constructor(name) {
    super(name);
  }
        
  getMessage() {
    return 'Hello ' + super.getName();
  }
}
      
let someone = new Child('person');
console.log(someone.getMessage());     // Displays "Hello person"  

notice the use of the super keyword, which allows you to reference the parent constructor and the method definitions from the Base class.

Although commas are used to separate method definitions in objects, they are not allowed in classes. Classes can also be defined using expressions:

const myAnimal = class Animal {
  constructor(name) {
    this.name = name;
  }
          
  printName() {
    console.log(this.name);
  }
}  
  
let duck = new myAnimal('duck');
duck.printName();  // prints "duck"  

Functions

In JavaScript functions are essentially special objects. As objects, they are first-class members of JavaScript. They can be assigned as the values of variables, passed into other functions as parameters, and returned from functions. 

There are two essential phases in the life of a function: definition and invocation.  

When function is declared, its definition is loaded into memory. A pointer is then assigned to it in the form of a variable name, parameter name, or an object property.

Function Declaration

A  declaration is a statement that uses the function keyword to create a function.

// declare function
function calculateGearRatio(driverGear, drivenGear){
  return (driverGear / drivenGear);
}
// call function
let gearRatio = calculateGearRatio(42, 30);
console.log(gearRatio); // 1.4

In this code sample, function is followed by the name of the function, with parameters enclosed in the parentheses. 

This works fine, but there’s some implicit stuff happening. First of all, the function name becomes the variable name. It also implicitly assigns the variable to the enclosing context. Finally, you can call this function before it is declared, such as below where calculateGearRatio is invoked the line before the declaration. 

// call function
let gearRatio = calculateGearRatio(42, 30);
// function is declared after the line it is called
// this is allowed in function declaration
function calculateGearRatio(driverGear, drivenGear){
  return (driverGear / drivenGear);
}
console.log(gearRatio); // 1.4

Function Expressions

Function expressions accomplish the same thing as declarations more explicitly. 

const calculateGearRatio = function(driverGear, drivenGear){
  return (driverGear / drivenGear);
}
// the rest works the same
let gearRatio = calculateGearRatio(42, 30);
console.log(gearRatio); // 1.4

n this instance we have an explicitly assigned variable. Since we’ve named the pointer, we can drop the function name after the function keyword. The only catch here is that the function must be declared prior to it being invoked. 

Returning a Function

Since functions are first-class objects, another way to declare a function is when a function returns another function. This pattern is often referred to as a factory function. 

// when invoked, this function will assign a function
function gearFactory(){
  return function(driverGear, drivenGear){
    return (driverGear / drivenGear);
  }
}
// calculateGearRatio can now be invoked as a function
const calculateGearRatio = gearFactory();
// and all the rest

Anonymous Functions

There are many APIs in JavaScript that require you to pass a function for them to work. Say, for instance, you have an array, and you want to create a new array derived from the values of that array. In this case you would probably use the Array.map function.

let myArray = [1, 5, 11, 17];
let newArray = myArray.map( function(item){ return item / 2 } );
console.log(newArray); // [0.5, 2.5, 5.5, 8.5]

In this snippet, myArray.map takes in a single parameter: a function that is executed once per item in myArray

This function is never reused. It is declared as an argument (with no name…thus “anonymous”) passed into the function, and is executed in the internals of the implementation of the map function. Anonymous functions (also called lambdas in some languages) are commonplace in JavaScript. 

Function Invocation

Invocation Versus Assignment

When working with functions, one potential source of confusion to those new to JavaScript is whether you are assigning/passing a function or invoking it. It all comes down to whether you use the ()

// invoke function and assign value to ratioResult
let ratioResult = bike.calculateGearRatio();
// assign calculateGearRatio function to a new pointer
const ratioFunction = bike.calculateGearRatio;

In the first instance, calculateGearRatio is invoked with the result returned from the function being assigned (in this instance as a primitive value) to the ratioResult variable. On the other hand ratioFunction has simply been assigned or pointed to the calculateGearRatio function. You could then turn around and invoke it as ratioFunction.

Context, Scope, and Closures

Variable Scope

Variables in JavaScript are declared using the varlet, or const keywords. Where you call the keyword dictates the scope of variable being created. 

When using let or const, an argument’s or a variable’s scope is always the actual block in which it is declared. There is a classic thought exercise to show this: https://jsbin.com/gukeyic/4/edit?js,console,output

function countToThree() {
  // i is in the scope of the countToThree function
  for (var i = 0; i < 3; i++){
    console.log(i); // iteration 1: 0
    // iteration 2: 1
    // iteration 3: 2
  }
  console.log(i); // 3
}

While not necessarily bad, hoisting is often misunderstood and can create variable leakage or cause accidental overwrites if a variable is redeclared in a code block. To address these misunderstandings let and const were added to the language to create variables with block-level scope. Let’s revisit the thought exercise.

for (let j = 0; j < 3; j++){
  console.log(j); // 0
  // 1
  // 2
}
console.log(j); // error

By substituting let for var, we now have a variable that exists only in the context of the for loop. Trying to access it after the loop has closed gives us an error. 

Context and this

Objects are where state is tracked. When a function is invoked, there is always an object container around that function. This object container is its context and the this keyword points to that context. So context is not set when a function is declared, but rather where the function is invoked

Because functions can be passed around between objects, what this points to can change.

var obj = {
  aValue: 0,
  increment: function(incrementBy) {
    this.aValue = this.aValue + incrementBy;
  }
}

If you then access the increment function, it works as expected.

obj.increment(2);
console.log(obj.aValue); // 2

But let’s assign that function to another variable and see how it works. 

//assign function to variable
var newIncrement = obj.increment;
//now invoke through the new pointer
newIncrement(2);
console.log(obj.aValue); // still 2 not 4

By assigning the variable to newIncrement, the function now is executed in a different context. Specifically, in this case, in the global context. 

The Function.apply()Function.call(), and Function.bind() functions provide ways to invoke a function while explicitly binding it to a different object context.

The Global Object

When JavaScript is executed without any containing object that you write as a developer, it runs in a global object. For this reason, functions invoked there are said to be running in the global context, which means that accessing this will point there. 

In a browser, the global context is the window object. You can test this easily by running the following in your browser developer tools. 

this === window; // true

Context with Your Objects

In the increment example, as long as the increment function is invoked using obj with the dot notation, this points to obj. Or, generally speaking, when calling a function as object.function() the thing to the left of the dot is always the context in which that function is invoked. 

Think about the Bike example. The Bike constructor defines several properties with the this reference. It also has functions assigned to its prototype that reference this

const Bike = function(frontIndex, rearIndex){
  this.frontGearIndex = frontIndex || 0;
  this.rearGearIndex = rearIndex || 0;
  ...
}
...
Bike.prototype.calculateGearRatio = function(){
  let front = this.transmission.frontGearTeeth[this.frontGearIndex],
  rear = this.transmission.rearGearTeeth[this.rearGearIndex];
  if (front && rear) {
    return (front / rear) ;
  } else {
    return 0;
  }
};

We then call Bike with the new keyword. 

const bike = new Bike(1,2);
console.log(bike.frontGearIndex); // 1
console.log(bike.rearGearIndex); // 2

This looks like we’re invoking the Bike constructor in the global context. However, the new keyword shifts the context (and the this pointer) to the new object on the left side of the assignment. 

When we invoke any of the functions they are now members of the bike object, so they use that as the containing context. 

let gearRatio = bike.calculateGearRatio();
console.log(gearRatio); // 3

It’s easy to invoke a constructor the wrong way. Here’s where things can fall apart.

const badBike = Bike(1,2);
console.log(badBike.frontGearIndex); // error
console.log(window.frontGearIndex); // 1

When you forget to use new, Bike is called like any other function and that crucial shift of this from window to the newly created object fails. Object mutability steps in and the frontGearIndex property is added to window instead

The class syntax in JavaScript forces you to invoke a constructor with the new keyword, so you can’t misdirect your context.

Closures

When a function is declared, it holds a reference to any variables or arguments declared within it, and any variables it references in the scope that it is contained within. This combination of its variables and arguments along with local variables and arguments from its containing scope is called a closure. 

Consider this function, and the function it returns. 

const greetingMaker = function(greeting){
  return function(whoGreeting){
    return greeting + ", " + whoGreeting + "!";
  }
}
const greetingHello = greetingMaker("Hello");
const greetingBonjour = greetingMaker("Bonjour");
const greetingCiao = greetingMaker("Ciao");
console.log(greetingHello("Gemma")); // Hello, Gemma!
console.log(greetingBonjour("Fabien")); // Bonjour, Fabien!
console.log(greetingCiao("Emanuela")); // Ciao, Emanuela!

When greetingMaker is invoked, we might normally imagine its greeting argument to last only for the life of it being called. 

But the returned function keeps a reference to the greeting argument in greetingMaker’s scope. So that finally when it is invoked through greetingHello/Bonjour/Ciao, it’s still accessible. 

Modules

ES6 introduced a long-overdue native module system. But it was separate from all the other ES6 functionality and for a long time, no major browsers supported it. However, that’s finally changed and now most browsers allow you to load ES6 modules with the type="module" attribute on the HTML5 script tag.

Modules are pretty simple to create and use. An ES6 module is essentially just a file containing some JavaScript. Everything inside the module is scoped to that module only. If you want to make something—like a function, a variable, or a class—available somewhere else, you need to use an export statement. You then use an import statement to specify what you want to use from the exported module.

Using variables and functions from other files:

import { printMsg } from './module1.js';
import { msg2, msg1 } from './module2.js';
printMsg(msg1 + msg2);    

module1.js:

export function printMsg(message) {
  const div = document.createElement('div');
  div.textContent = message;
  document.body.appendChild(div);
}

module2.js

let msg1 = 'Hello World! ';
let msg2 = 'This message was loaded from a module.';
export { msg1, msg2 };

It is feasible to rename imported variables

import { msg2, msg1 as msg3 } from './module2.js';
printMsg(msg3 + msg2);

If you just want to import everything from a module and not worry about naming any of the exports. You can do that too by using an asterisk:

import { printMsg } from './module1.js';
import * as message from './module2.js';
printMsg(message.msg1 + message.msg2);  

When referring to module exports, we call them named exports. Just the name gets exported, and you can see this for yourself. If you export a variable and then try to change the value in the imported module, you get an error. Essentially, it’s read-only.

Decorators

Decorators have become popular thanks to their use in Angular 2+. In Angular, decorators are available thanks to TypeScript, but in JavaScript they’re currently a stage 2 proposal, meaning they should be part of a future update to the language.

In its simplest form, a decorator is simply a way of wrapping one piece of code with another — literally “decorating” it. This is a concept you might well have heard of previously as functional composition, or higher-order functions.

This is already possible in standard JavaScript for many use cases, simply by calling on one function to wrap another:

function doSomething(name) {
  console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
  return function() {
    console.log('Starting');
    const result = wrapped.apply(this, arguments);
    console.log('Finished');
    return result;
  }
}

const wrapped = loggingDecorator(doSomething);

This example produces a new function — in the variable wrapped — that can be called exactly the same way as the doSomething function, and will do exactly the same thing. The difference is that it will do some logging before and after the wrapped function is called:

doSomething('Graham');
// Hello, Graham

wrapped('Graham');
// Starting
// Hello, Graham
// Finished

Decorators use a special syntax in JavaScript, whereby they are prefixed with an @ symbol and placed immediately before the code being decorated.

It’s possible to use as many decorators on the same piece of code as you desire, and they’ll be applied in the order that you declare them.

@log()
@immutable()
class Example {
  @time('demo')
  doSomething() {
    //
  }
}

This defines a class and applies three decorators — two to the class itself, and one to a property of the class:

  • @log could log all access to the class
  • @immutable could make the class immutable — maybe it calls Object.freeze on new instances
  • @time will record how long a method takes to execute and log this out with a unique tag.

Decorators also allow for a cleaner syntax for applying these wrappers around your code, resulting in something that detracts less from the actual intention of what you’re writing. Decorator provides a good way of writing some simple helper code that can be applied to a lot of places in a very clean and easy-to-understand manner.