Chapter 3Functions

People think that computer science is the art of geniuses but the actual reality is the opposite, just many people doing things that build on each other, like a wall of mini stones.

Donald Knuth
Picture of fern leaves with a fractal shape

Functions are the bread and butter of JavaScript programming. The concept of wrapping a piece of program in a value has many uses. It gives us a way to structure larger programs, to reduce repetition, to associate names with subprograms, and to isolate these subprograms from each other.

The most obvious application of functions is defining new vocabulary. Creating new words in prose is usually bad style. But in programming, it is indispensable.

Typical adult English speakers have some 20,000 words in their vocabulary. Few programming languages come with 20,000 commands built in. And the vocabulary that is available tends to be more precisely defined, and thus less flexible, than in human language. Therefore, we usually have to introduce new concepts to avoid repeating ourselves too much.

3.1

Defining a function

A function definition is a regular binding where the value of the binding is a function. For example, this code defines square to refer to a function that produces the square of a given number:

const square = function(x) {
  return x * x;
};

console.log(square(12));
// → 144

Voor een function binding kunnen we wel goed voorstellen als een tentakel die de definitie van de functie grijpt zoals in onderstaande figuur te zien is.

Omdat de functie-definities te groot kunnen worden voor een dergelijk diagram, schrijven we meestal Function Definition in plaats van de hele broncode. Daarnaast schrijven we de naam van de functie soms bovenop het blok van de functie-definitie. Dit is hieronder te zien:

Om aan te geven dat dit hulpmiddelen voor onszelf zijn en niet in JavaScript-engine bestaan, geven we de tekst cursief weer.

A function is created with an expression that starts with the keyword function. Functions have a set of parameters (in this case, only x) and a body, which contains the statements that are to be executed when the function is called. The function body of a function created this way must always be wrapped in braces, even when it consists of only a single statement.

A function can have multiple parameters or no parameters at all. In the following example, makeNoise does not list any parameter names, whereas power lists two:

const makeNoise = function() {
  console.log("Pling!");
};

makeNoise();
// → Pling!

const power = function(base, exponent) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
};

console.log(power(2, 10));
// → 1024

Some functions produce a value, such as power and square, and some don’t, such as makeNoise, whose only result is a side effect. A return statement determines the value the function returns. When control comes across such a statement, it immediately jumps out of the current function and gives the returned value to the code that called the function. A return keyword without an expression after it will cause the function to return undefined. Functions that don’t have a return statement at all, such as makeNoise, similarly return undefined.

Parameters to a function behave like regular bindings, but their initial values are given by the caller of the function, not the code in the function itself.

Zoals je kunt zien in het diagram van de function binding power nemen we de paramaters en de return-waarde niet als aparte ‘doosjes’ op in de definitie van de functie.

De JavaScript engine houdt deze bindings echter wel bij. Verderop laten we zien waar de engine dit doet.

3.2

Bindings and scopes

Each binding has a scope, which is the part of the program in which the binding is visible. For bindings defined outside of any function or block, the scope is the whole program—you can refer to such bindings wherever you want. These are called global.

But bindings created for function parameters or declared inside a function can be referenced only in that function, so they are known as local bindings. Every time the function is called, new instances of these bindings are created. This provides some isolation between functions—each function call acts in its own little world (its local environment) and can often be understood without knowing a lot about what’s going on in the global environment.

Bindings declared with let and const are in fact local to the block that they are declared in, so if you create one of those inside of a loop, the code before and after the loop cannot “see” it. In pre-2015 JavaScript, only functions created new scopes, so old-style bindings, created with the var keyword, are visible throughout the whole function that they appear in—or throughout the global scope, if they are not in a function.

let x = 10;
if (true) {
  let y = 20;
  var z = 30;
  console.log(x + y + z);
  // → 60
}
// y is not visible here
console.log(x + z);
// → 40

Each scope can “look out” into the scope around it, so x is visible inside the block in the example. The exception is when multiple bindings have the same name—in that case, code can see only the innermost one. For example, when the code inside the halve function refers to n, it is seeing its own n, not the global n.

const halve = function(n) {
  return n / 2;
};

let n = 10;
console.log(halve(100));
// → 50
console.log(n);
// → 10
Oefening 3.2.1: Scoping rules let and var (A)

Schrijf voor elk van de onderstaande stukken code de output op van console.log. Kies daarbij uit 42, undefined, of ReferenceError

Voorbeeld: code

console.log(a1);
let a1 = 'hello';

Voorbeeld: antwoord

