engine.ts
export class Engine {}
car.ts
import {Engine} from './engine'
export class Car {
constructor() {
this.engine = new Engine();
}
}
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
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
ornew.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
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
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 theconst
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! }
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 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
.
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:
- 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'!
});
}
.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!
});
}
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
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');
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');
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?
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 theasync
-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');