Functions#

Functions are first-class values. JavaScript ships three forms (function declarations, function expressions, arrow functions), plus generators (function*) and async functions (async function). Pick the form that fits this semantics and the calling convention.

/concurrency`. For methods and this binding inside classes, see OOP.

Forms#

// function declaration; hoisted
function add(a, b) { return a + b; }

// function expression; not hoisted
const add2 = function(a, b) { return a + b; };

// arrow; lexical this, no arguments, no constructor
const add3 = (a, b) => a + b;

// method shorthand
const obj = {
  add(a, b) { return a + b; },
};

// generator
function* counter() { let n = 0; while (true) yield n++; }

// async
async function fetchJson(url) {
  const r = await fetch(url);
  return r.json();
}

Pick the form by what binding the function needs.

  • Arrow functions capture this from the enclosing scope. Use them for callbacks (map, event handlers) and short expressions.

  • Function declarations / expressions have their own this, bound by the call site. Use them for methods (when the method shorthand inside an object literal is too far away) and constructors.

  • Generators suspend with yield and resume on demand.

  • Async functions return a Promise; await inside them unwraps another Promise without nesting .then.

Parameters#

JavaScript does not enforce arity. Missing arguments are undefined; extras are ignored (but available via the arguments object in non-arrow functions or via a ...rest parameter).

function greet(name = "operator") {
  return `hello, ${name}`;
}

function log(level, ...msg) {
  console.error(`[${level}]`, ...msg);
}
log("info", "started", 42);

// destructuring parameters
function listen({host = "0.0.0.0", port = 80} = {}) {
  console.log(`${host}:${port}`);
}
listen({port: 8080});

The = {} default keeps listen() (no argument) from destructuring undefined and throwing.

Returns#

return exits the function with a value; bare return or falling off the end returns undefined. ASI means a newline after return returns undefined; the operator keeps the value on the same line.

function buildResponse() {
  return                            // ASI inserts ; here
    { ok: true };                   // unreachable
}
// returns undefined, not the object

Multiple returns you want together: return an array ([value, error]) or an object ({value, error}).

Closures#

A function value carries the bindings visible at its definition site. Those bindings are upvalues; they live as long as a closure that references them.

function makeCounter() {
  let n = 0;
  return () => ++n;
}

const next = makeCounter();
next(); next(); next();             // 1, 2, 3

Closures are how event handlers, promise chains, and partial application all work. They are the operator’s primary tool for private state in functional code.

This binding#

this in a regular function is set by the call site.

function show() { console.log(this.x); }
const obj = {x: 1, show};
obj.show();               // this = obj, prints 1
const ref = obj.show;
ref();                    // this = undefined (strict) or global
show.call({x: 99});       // this = {x: 99}, prints 99

Arrow functions ignore the call site; this is whatever the enclosing scope’s this is. This is what makes them right for callbacks inside methods.

class Server {
  constructor() { this.requests = 0; }
  onRequest() {
    setInterval(() => { this.requests++; }, 1000);  // arrow captures `this`
  }
}

Generators#

function* produces a generator function; calling it returns an iterator that yields values one at a time.

function* range(start, stop, step = 1) {
  for (let i = start; i < stop; i += step) yield i;
}

for (const i of range(0, 10, 2)) console.log(i);

Generators implement the iterator protocol, so they work directly in for...of and spread.

const xs = [...range(0, 5)];        // [0, 1, 2, 3, 4]

Async#

async functions always return a Promise. await inside them pauses the function until the awaited promise settles, then resumes with its resolved value (or throws its rejection).

async function fetchAll(urls) {
  const responses = await Promise.all(urls.map(u => fetch(u)));
  return Promise.all(responses.map(r => r.json()));
}

try {
  const data = await fetchAll(urls);
} catch (e) {
  console.error("failed:", e);
}

See Concurrency for the event loop, promise mechanics, and parallel patterns.

References#