error

De oefening

console.log(a2);
var a2 = 42;
Oefening 3.2.2: Scoping rules let and var (B1)
if (true) {
    let b1 = 42;   
}
console.log(b1);
Oefening 3.2.3: Scoping rules let and var (B2)
if (true) {
    var b2 = 42;   
}
console.log(b2);
Oefening 3.2.4: Scoping rules let and var (C1)
for (let c1 = 0; c1 < 42; c1++) {
    //do something interesting
}
console.log(c1);
Oefening 3.2.5: Scoping rules let and var (C2)
for (var c2 = 0; c2 < 4; c2++) {
     //do something interesting
}
console.log(c2);
Oefening 3.2.6: Scoping rules let and var (D1)
function test() {
    let d1 = 42;
}
console.log(d1);
Oefening 3.2.7: Scoping rules let and var (D2)
function test() {
    var d2 = 42;
}
console.log(d2);

Nested scope

JavaScript distinguishes not just global and local bindings. Blocks and functions can be created inside other blocks and functions, producing multiple degrees of locality.

For example, this function—which outputs the ingredients needed to make a batch of hummus—has another function inside it:

const hummus = function(factor) {
  const ingredient = function(amount, unit, name) {
    let ingredientAmount = amount * factor;
    if (ingredientAmount > 1) {
      unit += "s";
    }
    console.log(`${ingredientAmount} ${unit} ${name}`);
  };
  ingredient(1, "can", "chickpeas");
  ingredient(0.25, "cup", "tahini");
  ingredient(0.25, "cup", "lemon juice");
  ingredient(1, "clove", "garlic");
  ingredient(2, "tablespoon", "olive oil");
  ingredient(0.5, "teaspoon", "cumin");
};

The code inside the ingredient function can see the factor binding from the outer function. But its local bindings, such as unit or ingredientAmount, are not visible in the outer function.

The set of bindings visible inside a block is determined by the place of that block in the program text. Each local scope can also see all the local scopes that contain it, and all scopes can see the global scope. This approach to binding visibility is called lexical scoping.

3.3

Functions as values

A function binding usually simply acts as a name for a specific piece of the program. Such a binding is defined once and never changed. This makes it easy to confuse the function and its name.

But the two are different. A function value can do all the things that other values can do—you can use it in arbitrary expressions, not just call it. It is possible to store a function value in a new binding, pass it as an argument to a function, and so on. Similarly, a binding that holds a function is still just a regular binding and can, if not constant, be assigned a new value, like so:

let launchMissiles = function() {
  missileSystem.launch("now");
};
if (safeMode) {
  launchMissiles = function() {/* do nothing */};
}

In Chapter 5, we will discuss the interesting things that can be done by passing around function values to other functions.

Dit is een van de belangrijkste verschillen van JavaScript ten opzichte van bijvoorbeeld Java. In JavasScript is het al vanaf de eerste versie mogelijk om function-bindings te maken. Daarnaast kun je functies op elke plek in de code definieren en niet alleen in een klasse. Soms lees, of hoor je daarom: ‘In JavaScript functies are firts-class citizens’.

Oefening 3.3.1: Function in Variable (A)

Gegeven deze code.

const testFunction = function() {
    console.log('test');
}

const x = testFunction;

Vul het onderstaande lege geheugenmodelsjabloon in op basis van deze code.

Vul het sjabloon in dat te vinden is in je persoonlijke repo en raadpleeg onderstaande tekeninstructies om te weten wat je waar moet invullen.

Oefening 3.3.2: Function in Variable (B)
const testFunction = function() {
    console.log('test');
}

const x = testFunction;
x(); //<-- Werkt dit, of krijg je een foutmelding?

Leg op basis van het geheugenmodel uit of de regel x() werkt, of een foutmelding geeft.

Oefening 3.3.3: Value and Type (A)

Gegeven is onderstaande functie:

createGreeting = function(name) {
    return `hello ${name}`;
}

Geef bij elke onderstaande expressie de waarde en het datatype die deze expressie oplevert.

Voorbeeld: code

createGreeting(10);

Voorbeeld: antwoord

waarde: hello 10,  datatype: string

Opgave

createGreeting('han');
Oefening 3.3.4: Value and Type (B)
createGreeting();
Oefening 3.3.5: Value and Type (C)
createGreeting;
Oefening 3.3.6: Value and Type (D)
createGreeting.toString();
3.4

Declaration notation

There is a slightly shorter way to create a function binding. When the function keyword is used at the start of a statement, it works differently.

