Web Platform Javascript

Functions

Why functions?

A function is a way of wrapping up some code as a reusable block. It's sort of like creating a little machine which you can put to work repeatedly, or with different inputs. This cuts down on the code you have to repeat - saving you writing and making your code easier to maintain.

For example, let's say you want to check if the x coordinate of a touch was within a range, before allowing something else to happen

if (touch.x > 50 && touch.x < 100) {
  // do something if x is greater than 50 and less than 100
}

You might have this line of code scattered all through your program, and use it in different ways:

// Check touch position
if (touch.x > 50 && touch.x < 100) {
  // do something if x is greater than 50 and less than 100
}  
...
if (!(touch.x > 50 && touch.x < 100)) {
  // x is outside of the normal range
}
...

But what happens if you then decide to change the range from 50-100? You would have to go through and update each line of code. Tedious.

One way of improving this to use variables to represent the range:

const xMin = 50;
const xMax = 100;
...

// Check touch position
if (touch.x > xMin && touch.x < xMax) {
  // do something if x is greater than 50 and less than 100
}  
...
if (!(touch.x > xMin && touch.x < xMax)) {
  // x is outside of the normal range
}

Now we only have to update xMin and xMax in one place, and all the lines of code we have for checking the range will not need to change. But what if the very logic of range checking changes? That would still mean all the lines of code would have to change.

Named and anonymous functions

When you find yourself repeating the same code - and worse, copy and pasting over and over again - it's a good sign you need a function.

In the below example, the logic of checking touch.x is centralised in a function assigned to the variable inRange, and we call it twice:

const inRange = function() {
  return (touch.x > 50 && touch.x < 100);
}
...
if (inRange()) {
  // do something if in range
}
...
if (!inRange()) {
  // do something if out of range
}

Now any changes to the logic only have to happen in one place. Note that the function has uses the return statement to "return" a value back to the caller of the function. You could read the function as being "inRange returns true if touch.x is within range or false if it is not". And likewise, when we go to use the function, you can read it as "If the return value of the function is true, then do something because it is within range".

Declaring functions like this is common practice, but you may also come across creating named functions, which look like this:

function inRange() {
  ...
}

There are some subtle differences with how they work, but we can skip over this for now.

Arrow functions

Another form of function you'll come across are arrow functions. These are useful because you can express a function with minimal writing, and this works a little differently.

// Expressing a function with no parameters
const bye = () => {
  console.log('Bye!');
}

// A function with two parameters
const sendFile = (fileName, destination) => {
  //...
  return true;
}

// Arrow functions are often used in functional
// style of programming. In this case, we don't use ()
// or even assign a name to the function 
const names =['alice', 'bob', 'cindy'];
const upperNames = names.map(name=>name.toUpperCase());

Providing data to functions

Good functions are self-contained and depend as little as possible on other parts of your code. This principle minimises the trouble you have to go through when making changes in your code.

Going back to the inRange example:

const inRange = function() {
  return (touch.x > 50 && touch.x < 100);
}

...inRange has a clear dependency: it assumes that touch.x is available from its parent scope. But what if we rename that variable, or if we want the function to also be able to work with mouse coordinates? This shows why we want to have functions that don't rely on anything other than gets passed in to it. In this case, we need to refactor it to take in values to check as arguments (also known as parameters). This is how data is provided to functions.

To define a function that takes arguments, simply provide a list of variable names to use, separated by a comma. Each name you put there effectively becomes a variable within the scope of your function.

In the below example, valueToCheck is used as the name for the first parameter:

const inRange = function(valueToCheck) {
  // valueToCheck is variable we can use because it's defined as part of the function
  return (valueToCheck > 50 && valueToCheck < 100); 
}

Now, to call inRange we have to pass it a value:

if (inRange(touch.x)) {
  // do something if in range
}

The benefit now is that we can also ask the function to check other values:

if (inRange(mouse.x) || inRange(touch.x)) {
  // do something if mouse or touch x is within range
}

Function parameters can also be used to modify its behaviour. Let's say that most of the time we want to use the same range within our programme, but sometimes a narrower range is needed. We could write a separate function, but since the logic is so similar, it's better to keep it within the function we have.

One possibility could be as follows, using a second parameter as true/false switch to change its behaviour:

const inRange = function(valueToCheck, useNarrowRange) {
  if (useNarrowRange) {
    return (valueToCheck > 90 && valueToCheck < 100);
  } else {
    return (valueToCheck > 50 && valueToCheck < 100);
  }
}

if (isRange(touch.x, true)) {
  // do something if touch.x is within the narrow range 
}
if (isRange(touch.x)) {
  // do something if touch.x is within the wude range 
}

Note the second parameter is omitted in the second use of isRange. When the function runs in this case, useNarrowRange will be undefined, which suits us well because that equates to false.

What about if we wanted even more flexibility? Rather than a simple wide/narrow range band, we wanted a multiplier. In this case the range values are calculated based on what the caller wants:

const inRange = function(valueToCheck, rangeMultiplier) {
  return (valueToCheck > 50*rangeMultiplier && valueToCheck < 100*rangeMultiplier);
}

if (isRange(touch.x, 5)) {
  // do something if touch.x is within a large range 
}
if (isRange(touch.x)) {
  // do something if touch.x is within the default range 
}

In this case, we run into problems if the caller does not pass a value. valueToCheck will be multiplied by an undefined value, and thus it too becomes undefined. Calling inRange without the second parameter will always be false. There are a few ways of solving this problem, the easiest is to define a default value in the function declaration:

const inRange = function(valueToCheck, rangeMultiplier = 1) {
  return (valueToCheck > 50*rangeMultiplier && valueToCheck < 100*rangeMultiplier);
}

Now when the function is called without the second parameter, a default of 1 is used.

Nested functions

You'll often see calls to functions nested within each other like algebra. This saves having to name lots of variables, when you don't really care about intermediate results, but want the result of one function to be the input of another function and so on. For example:

const addNumbers = function(x, y) {
  return x + y;
}
const printNumber = function(x) {
  console.log("The number is: " + x);
}
const doubleNumber = function(x) {
  return x * 2;
}
printNumber(doubleNumber(addNumbers(5, 2))); // Prints out: The number is: 14

Nested functions can be a little difficult to figure out, so we normally only write it that way when it's something trivial. Like algebra, the result from the inner most functions flow outward. The result of addNumber is passed to doubleNumber which then passes to printNumber.

Another pattern you'll see in Javascript is chaining, a so-called fluent style. This allows you to call a function based on the result of another function, without having to declare variables to stash results along the way. It can read very elegantly.

const array = [ 2, 100, 101, 500];
array.filter(v=>v > 100).forEach(v => console.log(`Value: ${v}`));

In this example, filter is called on the array, passing in an arrow function to define what we want to filter. In this case, numbers above 100. The result of filter is an array, which we call forEach of. This will call the arrow function for each item in the array. So the result is:

Value: 101
Value: 500