Skip to content

Instantly share code, notes, and snippets.

@heikomat
Last active December 10, 2020 10:41
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save heikomat/c3842dcb83f3dd2a62684744979aed3d to your computer and use it in GitHub Desktop.
Save heikomat/c3842dcb83f3dd2a62684744979aed3d to your computer and use it in GitHub Desktop.
Introduction to ioc, di, functions, scopes, callbacks, promises and async/await

Inversion of Control via Dependency Injection

Without dependency injection

engine.ts

export class Engine {}

car.ts

import {Engine} from './engine'

export class Car {
  constructor() {
    this.engine = new Engine();
  }
}

With dependency injection

engine.ts

export class Engine {}

car.ts

export class Car {
  constructor(engine) {
    this.engine = engine;
  }
}

ioc_modue.ts

import {Engine} from './engine';
import {Car} from './car'

ioc_container.register('Engine', Engine);

ioc_container.register('Car', car)
  .dependencies('Engine');

ratinonale:

The engine is now no longer hard coupled to the car, but loosely coupled. We can change the engine the car is using without touching the car-file. This is very important to keep code easily replaceable, even when used in a lot of places. With this we can for example easily switch between a mock-repository and real one

Functions

There are four ways to define Functions:

  • The function declaration (test it)

    • Can only called by its name or immediately
    • Can be called before it is defined, because of function-hoisting
    • Its scope is based on who owns it, not where it is defined
    myFunction();
    function myFunction(param1, param2){}
  • The function expression (test it)

    • Can only be called by the name of the variable
    • The name of the function is optional. Omitting it makes the function anonymous
    • Can not be called before it is defined
    • Its scope is based on who owns it, not where it is defined
    // someFunction = variable name
    // myFunction = optional name of the function
    const someFunction = function myFunction(param1, param2){};
    someFunction();
  • The arrow function expression (test it)

    • Can only be called by the name of the variable or immediately
    • The function can not have a name, and is therefore always anonymous
    • Does not overwrite this, arguments, super or new.target: Its scope is inherited from the surrounding function, and not based on who owns it!
    const someFunction = (param1, param2) => {};
    someFunction();
  • The function constructor (you probably never need this one) (test it)

    const sum = new Function('a', 'b', 'return a + b');
    sum(2, 6); // 8

Scope

The scope of something is the context in which it exists. There are two interesting scopes:

  • The scope in which a variable exists, and
  • The scope in which a function is executed

Scopes of variables

The scope in which a variable exists can be

  • global
  • the closest function or
  • the closest block