function square(x) {
  return x * x;
}

This is a function declaration. The statement defines the binding square and points it at the given function. It is slightly easier to write and doesn’t require a semicolon after the function.

In diagrammen tekenen we functie-declaraties op dezelfde manier als functie-expressies.

There is one subtlety with this form of function definition.

console.log("The future says:", future());

function future() {
  return "You'll never have flying cars";
}

The preceding code works, even though the function is defined below the code that uses it. Function declarations are not part of the regular top-to-bottom flow of control. They are conceptually moved to the top of their scope and can be used by all the code in that scope. This is sometimes useful because it offers the freedom to order code in a way that seems meaningful, without worrying about having to define all functions before they are used.

3.5

Arrow functions

There’s a third notation for functions, which looks very different from the others. Instead of the function keyword, it uses an arrow (=>) made up of an equal sign and a greater-than character (not to be confused with the greater-than-or-equal operator, which is written >=).

const power = (base, exponent) => {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
};

Ook arrow-functies tekenen we op dezelfde manier als eerder genoemde notaties.

The arrow comes after the list of parameters and is followed by the function’s body. It expresses something like “this input (the parameters) produces this result (the body)”.

When there is only one parameter name, you can omit the parentheses around the parameter list. If the body is a single expression, rather than a block in braces, that expression will be returned from the function. So, these two definitions of square do the same thing:

const square1 = (x) => { return x * x; };
const square2 = x => x * x;

When an arrow function has no parameters at all, its parameter list is just an empty set of parentheses.

const horn = () => {
  console.log("Toot");
};

There’s no deep reason to have both arrow functions and function expressions in the language. Apart from a minor detail, which we’ll discuss in Chapter 6, they do the same thing. Arrow functions were added in 2015, mostly to make it possible to write small function expressions in a less verbose way. We’ll be using them a lot in Chapter 5.

Oefening 3.5.1: To Return (A)

Geef voor elk stuk code aan wat de return-waarde is van square(10). Maak daarbij de keuze uit 100, undefined, of error.

function square(x) {
    return x * x;
}

square(10); //Wat is de return-waarde van dit statement?
Oefening 3.5.2: To Return (B)
function square(x) {
    x * x;
}

square(10); //Wat is de return-waarde van dit statement?
Oefening 3.5.3: To Return (C)
function square(x) {
    return console.log(x * x);
}

square(10); //Wat is de return-waarde van dit statement?
Oefening 3.5.4: To Return (D)
const square = x => {
    return x * x;
}

square(10); //Wat is de return-waarde van dit statement?
Oefening 3.5.5: To Return (E)
const square = x => {
    x * x;
}

square(10); //Wat is de return-waarde van dit statement?
Oefening 3.5.6: To Return (F)
const square = x => x * x;

square(10); //Wat is de return-waarde van dit statement?
Oefening 3.5.7: To Return (G)
const square = x => return x * x;

square(10); //Wat is de return-waarde van dit statement?
Oefening 3.5.8: To Return (H)
const square = x => console.log(x * x);

square(10); //Wat is de return-waarde van dit statement?
3.6

The call stack

The way control flows through functions is somewhat involved. Let’s take a closer look at it. Here is a simple program that makes a few function calls:

function greet(who) {
  console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");

A run through this program goes roughly like this: the call to greet causes control to jump to the start of that function (line 2). The function calls console.log, which takes control, does its job, and then returns control to line 2. There it reaches the end of the greet function, so it returns to the place that called it, which is line 4. The line after that calls console.log again. After that returns, the program reaches its end.

We could show the flow of control schematically like this:

not in function
   in greet
        in console.log
   in greet
not in function
   in console.log
not in function

Because a function has to jump back to the place that called it when it returns, the computer must remember the context from which the call happened. In one case, console.log has to return to the greet function when it is done. In the other case, it returns to the end of the program.

The place where the computer stores this context is the call stack. Every time a function is called, the current context is stored on top contextmeestal gebruiken wij het begrip ‘stack frame’ in plaats van ‘context’ of this stack. When a function returns, it removes the top context from the stack and uses that context to continue execution.

We maken een uitgebreider diagram dan de bovenstaande schematische weergave om te laten zien hoe het geheugengebruik van een programma er op een bepaald moment eruit ziet.

We noemen dit het geheugenmodel. In dit model zie je naast de stack ook de heap. Op de heap staan alle functie-definities.

Hieronder zie je een voorbeeld van een geheugenmodel van onderstaande code. Het geheugenmodel geeft het moment weer dat de functie power wordt uitgevoerd op regel 9. De functie heeft zijn werk gedaan, maar is nog niet van de stack verwijderd. De pijltjes in het commentaar in de code geven aan van welk moment er precies bedoeld wordt.

const power = function(base, exponent) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  } 
  return result; 
}; //<- 2

