Declaring and calling a function

A function is a named, reusable block of code. The most common way to create one is a function declaration: the keyword function, a name, a list of parameters in parentheses and a body in curly braces. You call it by writing its name followed by parentheses with the arguments. return ends the function and hands a value back to the caller.

function energyCost(kwh, tariff) {
  return kwh * tariff;
}

const cost = energyCost(318.4, 0.27);
console.log(cost.toFixed(2));
console.log(energyCost(100, 0.21));
console.log(typeof energyCost);
85.97
21
function

The general ideas behind functions (decomposition, abstraction, a signature as a contract) are on Functions and Abstraction. Function names are usually verbs in lowerCamelCase: energyCost, formatTicket, isValidMeterId.

Function expressions and arrow functions

Functions in JavaScript are first-class values: a function is an object, just like an array or a string, so it can be stored in a variable, put in an array, passed as an argument and returned from another function.

Function expressions

A function expression creates a function as part of an expression, usually to assign it to a variable. The name after function is optional; without one it is an anonymous function. Unlike a declaration, a function expression only exists once the line that creates it has run (see hoisting).

const vat = function (amount) {
  return amount * 0.21;
};

const formatters = [
  function (n) { return n.toFixed(2); },
  function (n) { return `€${n.toFixed(2)}`; },
];

console.log(vat(100));
for (const format of formatters) {
  console.log(format(vat(86)));
}
21
18.06
€18.06

Arrow functions

An arrow function (ES2015) is a shorter function expression written with =>. If the body is a single expression, you can leave out the braces and the return: the value of the expression is returned automatically. With one parameter the parentheses are optional, though many teams always write them.

const double = (n) => n * 2;                 // expression body: returns implicitly
const add = (a, b) => a + b;
const now = () => 'no parameters, empty parentheses';
const describe = (kwh) => {                  // block body: needs return
  const level = kwh > 300 ? 'high' : 'normal';
  return `${kwh} kWh (${level})`;
};
const toReading = (kwh) => ({ kwh, unit: 'kWh' }); // object: wrap in ( )

console.log(double(21), add(2, 3), now());
console.log(describe(318.4));
console.log(toReading(290.1));
console.log([118, 96, 104].map((kwh) => kwh / 2));
42 5 no parameters, empty parentheses
318.4 kWh (high)
{ kwh: 290.1, unit: 'kWh' }
[ 59, 48, 52 ]

Arrow functions are not just shorter. They have no this of their own (they use the this of the surrounding code), no arguments object, and cannot be called with new. That makes them ideal for short callbacks, and a poor choice for object methods that need this; see Classes, Prototypes and this.

Parameters and arguments

Parameters are the names in the function definition; arguments are the values passed in a call. JavaScript is lenient about the number of arguments: a missing argument makes its parameter undefined, and extra arguments are silently ignored. There is no error either way, so a forgotten argument usually shows up later as NaN or undefined in a result.

function energyCost(kwh, tariff) {
  return kwh * tariff;
}

console.log(energyCost(100, 0.27));
console.log(energyCost(100));            // tariff is undefined
console.log(energyCost(100, 0.27, 'x')); // extra argument ignored
27
NaN
27

Default parameters

A parameter can have a default value, written with =. It is used when the argument is missing or is undefined (but not when it is null). The default expression is evaluated anew on every call, so a default of [] gives each call a fresh array.

function energyCost(kwh, tariff = 0.27) {
  return Number((kwh * tariff).toFixed(2));
}
console.log(energyCost(100), energyCost(100, 0.21), energyCost(100, undefined));

function addReading(value, log = []) {
  log.push(value);
  return log;
}
console.log(addReading(318.4));
console.log(addReading(290.1));   // a new empty array again, not [318.4, 290.1]
27 21 27
[ 318.4 ]
[ 290.1 ]

Rest parameters and spread

A rest parameter, written with three dots before the last parameter name (...readings), collects all remaining arguments into a real array. It is the counterpart of Python's *args. The same three dots in a call do the opposite: they spread an array out into separate arguments.

function average(label, ...readings) {
  const total = readings.reduce((sum, r) => sum + r, 0);
  return `${label}: ${(total / readings.length).toFixed(1)} kWh from ${readings.length} readings`;
}

console.log(average('KE-0042', 318.4, 290.1, 305.7));

const week = [118, 96, 104.5, 99];
console.log(average('KE-0040', ...week));   // spread the array into arguments
console.log(Math.max(...week));
KE-0042: 304.7 kWh from 3 readings
KE-0040: 104.4 kWh from 4 readings
118

Older code uses the arguments object, an array-like list of all arguments that exists inside every non-arrow function. Rest parameters replace it: they are a real array, have a clear name and also work in arrow functions.

Named options with an object

When a function has several optional settings, pass one object and unpack it in the parameter list with destructuring and defaults. Calls then read like named arguments, and the order no longer matters. The final = {} allows calling the function with no argument at all.

