Functions are objects
Functions are used for code reuse, information hiding, and composition. Function are objects, and therefore presents a prototype chain, that ==may includes one or more Function.prototipe and that will end up in the same Object.prototype==.
Everything in JavaScript is included into a giant Object.prototype. If we add a property at this root level, both all the functions and all the new created objects will access this property.
Object.prototype.newProperty = 'This is a new property';
const myObject = {};
function myFunction() {}
console.log(myObject.newProperty); // "This is a new property"
console.log(myFunction.newProperty); // "This is a new property
Note that since function are objects, we can access functions properties with the dot syntax
So what is the difference between an object and a function?
- a function can be invoked
- a function have parameters
- in addition to the declared parameters, every function receives two additional parameter, arguments, and this
Arguments
Arguments is a silent keyword that we can use to get an array of all the parameters, without specifying their name. This give us some flexibility but it is not very used
// Example 1: predefined number of arguments
function func1(a, b, c) {
console.log(arguments[0]); // 1
console.log(arguments[1]); // 2
console.log(arguments[2]); // 3
}
func1(1, 2, 3);
// Example 2: undefined number of arguments
function sum() {
let out = 0;
for (let i=0; i<arguments.length; i++) {
out += arguments[i];
}
return out;
}
const ret = sum(1, 2, 3);
console.log( ret ); // 6
The rest operator ’…’ has a similar function, but it allows us to freeze some parameters while simultaneously allowing an indefinite number of parameters.
function test(a, ...args) {
console.log('the value of a:', a);
console.log('the other values:', ...args);
}
test(10, 20, true, []);
This
This is a special keyword in JavaScript, and its meaning changes depending on the function invocation pattern. There are 4 different invocation pattern:
- method invocation
- function invocation
- constructor invocation
- apply invocation
Method invocation
When a function is stored as a property of an object, it is called method.
- In this scenario, this points to the local object
const obj = {
val: 0,
text: 'test',
increment: function (inc) {
this.val += (typeof inc === 'number') ? inc : 1;
return this.val;
},
// (!) note that we cannot access the local object in an arrow function
getText: () => {
return this.text;
}
};
const incr = obj.increment(2);
const txt = obj.getText();
console.log( incr ); // 2
console.log( txt ); // undefined
Function invocation pattern
When a function is not a property of an object, it is invoked as a function:
- In this scenario, this points to the global object
- This was a mistake in the design of the language
function add(a,b){
console.log(this); // global object
return a+b;
}
const sum = add(3,4);
As a consequence, an inner function inside a method has an unexpected output regarding the keyword this:
const obj = {
val: 0,
increment: function (inc) {
this.val += (typeof inc === 'number') ? inc : 1;
return this.val;
},
decrement: function() {
// the variable name 'that' is a common convention
let that = this;
// here 'this' and 'that' are the same {val: 2, increment: fn, decrement: fn}
const helper = function () {
// here, 'this' points to the global object
// we can use 'that' to access the local 'this'
console.log( that ); // {val: 2, increment: fn, decrement: fn}
}
helper()
}
};
obj.decrement = function() {
// the decrement method could have been defined even as follows, with the same results
}
obj.decrement = () => {
// with an arrow function, the value of 'this' is an empty object at the first level and a global object inside the inner function
}
obj.decrement()
Construct invocation pattern
If a function is invoked with the new prefix, a new object will be created with a hidden link to the function’s prototype
- In this scenario, this points to the function’s prototype object
- This pattern is not recommended
// functions to be called with the 'new' prefix have a capitalized name by convention
const Quo = function (str) {
this.state = str;
}
Quo.prototype.get_status = function () {
return this.state;
}
const qui = new Quo('confused');
console.log( qui.get_status() ); // confused
Apply invocation pattern
The apply() method lets us to:
- choose the object to which this points (first parameter)
- construct an array of arguments to use to invoke the function (second parameter)
function sum(a,b,c) {
console.log(this); // {test: true}
return a + b + c;
}
const arr = [1, 2, 3];
const out = sum.apply({test: true}, arr);
console.log(out); // 6
Return
A function always return a value.
- If the return value is not specified, then undefined is returned
- If the function is invoked with the new prefix, and the return value is not an object, then the new object (local this) is returned
Exceptions
Exceptions are used to prevent error and to define custom responses in error scenarios. With the throw statement the function is interrupted.
- the keys name and message are not mandatory but conventional
function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw {
name: 'TypeError',
message: 'add needs numbers'
}
}
return a + b;
}
add('four', 'two'); // { name: 'TypeError', message: 'add needs numbers' }
The exception object will be delivered to the catch clause of a try statement
try {
add('four', 'two');
} catch (e) {
console.log(e); // { name: 'TypeError', message: 'add needs numbers' }
}
Note that a catch statement will catch all exceptions - if you have more than one error, inspect the name or the id of the exception object.
Augmenting types
We can make a method available to all functions by augmenting the Function.prototype. This practice may have limited advantages in few scenarios but it is typically discouraged
This example create a new method called ‘method’, that allow to recall the prototype object without typing .prototype:
// The method 'method' is now globally available
Function.prototype.method = function (name, func) {
this.prototype[name] = func;
return this;
}
Adding global methods directly on the prototype is generally discouraged because of:
- polluting of the global scope
- possibility of names collision, especially while using libraries or plugins
- negative impact on performance (with lots of global methods available everywhere)
- less predictability and maintainability of the code
- compatibility problems with different version of JavaScript
A better solution simply relies on importing and exporting utilities functions
Recursion
function factorial(n, incr = 1) {
return n < 2 ? incr : factorial(n - 1, incr*n );
}
const out = factorial(5);
console.log( out ); // 120
/*
factorial(5, undefined) {
⏎ factorial(4, 5);
⏎ factorial(3, 5*4)
⏎ factorial(2, 20*3);
⏎ factorial(1, 60*2);
⏎ 120;
}
*/
JavaScript does not have native tail recursion optimization. If a function returns the result of invoking itself recursively, the invocation is not replaced with a loop. Valid functions that recurse very deeply may fail by exhausting the return stack
Scope
The scope controls the visibility and lifetimes of variables and parameters. It is best to declare all of the variables used in a function, at the top of the function body.
Closure
A closure is created when a nested (inner) function "remembers" the scope in which it was defined, even after the outer scope has finished executing. In other words, a closure is a function that has access to the variables of its external lexical scope even after the external scope has ended.
function incr() {
let tot = 0;
return function() {
tot +=1;
return tot;
}
}
const counter = incr(); // a function is assigned to counter
console.log( counter() ); // 1
console.log( counter() ); // 2 the previous tot value is saved
console.log( counter() ); // 3 the previous tot value is saved
Callbacks
JavaScript is single threaded but support asynchronous programming and WW (Web Worker), that allow it to perform Parallel programming
Module
The module pattern refers to the possibility of creating an independent piece of code that exposes public variables and methods, maintaining its private variables for itself
- This pattern exploit the closure principle to create a private space that is not accessible unless it is explicitly exposed
The following example makes use of an IIFE (immediately invoked function expression)
const myModule = (function () {
// private variables
const privateVariable = "I am private";
function privateFunction() {
console.log(privateVariable);
}
// Public functions and variables (exposed)
return {
publicMethod: function () {
privateFunction();
}
};
})();
myModule.publicMethod(); // "I am private"
console.log(myModule.privateVariable); // undefined
Advantages:
- encapsulation: implementation details and complexity is hidden
- clear differentiation between private state and public state
- name space management: an isolation of private variables and methods prevents name collisions reducing the global namespace pollution
Disadvantages:
- a private state is complex to debug and to test
- difficult to integrate with other external dependencies or functions
- ES6 offers a better solution with the import/export syntax, allowing a simpler management of isolated modules
Cascade (or method chaining)
If a method of an object return this, we can enable cascades of functions. Cascades can produce interfaces that are very expressive.
- this pattern can help control the tendency to make interfaces that try to do too much at once and promotes separation of concerns
- it also makes extremely clear the order of execution
const snake = {
moveHead: function () {
console.log('moving head');
return this;
},
moveBody: function() {
console.log('moving body');
return this;
},
moveTail: function() {
console.log('shaking the tail');
return this;
}
}
snake
.moveHead() // 'moving head'
.moveBody() // 'moving body'
.moveTail(); // 'shaking the tail'
Curry
Currying is a functional programming technique that involves splitting a function that takes multiple arguments into a series of functions that take one argument each.
- This allows you to apply arguments to a function incrementally, and can lead to more modular and reusable code.
function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2);
// double contains:
// function(b) {
// return 2 * b;
// }
console.log( double(5) ); // 10
Memoization
Memoization is an optimization technique used to optimize the performance of a function that has to be called several time. It is basically a cache technique that exploits the closure principle.
With memoization, results are cached and immediately returned, preventing unnecessary computations.
The memoized version is better even with a single call, because
function testPerformance( func, ...args ) {
const iterations = 5000;
let totalTime = 0;
for (let i = 0; i < iterations; i++) {
const t0 = performance.now();
func( ...args );
const t1 = performance.now();
totalTime += t1 - t0;
}
// return an average for more precision
return totalTime / iterations;
}
function factorial(n) {
if (n == 0 || n == 1 ) return 1;
return n *factorial( n - 1 );
}
function memoizedFactorial() {
const cache = {}; // persistent cache
return function _factorial(n) {
if (n in cache) {
return cache[n];
}
if (n == 0 || n == 1 ) {
cache[n] = 1;
return 1;
}
const res = n * _factorial(n - 1);
cache[n] = res;
return res;
};
}
const num = 2000;
const t1 = testPerformance(factorial, num);
const t2 = testPerformance(factorial, num);
const t3 = testPerformance(factorial, num);
const avg = (t1+t2+t3)/3
console.log( 'avg time: ', avg );
const t4 = testPerformance(memoizedFactorial, num);
const t5 = testPerformance(memoizedFactorial, num);
const t6 = testPerformance(memoizedFactorial, num);
const avgMemo = (t4+t5+t6)/3;
console.log( 'avg time memoization: ', avgMemo );