let result = power(2, 10)); //<- 1
console.log(result);
// → 1024

Nog een voorbeeld van een geheugenmodel.

const hummus = function(factor) {
  const ingredient = function(amount, unit, name) {
    let ingredientAmount = amount * factor; //<- 3
    if (ingredientAmount > 1) {
      unit += "s";
    }
    console.log(`${ingredientAmount} ${unit} ${name}`);
  };
  ingredient(1, "can", "chickpeas");
  ingredient(0.25, "cup", "tahini"); //<- 2 
  ingredient(0.25, "cup", "lemon juice");
  ingredient(1, "clove", "garlic");
  ingredient(2, "tablespoon", "olive oil");
  ingredient(0.5, "teaspoon", "cumin");
};

hummus(2); //<- 1

Het diagram geeft het moment aan dat de functie hummus (regel 17) en ingredient (regel 10) worden uitgevoerd:

Storing this stack requires space in the computer’s memory. When the stack grows too big, the computer will fail with a message like “out of stack space” or “too much recursion”. The following code illustrates this by asking the computer a really hard question that causes an infinite back-and-forth between two functions. Rather, it would be infinite, if the computer had an infinite stack. As it is, we will run out of space, or “blow the stack”.

function chicken() {
  return egg();
}
function egg() {
  return chicken();
}
console.log(chicken() + " came first.");
// → ??
3.7

Optional Arguments

The following code is allowed and executes without any problem:

function square(x) { return x * x; }
console.log(square(4, true, "hedgehog"));
// → 16

We defined square with only one parameter. Yet when we call it with three, the language doesn’t complain. It ignores the extra arguments and computes the square of the first one.

JavaScript is extremely broad-minded about the number of arguments you pass to a function. If you pass too many, the extra ones are ignored. If you pass too few, the missing parameters get assigned the value undefined.

The downside of this is that it is possible—likely, even—that you’ll accidentally pass the wrong number of arguments to functions. And no one will tell you about it.

The upside is that this behavior can be used to allow a function to be called with different numbers of arguments. For example, this minus function tries to imitate the - operator by acting on either one or two arguments:

function minus(a, b) {
  if (b === undefined) return -a;
  else return a - b;
}

console.log(minus(10));
// → -10
console.log(minus(10, 5));
// → 5

If you write an = operator after a parameter, followed by an expression, the value of that expression will replace the argument when it is not given.

For example, this version of power makes its second argument optional. If you don’t provide it or pass the value undefined, it will default to two, and the function will behave like square.

function power(base, exponent = 2) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
}

console.log(power(4));
// → 16
console.log(power(2, 6));
// → 64

In the next chapter, we will see a way in which a function body can get at the whole list of arguments it was passed. This is helpful because it makes it possible for a function to accept any number of arguments. For example, console.log does this—it outputs all of the values it is given.

console.log("C", "O", 2);
// → C O 2
3.8

Closure

The ability to treat functions as values, combined with the fact that local bindings are re-created every time a function is called, brings up an interesting question. What happens to local bindings when the function call that created them is no longer active?

The following code shows an example of this. It defines a function, wrapValue, that creates a local binding. It then returns a function that accesses and returns this local binding.

function wrapValue(n) {
  let local = n;
  return () => local;
}

let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2

Bij DWA maken we geen geheugenmodellen van closures omdat dit het diagram erg uitgebreid maakt.

This is allowed and works as you’d hope—both instances of the binding can still be accessed. This situation is a good demonstration of the fact that local bindings are created anew for every call, and different calls can’t trample on one another’s local bindings.

This feature—being able to reference a specific instance of a local binding in an enclosing scope—is called closure. A function that references bindings from local scopes around it is called a closure. This behavior not only frees you from having to worry about lifetimes of bindings but also makes it possible to use function values in some creative ways.

De volgende, grijze tekst hoef je niet te lezen.
Lees verder waar de tekst weer zwart-op-wit wordt.
einde van tekst die overgeslagen kan worden
3.11

Functions and side effects

Functions can be roughly divided into those that are called for their side effects and those that are called for their return value. (Though it is definitely also possible to both have side effects and return a value.)