Which of these applies is determined by the way the variable is declared. There are four ways:

  • without a keyword: The variable is global. (test it)

    foo = 'test'
  • with the var keyword: The variable exists in the closest function. (test it)

    function abc() {
      function def() {
        if (true) {
          var foo = 'test';
          // foo exists here
        }
    
        // and foo exists here, too
      }
    
      // but foo does not exist here!
    }
  • As parameter: The variable exists in the closest function. A Parameter has the same scope as a variable that was declared using var (test it)

  • with the let or the const keywords: The variable exists in the closest block` (test it)

    function abc() {
      if (true) {
        const foo = 'test';
        // foo exists here
      }
    
      // but foo does not exist here!
    }

Scopes of functions

Basics

The scope of a function is the object that owns the function. You can imagine it as its "home". Within the function, it can be accessed using the this-keyword.

Here is a simple example (test it):

const someObject = {
  hello: 'world',
  myMethod0: function() {
    console.log(this);
  }
}

someObject.myMethod0(); // {hello: "world", myMethod0: ƒ}

Now lets see what happens if we make another object own the very same function (test it):

const someObject = {
  hello: 'world',
  myMethod0: function() {
    console.log(this);
  }
}

const someOtherObject = {
  hello: "moon",
  myMethod0: someObject.myMethod0
}

someObject.myMethod0(); // {hello: "world", myMethod0: ƒ}
someOtherObject.myMethod0(); // {hello: "moon", myMethod0: ƒ}

We haven't changed the function, but in one example its scope (or "home") is someObject, and in the other its someOtherObject!

We can also force the scope of a function with .bind. It returns a clone of that function, but with a different scope that cannot be changed, even when we'd try to .bind again (test it):

const someObject = {
  hello: 'world',
  myMethod0: function() {
    console.log(this);
  }
}

const someOtherObject = {hello: "moon"}
const myMethod0Clone = someObject.myMethod0.bind(someOtherObject);

someObject.myMethod0(); // {hello: "world", myMethod0: ƒ}
myMethod0Clone(); // {hello: "moon"}

Note how the scope of the cloned method is only {hello: "moon"}, wihtout the myMethod0: ƒ.

The arrow function

The main difference between the arrow function and other methods of defining functions is, that its scope is not defined by who owns the function, but is instead inherited from the surrounding function.

function someFunction() {
  console.log('scope of parentFunction', this);
  const someObject = {
    hello: 'world',
    arrowFunction: () => {
      console.log('scope of arrowFunction', this);
    },
    regularFunction: function() {
      console.log('scope of regularFunction', this);
    }
  }
  
  someObject.arrowFunction();  // {hello: "moon", parentFunction: ƒ}
  someObject.regularFunction();  // {hello: "world", arrowFunction: ƒ, regularFunction: ƒ}
}

const someOtherObject = {
  hello: "moon",
  parentFunction: someFunction,
}

someOtherObject.parentFunction(); // {hello: "moon", parentFunction: ƒ}

arrowFunction will always inherit the scope from someFunction, because

  • It is an arrow-function-expression, and
  • someFunction is the surrounding function.

That means the scope of arrowFunction is someOtherObject even though its "home" is someObject.

Staying in control of the scope

In JavaScript, you often pass around functions as parameters (so called callbacks). The problem with this is, that you can't always control who owns these functions. In other words, by default you don't have control of the scope (the value of the this-keyword) of your own function.

This is ok if you don't use this within your function. But what can you do if you need to access this?

Here are two things people used to do:

  1. Define a variable to access the outer scope, even when this is overwritten (test it)
function abc() {
  var self = this;
  someOtherFunction(function myCallback() {
    // i have no idea in what scope this callback is executed, so i can't trust 'this'
    // but i can still trust 'self'!
  });
}
  1. .bind the callback to the surrounding scope (test it)
function abc() {
  someOtherFunction(function myCallback() {
    // i was the first one to bind the function, so 'this' is definitely unchanged!
  }.bind(this));
}

Today, people use arrow-functions (test it):

function abc() {
  someOtherFunction(() => {
    // arrow-functions don't overwrite 'this', so 'this' is definitely unchanged!
  });
}

Additional Information about arrow-functions in classes

One important information is, that if you define a class method as an arrow function, the scope of that function will always be the class instance, no matter what. Here is a simple example:

class Test {
  someProperty = 'test'; 
  someRegularMethod() { console.log(this) }
  someArrowFunction = () => { console.log(this) };
}
const test = new Test();

in this code, you could change the owner, and therefore the scope of someRegularMethod if you wanted to, but you can not change the scope of someArrowFunction.

The reason for this is the way classes work under the hood, which is out of the scope of this document, but here is the exact same class without using the class-keyword:

function Test() {
  this.someProperty = 'test';
  this.someArrowFunction = () => { console.log(this) };
}
Test.prototype.someRegularMethod = function() { console.log(this) };
const test = new Test();
  • The new-keyword executes a function as a constructor-function
    • The job of A constructor-function is to construct a new object (aka class instance)
    • To do so, the this-keyword of a constructor-function always references the to-be-created object, instead of the functions owner
    • Therefore the "scope" of the constructor-function is always the to-be-created object
  • the arrow-function (someArrowFunction) is defined in the constructor-function
    • Therefore the closest function is the constructor-function
    • Therefore it inherits the scope of the constructor function
    • Therefore the scope of the arrow-function is always the to-be-created object

Callbacks, Promises und Async/await

An Asynchronous function

As you probably know by now, callbacks are functions that are passed to other functions as parameters. This allows us to "wait" for something asynchronous to be finished (test it):

console.log('first log')

// lets send some http-request
doSomeAsyncHTTPRequest((result) => {
  // The function that made the request executed this callback here when the request finished
  console.log('third log');
});

// the code immediately continues executing. It doesn't wait for the request to finish.
// Thats why we call it asynchronous
console.log('second log');

Callback Hell

Doesn't look to bad, does it? But what if we want to wait for something asynchronous, and when that is done, wait for the next thing that is asynchronous etc.? Then "callback hell" happens (test it):

console.log('log1');
someAsyncFunction1(() => {

  console.log('log3');
  someAsyncFunction2(() => {

    console.log('log4');
    someAsyncFunction3(() => {

      console.log('log5');
      someAsyncFunction4(() => {

        console.log('log6');
        // we are finally done waiting for four asynchronous calls
      });
    });
  });
});

console.log('log2');

Promise to the rescue!

A Promise is a special object that represents (as the name implies) the Promise for a result. It's not the result itself, but a placeholder for a result, and it is something you can immediately work with! All Promise-objects have the two Methods .then(callback) and .catch(callback).

  • with .then you can define a function that gets called when the promise gets fullfilled (aka. resolved).

    This happens when the asynchronous operation was successful, and the thing the Promise was a placeholder for is now available.

  • With .catch you can define a function that gets called when the promise won't get fullfilled (aka. rejected).

    This happens when the asynchronous operation was not successful, and the thing the promise was a placeholder for won't be available.

This is what the httpRequest-Example would look like with a Promise instead of a callback (test it):

console.log('first log')

// lets send some http-request. This now returns a Promise!
doSomeAsyncHTTPRequest()
  .then((result) => {
    // The function that made the request has now resolved the Promise it returned
    console.log('third log');
  });

// the code immediately continues executing. It doesn't wait for the request to finish.
console.log('second log');

You might be thinking now: "Hey, i see callbacks there, you promised me promises! How is that any better?"

The answer is: Chainability!

both the .then-function and the .catch-function return Promises themselves, that get resolved when the Promise that is returned by their callback is resolved.

Although this simple example doesn't really benefit much from the use of promises, we can fix callback-hell with this (test it):

console.log('log1');
someAsyncFunction1()
  .then(() => {

    console.log('log3');
    return someAsyncFunction2();
  })
  .then(() => {

    console.log('log4');
    return someAsyncFunction3();
  })
  .then(() => {

    console.log('log5');
    return someAsyncFunction4()
  })
  .then(() => {

    console.log('log6');
  });
});

console.log('log2');

See how this looks way better than callback-hell?

Async and Await: The better Promise syntax!

The keywords async and await can be used to make asynchronous code even more readable. They are not something new (like Promises), but they are an alternative syntax for promises!

This is important. When dealing with async await, you're not dealing with some magic, you're using promises.

Here are the rules for async/await:

  • use the await-keyword when you want to wait for a promies to get resolved
  • mark every function that uses await with the async-keyword.
  • a function marked with the async-keyword always returns a Promise

That's basically it. Here is an example. Both variants do the exact same thing!:

using regular promises (test it)

function test(): Promise<void> {
  someAsynchronousFunction()
    .then((result) => {
      console.log('here is the result', result);
    });
}

using the async/await-syntax (test it)

async function test(): Promise<void> {
  const result = await someAsynchronousFunction();
  console.log('here is the result', result);
}

Now have a look how nice callback-hell looks when using Promises with async/await-syntax (test it):

console.log('log1');
console.log('log2');

await someAsyncFunction1();
console.log('log3');

await someAsyncFunction2();
console.log('log4');

await someAsyncFunction3();
console.log('log5');

await someAsyncFunction4();
console.log('log6');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment