Functions

A function is a a of code that is defined once but may invoked any number of times. A function definition may include a list of parameters that work as local variables for the body of the function. Function invocations provide arguments for the function parameters. Functions often use their argument values to compute a return value that becomes the value of the function invocation expression. In addition to arguments, function invocation has the invocation context, which is the value of this keyword.

If a function is assigned to a property of an object, it is known as a method of that object. When a function is invoked on or through an object, that object is the invocation context for the function. Functions designed to initialize a newly created object are called constructors.

In JavaScript, functions are objects and you can assign properties on them and even invoke methods on them. Functions can be assigned to variables and passed to other functions. Functions can be nested within other functions.

Function declaration

Function declaration consist of function keyword, followed by the function name, then comma-separated list of zero or more parameters in parentheses, and a pair of curly braces with JavaScript statements, which is the body of the function. The name of the function becomes a variable whose value is the function itself. Function declarations are hoisted to the top of the enclosing script, function, or block.

function factorial(x) {
  if(x <= 1) return 1;
  return x * factorial(x-1);
}

Function expression

A function expression does not declare a variable, so it is up to you to assign the function object to a constant or variable if you need to refer to it later. It is a better practice to use const with functions expressions so you won't accidentally overwrite your functions by assigning a new value. Function expressions are not hoisted.

const greet = function() { console.log('Hello!'); };

The name of the function is optional, but is useful for recursions. If a function expression includes a name, it becomes a local variable within the function.

const f = function fact(x) {
    if (x <= 1) return 1;
    else return x* fact(x-1);
};

Function expression can be used as arguments to other functions.

[3,2,1].sort(function(a,b) { return a-b; });

Function expressions are sometimes defined and immediately invoked.

let tensquared = function(x) {return x*x;}(10);

Arrow functions

Arrow functions use a compact syntax to define function expressions. THe general form of an arrow function is a comma-separated list of parameters in parentheses, followed by the => arrow, followed by the function body in curly braces.

const distance = (x1, y1, x2, y2) => {
  let dx = x2 - x1;
  let dy = y2 - y1;
  return Math.hypot(dx, dy);
}

If the body of the function is a single return statement, you can omit the return keyword, the semicolon that goes with it, and the curly braces. However, When an object literal is returned, you have to put the object literal inside parentheses to avoid ambiguity.

const sum = (x, y) => x + y;
const func = x => ({ value: x });

If an arrow function has exactly one parameter, you can omit the parentheses around the parameter list. However, if an arrow function has no parameters, an empty pair of parentheses must be written.

const polynomial = x => x*x + 2*x + 3;
const constantFunc = () => 42;

Function invocation

Functions are invoked with an invocation expression. Each argument expression is evaluated, and the resulting values become the arguments to the function. The return value of the function becomes the value of the invocation expression. If there is no return statement when the interpreter reaches the end of the function, or if the return statement has no value, the invocation expression evaluates to undefined.

distance(0,0,3,4); // => 5
greet(); // => undefined

A function can be invoked given the condition it is not null or undefined by inserting ?. after the function expression.

let notFunc;
notFunc?.(); // => undefined

If a function is invoked in non-strict mode, the invocation context is the global object. However, in strict mode the invocation context is undefined, which can be used to determine whether we are in strict mode.

const strict = function() { return !this; }();

Method invocation

A method is a function that is stored in a property of an object. Methods are invoked with dot notation or array access notation.

let o = {
  x: 1,
  y: 1,
  add() {
    this.result = this.x + this.y;
  }
};
o.add(); // => invoke method
o.result; // => 2
o.g = greet; // define a method based on existing function
o['g'](); // invoke method

Only arrow functions inherit the invocation context (this value) of the containing function.

let obj = {
  f() {
    function nestedFunc() {
      return this === obj;
    }
    nestedFunc(); // => false

    const nestedArrow = () => this === obj;
    nestedArrow(); // => true

    return this === obj;
  }
}
obj.f(); // => true

Method chaining is a practice where an object can be named once and then multiple methods can be invoked on it. To accomplish this, make the methods of the object return this.

let calculator = {
  result: 0,
  add: function(num) {
      this.result += num;
      return this; // Return the object itself for chaining
  },
  subtract: function(num) {
      this.result -= num;
      return this; // Return the object itself for chaining
  },
  multiply: function(num) {
      this.result *= num;
      return this; // Return the object itself for chaining
  },
  divide: function(num) {
      this.result /= num;
      return this; // Return the object itself for chaining
  }
};

calculator.add(5).multiply(2).subtract(3).divide(2).result; // => 3.5

Optional parameters

Parameters are placeholders for the values that a function will receive when it is called. Arguments are the actual values that are passed to a function when it is called.

JavaScript does not specify the expected type for function parameters, nor does it check the expected type for argument values. The number of arguments passed to the function is not checked either.

When a function is invoked with fewer arguments than declared parameters, the additional parameters are set their default value or undefined. You can use the value of a previous parameter to define the default value of the parameters that follow it.

const rectangle = (width, height=width*2) => ({width, height});
rectangle(1); // => {width: 1, height: 2}

Rest parameters

Rest parameters allow you to write functions that can be invoked with arbitrary more arguments than parameters. Such functions are called variadic or vararg functions. When you invoke a function with a rest parameter, the arguments you pass are first assigned to the non-rest parameters, and then any remaining arguments are stored in an array that becomes the value of the rest parameter.

function max(first=-Infinity, ...rest) {
  let maxValue = first;
  for (let n of rest) {
    if (n > maxValue) {
      maxValue = n;
    }
  }
  return maxValue;
}
max(1,5,10,2,100,3,4); // => 100

The spread operator ... is used to unpack the elements of an array where individual values are expected.

let numbers = [1,5,10,2,100,3,4];
Math.min(...numbers); // => 1

With the combination of rest parameters and the spread operator, a wrapped version of function can be created.

function timed(f) {
  return function(...args) {
    console.log(`Starting function ${f.name}`);
    let startTime = performance.now();
    try {
      return f(...args);
    } catch(e) {
      console.log(`An error occured during execution: ${e}`);
    } finally {
      let endTime = performance.now();
      console.log(`Exiting ${f.name} after ${endTime - startTime}ms`);
    }
  };
}

function benchmark(n) {
  let sum = 0;
  for (let i = 1; i <= n; i++) sum += i;
  return sum;
}

timed(benchmark)(1000000)

Moreover, you can use destructuring of function arguments into more clearly named parameters.

function vectorMultiply({x, y, z=0, ...props}, scalar) {
  return { x: x*scalar, y: y*scalar, z: z*scalar, ...props};
}
vectorMultiply({x: 1, y: 2, w: -1}, 2); // => {x: 2, y: 4, z: 0, w: -1}

function foo([x, y, ...coords], ...rest) {
  return [x+y, ...rest, ...coords];
}
foo([1,2,3,4], 5, 6); // => [3, 5, 6, 3, 4]

Functions as values

In JavaScript functions can be manipulated in code as values, which means we can store them as variables, pass functions as arguments, and store functions as property names.

function sq(x) { return x*x; }
sq(4); // => 16

let s = sq;
s(4); // => 16

let anObj = {sq: x => x*x };
anObj.sq(16); // => 256

let a = [x => x*x, 10];
a[0](a[1]); // => 100

Function properties

We can define our own properties on a function object that will persist across invocations.

uniqueInteger.counter = 0;
function uniqueInteger() {
  return uniqueInteger.counter++;
}
uniqueInteger(); // => 0
uniqueInteger(); // => 1

function factorialCached(n) {
  if (Number.isInteger(n) && n > 0) {
    if (!(n in factorialCached)) {
      factorialCached[n] = n * factorialCached(n-1);
    }
    return factorialCached[n];
  } else {
    return NaN;
  }
}
factorialCached[1] = 1; // Initialize base case
factorialCached(6); // => 720
factorialCached[5]; // => 120

Closures

JavaScript functions are all closures and are executed using the variable scope they were defined in, not the variable scope that is in effect when they are invoked. The internal state of JavaScript function object includes not only the code of the function but also a reference to the scope in which the function definition appears. This combination of function object and a scope (a set of variable bindings) in which the function's variables are resolved is called a closure.

let uniqueInt = (function() {
  let counter = 0;
  return function() { return counter++; };
}());
uniqueInt(); // => 0
uniqueInt(); // => 1

function counter(n) {
  return {
    get count() { return n++; },
    set count(newValue) {
      if (newValue > n) n = newValue;
      else throw Error('count can only be set to a larger value');
    }
  };
}
let c = counter(1000);
c.count; // => 1000
c.count; // => 1001
c.count = 2000;
c.count; // => 2000

Function methods

The call() and apply() methods allow you to indirectly invoke a function as if it were a method of another object. The first argument in both methods specify the invocation context on which the function is to be invoked. Any arguments to call() after the first argument are the values that are passed to the function that is invoked. The apply() method expects the arguments to be passed to the function in the form of an array.

However, invocation context argument does not work with arrow functions. Note that object literal does not change the scope for an arrow function, only a block of code or a function does. Therefore, this in the following example refers to the global object for the arrow function.

let person = {
  name: 'John',
  greet: function(age, city) {
    console.log(`Hello, ${this.name}! You are ${age} years old and live in ${city}.`);
  },
  greetArrow: (age, city) => {
    console.log(`Hello, ${this}! You are ${age} years old and live in ${city}.`);
  },
};

let anotherPerson = {
  name: 'Alice',
};

person.greet(28, 'Madrid');
// Hello, John! You are 28 years old and live in Madrid.
person.greetArrow(28, 'Tokyo');
// Hello, [object Window]! You are 28 years old and live in Tokyo.
person.greet.call(anotherPerson, 28, 'Taipei');
// Hello, Alice! You are 28 years old and live in Taipei.
person.greet.apply(anotherPerson, [28, 'Moscow']);
// Hello, Alice! You are 28 years old and live in Moscow.
person.greetArrow.call(anotherPerson, 28, 'New York');
// Hello, [object Window]! You are 28 years old and live in New York.
person.greetArrow.apply(anotherPerson, [28, 'London']);
// Hello, [object Window]! You are 28 years old and live in London.

The bind() method returns a new function that is bound to an object passed as the first argument.

function bar(y) { return this.x + y; }
let bo = { x: 1 };
let gee = bar.bind(bo);
gee(2); // => 3
gee.name; // => 'bound bar'
let po = { x: 10, gee };
po.gee(2); // => 3: gee is still bound to bo, bot po

The bind() method does not work with arrow functions, except when binding predefined arguments to the function.

let mult = (x,y) => x * y;
let double = mult.bind(null, 2); // Bind first argument to 2
double(3); // => 6
double.name; // => 'bound mult'

Functional programming

You can use functional programming style using the array methods map() and reduce() that expect functions as argument values to compute the mean and standard deviation of numbers in an array.

const summation = (x,y) => x+y;
const square = x => x*x;

let data = [1,1,3,5,5];
let mean = data.reduce(sum)/data.length;
let deviations = data.map(x => x-mean);
let stddev = Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1));

A higher-order function is a function that operates on functions, taking one or more functions as arguments and returning a new function.

function not(f) {
  return function(...args) {
    let result = f.apply(this, args);
    return !result;
  };
}

const even = x => x%2 === 0;
const odd = not(even);
[1,1,3,5,5,7].every(odd); // => true

Memoization is a technique used in computer science to optimize the performance of functions by caching the results of expensive function calls and returning the cached result when the same inputs occur again. You can create a higher-order function that accepts a function as its argument and returns a memoized version of the function.

function memoize(f) {
  const cache = new Map();

  return function(...args) {
    // Create a string version of the arguments to use as a cache key
    let key = args.length + args.join('+');
    if (cache.has(key)) {
      return cache.get(key);
    } else {
      let result = f.apply(this, args);
      cache.set(key,result);
      return result;
    }
  };
}

// Recurse to the memoized version, not original
const memoFactorial = memoize(function(n) {
  return (n <= 1) ? 1 : n * memoFactorial(n-1);
});
memoFactorial(5); // => 120