function createTicket(title, { priority = 'P3', team = 'Service desk', notify = true } = {}) {
  return `[${priority}] ${title} -> ${team}${notify ? ' (notified)' : ''}`;
}

console.log(createTicket('Meter display blank'));
console.log(createTicket('Substation South offline', { priority: 'P1', team: 'NOC' }));
console.log(createTicket('Typo on invoice', { notify: false }));
[P3] Meter display blank -> Service desk (notified)
[P1] Substation South offline -> NOC (notified)
[P3] Typo on invoice -> Service desk

Arguments are passed the same way as in Python: the function gets the value, and for an object or array that value is a reference to the same object. A function that changes an object it received changes the caller's object too; see pass by object reference.

Return values

return stops the function and gives back one return value. A function that ends without return, or with a bare return;, returns undefined. To return several values, return an array or an object and destructure it at the call site.

function logReading(kwh) {
  console.log(`logged ${kwh}`);
}                                 // no return statement

function stats(values) {
  return { min: Math.min(...values), max: Math.max(...values) };
}

console.log(logReading(318.4));
const { min, max } = stats([12.4, 15.1, 9.8, 17.3]);
console.log(min, max);
logged 318.4
undefined
9.8 17.3
function status() {
  return
    { state: 'ok' };
}
console.log(status());
undefined

Scope and shadowing

Scope is the part of the program where a name can be used. Each function call creates its own scope for its parameters and the variables declared inside it; those are local variables, invisible from outside. Code inside a function can read and change variables from the scopes around it.

Block scope

Block scope means that a variable declared with let or const exists only inside the nearest pair of curly braces: an if block, a loop body or a bare { } block. The older var ignores blocks; it is scoped to the whole function (or the whole script), so it leaks out of if blocks and loops. That is one of the main reasons modern code uses let and const only.

function checkTemperature(tempC) {
  if (tempC > 30) {
    let message = 'heat alert';
    var legacy = 'set inside the if block';
    console.log(message);
  }
  console.log(typeof message);   // let: gone outside its block
  console.log(legacy);           // var: still visible in the whole function
}

checkTemperature(34);
heat alert
undefined
set inside the if block

Shadowing

When an inner scope declares a variable with the same name as an outer one, the inner variable shadows the outer one: inside the inner scope, the name refers to the inner variable, and the outer variable is untouched. Shadowing is legal, but it makes code harder to read, and linters can warn about it.

const tariff = 0.27;          // outer (global) variable

function nightCost(kwh) {
  const tariff = 0.21;        // shadows the outer tariff inside this function
  return kwh * tariff;
}

console.log(nightCost(100));
console.log(tariff);          // the outer variable did not change

{
  const tariff = 0.35;        // a block can shadow too
  console.log('peak', tariff);
}
21
0.27
peak 0.35

Hoisting

Hoisting is JavaScript's behavior of registering declarations at the start of their scope, before any code in that scope runs. The effect differs per kind of declaration:

  • Function declarations are hoisted completely, so you can call them above the line where they are written.
  • var variables are hoisted and start as undefined, so reading one too early gives undefined instead of an error.
  • let, const and classes are hoisted too, but stay uninitialized until their line runs. Using them earlier throws a ReferenceError; that gap is called the temporal dead zone.
  • Function expressions and arrow functions follow the rules of the variable they are assigned to, so they cannot be called before that line.
console.log(formatTicket(31877));   // works: declarations are hoisted
console.log(region);                // var: hoisted, but still undefined
var region = 'South';

function formatTicket(n) {
  return `INC-${n}`;
}
INC-31877
undefined
console.log(tariff);   // let/const: temporal dead zone
const tariff = 0.27;
ReferenceError: Cannot access 'tariff' before initialization

The temporal dead zone is a feature: an error at the exact line is far easier to fix than a silent undefined. A function expression stored in var fails differently: the variable exists but holds undefined, so calling it gives TypeError: … is not a function.

Closures

A closure is a function together with the variables of the scope in which it was created. A JavaScript function keeps a reference to that surrounding scope, so it can still read and update those variables later, even after the outer function has returned. Every function in JavaScript is a closure in this sense; it becomes interesting when a function outlives the call that created it.

function makeTicketCounter(prefix) {
  let next = 31877;                 // lives on after makeTicketCounter returns
  return function () {
    const id = `${prefix}-${next}`;
    next++;
    return id;
  };
}

const newIncident = makeTicketCounter('INC');
const newChange = makeTicketCounter('CHG');