The first helper function in the farm example, printZeroPaddedWithLabel, is called for its side effect: it prints a line. The second version, zeroPad, is called for its return value. It is no coincidence that the second is useful in more situations than the first. Functions that create values are easier to combine in new ways than functions that directly perform side effects.

A pure function is a specific kind of value-producing function that not only has no side effects but also doesn’t rely on side effects from other code—for example, it doesn’t read global bindings whose value might change. A pure function has the pleasant property that, when called with the same arguments, it always produces the same value (and doesn’t do anything else). A call to such a function can be substituted by its return value without changing the meaning of the code. When you are not sure that a pure function is working correctly, you can test it by simply calling it and know that if it works in that context, it will work in any context. Nonpure functions tend to require more scaffolding to test.

Still, there’s no need to feel bad when writing functions that are not pure or to wage a holy war to purge them from your code. Side effects are often useful. There’d be no way to write a pure version of console.log, for example, and console.log is good to have. Some operations are also easier to express in an efficient way when we use side effects, so computing speed can be a reason to avoid purity.

Oefening 3.11.1: Pure or not (A)

Hieronder zie je steeds een andere definitie van de functie pureOrNot. Geef bij elke situatie hieronder aan of de functie puur, of niet puur is.

function pureOrNot(name) {
    console.log(`hello ${name}`);
}
pureOrNot('han');
Oefening 3.11.2: Pure or not (B)
function pureOrNot() {
    return console;
}
pureOrNot();
Oefening 3.11.3: Pure or not (C)
function pureOrNot(name) {
    return `hello ${name}`;
}
pureOrNot('han');
Oefening 3.11.4: Pure or not (D)
let name = 'han'
function pureOrNot(name) {
    return `hello ${name}`;
}
pureOrNot(name);
Oefening 3.11.5: Pure or not (E)
let name = 'han';
function pureOrNot() {
    return `hello ${name}`;
}
pureOrNot();
Oefening 3.11.6: Pure or not (F)
function pureOrNot(name) {
    let greeting = `hello ${name}`;
    return greeting;
}
pureOrNot('han');
Oefening 3.11.7: Pure or not (G)
function pureOrNot(name) {
    return `hello ${name.toUpperCase()}`;
}
pureOrNot('han');
Oefening 3.11.8: Pure or not (H)
let greeting = ''
function pureOrNot(name) {
    greeting = `hello ${name.toUpperCase()}`
    return greeting;
}
pureOrNot('han');
Oefening 3.11.9: Pure or not (I)
function getHan() {
    return 'han';
}

function pureOrNot(nameGetter) {
    return `hello ${nameGetter()}`;
}
pureOrNot(getHan);
3.12

Summary

This chapter taught you how to write your own functions. The function keyword, when used as an expression, can create a function value. When used as a statement, it can be used to declare a binding and give it a function as its value. Arrow functions are yet another way to create functions.

// Define f to hold a function value
const f = function(a) {
  console.log(a + 2);
};

// Declare g to be a function
function g(a, b) {
  return a * b * 3.5;
}

// A less verbose function value
let h = a => a % 3;

A key aspect in understanding functions is understanding scopes. Each block creates a new scope. Parameters and bindings declared in a given scope are local and not visible from the outside. Bindings declared with var behave differently—they end up in the nearest function scope or the global scope.

Separating the tasks your program performs into different functions is helpful. You won’t have to repeat yourself as much, and functions can help organize a program by grouping code into pieces that do specific things.

Oefening 3.12.1: Execute Multiple Times

Plak onderstaande code in het antwoordveld en implementeer de definitie van executeNrOfTimes volgens de specificatie die erboven staat.

Je kunt de implementatie testen met de code eronder.

LET OP: de implementatie van executeNrOfTimes moet wel onafhankelijk blijven van de code waarmee je deze test. Dus je mag sayHello zelf niet in de definitie van executeNrOfTimes gebruiken.

/**
 * Exexutes the provided function the given number of times
 * @param {Number} nrTimes: the number of times the given function 
 *   is executed
 * @param {Function} func: the function that is executed
 */

const executeNrOfTimes = (nrTimes, func) => {
    // Maak hier je implementatie
}

//Testcode voor executeNrOfTimes
const sayHello = () => {
  console.log('hello');
}

executeNrOfTimes(5, sayHello);
De volgende, grijze tekst hoef je niet te lezen.
Lees verder waar de tekst weer zwart-op-wit wordt.
einde van tekst die overgeslagen kan worden