console.log(newIncident(), newIncident(), newIncident());
console.log(newChange());          // its own, separate counter
INC-31877 INC-31878 INC-31879
CHG-31877
The global scope holds newIncident and newChange. Each refers to a function that keeps a link to its own environment from a separate makeTicketCounter call: one with prefix INC and next 31880, the other with prefix CHG and next 31878. Global scope newIncident newChange function () { … } function () { … } makeTicketCounter('INC') call prefix = 'INC', next = 31880 makeTicketCounter('CHG') call prefix = 'CHG', next = 31878 keeps keeps
After the example: each returned function keeps its own environment alive (dashed links). The environments are not reachable any other way, so next is effectively private.

Closures give you private state without classes: nothing outside can reach next except through the returned function. They are also how callbacks remember context, how functions such as makeTicketCounter configure other functions, and how many libraries hide their internals. Functions that take or return other functions are called higher-order functions.

Closures in loops

A closure captures the variable, not a copy of its value at that moment. With var, a loop has one shared counter variable, so every function created in the loop sees its final value. With let, each round of a for loop gets a new binding, so each function keeps its own value. This is a classic interview question, and another reason to use let.

const withVar = [];
for (var i = 1; i <= 3; i++) {
  withVar.push(() => `feeder ${i}`);
}

const withLet = [];
for (let j = 1; j <= 3; j++) {
  withLet.push(() => `feeder ${j}`);
}

console.log(withVar.map((f) => f()));
console.log(withLet.map((f) => f()));
[ 'feeder 4', 'feeder 4', 'feeder 4' ]
[ 'feeder 1', 'feeder 2', 'feeder 3' ]

Callbacks and timers

A callback function is a function you pass as an argument to another function, which then calls it at the right moment: for each element of an array, when a timer expires, when a user clicks, or when data arrives. You hand over the function itself, without parentheses; check(reading) would call it immediately and pass its result instead.

function processReadings(readings, onAlert) {
  for (const r of readings) {
    if (r.kwh > 300) {
      onAlert(r);                   // call back the function we were given
    }
  }
}

const readings = [
  { meter: 'KE-0040', kwh: 118 },
  { meter: 'KE-0042', kwh: 318.4 },
  { meter: 'KE-0047', kwh: 412.9 },
];

processReadings(readings, (r) => console.log(`High usage on ${r.meter}: ${r.kwh} kWh`));

const isHigh = (r) => r.kwh > 300;
console.log(readings.filter(isHigh).length);   // pass isHigh, not isHigh()
High usage on KE-0042: 318.4 kWh
High usage on KE-0047: 412.9 kWh
2

setTimeout and setInterval

Timers are the classic callbacks. setTimeout(fn, ms) runs fn once, after at least ms milliseconds; setInterval(fn, ms) runs it repeatedly until you stop it with clearInterval(id). Both return an ID for cancelling. They are not part of the language standard itself but are provided by browsers and Node.js alike.

The callback never interrupts running code. It waits until the current script has finished and is then started by the event loop. That is why "after 0 ms" still prints after the last line of the script. How this works, and what promises and async/await add, is on Asynchronous JavaScript.

console.log('Start polling');

setTimeout(() => console.log('Timeout after 0 ms'), 0);

let polls = 0;
const timerId = setInterval(() => {
  polls++;
  console.log(`Poll ${polls}: meter KE-0042 online`);
  if (polls === 3) {
    clearInterval(timerId);
    console.log('Polling stopped');
  }
}, 20);

console.log('End of script');
Start polling
End of script
Timeout after 0 ms
Poll 1: meter KE-0042 online
Poll 2: meter KE-0042 online
Poll 3: meter KE-0042 online
Polling stopped

Recursion

A function that calls itself is recursive. Every recursive function needs a base case that stops the calls, and each call must move toward it. Recursion suits data that is itself nested, such as a tree of grid connections, folders or a JSON document. The theory, including the call stack, is on Recursion.

// A substation feeds feeders, which feed streets, which have customers
const grid = {
  name: 'Substation South',
  children: [
    { name: 'Feeder 1', children: [{ name: 'Oude Delft', customers: 120 }, { name: 'Markt', customers: 85 }] },
    { name: 'Feeder 2', children: [{ name: 'Phoenixstraat', customers: 207 }] },
  ],
};

function countCustomers(node) {
  if (!node.children) {            // base case: a street
    return node.customers;
  }
  let total = 0;
  for (const child of node.children) {
    total += countCustomers(child); // recursive case: same question, smaller part
  }
  return total;
}

console.log(countCustomers(grid));
console.log(countCustomers(grid.children[0]));
412
205

Without a reachable base case, the calls never end. Each call takes space on the call stack, and the engine stops the program when the stack is full:

function countdown(n) {
  return countdown(n - 1);   // no base case
}
countdown(3);
RangeError: Maximum call stack size exceeded

Apart from Safari's JavaScriptCore, which optimizes tail calls in strict mode, engines do not optimize deep recursion away. In Node.js with its default settings, a very simple function reaches the limit after roughly 10,000 nested calls; the exact number depends on the engine and on how much each call stores. For very deep data, or a long chain of steps, a loop is the safer choice.