Learn JavaScript: Novice to Ninja

Chapter 4: Functions

A function is a chunk of code that can be referenced by a name, and is almost like a small, self-contained mini program. Functions can help reduce repetition and make code easier to follow.

In this chapter, we’ll be covering these topics:

  • Defining functions―function declarations, function expressions, Function() constructors and the new arrow syntax
  • Invoking a function
  • Return values
  • Parameters and arguments
  • Hoisting―variables and functions
  • Callbacks―functions as a parameter
  • Project ― we’ll be using functions to make the Quiz Ninja code easier to follow

In JavaScript, functions are considered to be first-class objects. This means they behave in the same way as all the other primitive data types and objects in the language. They can be be assigned to variables, stored in arrays and can even be returned by another functions.

This makes functions a very important and powerful part of the JavaScript language with many of its features relying on them. Fully understanding functions is an essential skill of the JavaScript ninja.

Defining a Function

There are a number of ways to define a function in JavaScript. Three of the most common are covered below. ES6 introduced a new way to define functions, using what is known as ‘arrow’ notation. This is covered later in the chapter.

Function Declarations

To define a function literal we can use a function declaration:

function hello(){    console.log('Hello World!');}

This starts with the function keyword and is followed by the name of the function, which in this case is called ‘ hello ‘, followed by parentheses. Following this is a block that contains the code for the function.

This is known as a named function as the function has a name: ‘ hello ‘.

Function Expressions

Another way of defining a function literal is to create a function expression. This assigns ananonymous functionto a variable:

const goodbye = function(){    console.log('Goodbye World!');};

The function in this example is known as an anonymous function because it doesn’t have a name; it is simply created, then assigned to the variable goodbye . Alternatively, we can create a named function expression instead:

const goodbye = function bye(){    console.log('Goodbye World!');};

The name of this function is bye , and it has been assigned to the variable goodbye .

Notice also that the example ends with a semicolon. This finishes the assignment statement, whereas a normal function declaration ends in a block (there’s no need for semicolons at the end of blocks).

Every Function Has a Name

All functions have a read-only property called name , which can be accessed like so:

<< 'hello'

The name property is not actually part of the ECMAScript standard, although most JavaScript engines support it and use it internally.

Anonymous functions have an empty string as their name property in most browsers, although some versions of Internet Explorer use undefined .

The name property can be useful when debugging code, as the name of a function will be used to indicate which functions are causing a problem.

 Function() Constructors

A function can also be declared using the constructor Function() . The body of the function is entered as a string, as shown in this example:

const hi = new Function('console.log("Hi World!");');

It’s not recommended to declare functions in this way as there are numerous problems associated with placing the function body inside a string. Even in this simple example, we had to use different quotation marks for the console.log method, as those used for defining the function body itself. Functions created this way are also created in the global scope, regardless of where they are actually declared. This can lead to some strange and unexpected behavior.

A ninja programmer should always declare functions using function literals, function declarations or function expressions. These two ways of creating functions are similar, although there are some subtle differences that will be covered later in the chapter. Some people prefer function declarations as they are akin to how functions are declared in other languages. Others prefer function expressions because it is clear that functions are just another value assigned to a variable, rather than a special feature of the language. Whether you use function declarations or function expressions is often a matter of personal taste, but whatever you choose to do, be consistent!

You can read more about using function expressions or declarations inthis article

Invoking a Function

Invoking a function is to run the code inside the function’s body. To invoke a function, simply enter its name, followed by parentheses. This is how we’d invoke the hello function:

hello();<< 'Hello world!'

The function can be invoked over and over again just by typing its name followed by parentheses. This is one of the advantages of using functions ― there’s no need to write repetitive blocks of code. Another advantage is that all the functionality is kept in one place. So if you want to change part of it, you only need to update the one piece of code in the function declaration. This is known as the DRY principle, which stands for Don’t Repeat Yourself.

Keep Your Code DRY

Don’t Repeat Yourself,or DRY, is a principle of programming that specifies that every part of a program should only be written once. This avoids duplication and means there’s no need to keep multiple pieces of code up to date and in sync.

If you have assigned a function to a variable, you need to place parentheses after the variable to invoke it as a function:

goodbye();<< 'Goodbye World!'

Remember: you need parentheses to invoke a function ― either by name or by reference to the variable it is assigned to. If you skip the parentheses, you are simply referencing the function itself rather than invoking it, as you can see here:

goodbye;<< [Function: goodbye]

All that has been returned is the function definition that the variable goodbye is pointing to, rather than running the code. This can be useful if you want to assign the function to another variable, like so:

seeya = goodbye;<< [Function: goodbye]

Now the variable seeya also points to the function called bye and can be used to invoke it:

seeya();<< 'Goodbye World!'

Return Values

All functions return a value, which can be specified using the return statement, which comes after the return keyword. A function that doesn’t explicitly return anything (such as all the examples we have seen so far) will return undefined by default.

The function in this example will return the string ‘Howdy World!’:

function howdy(){    return 'Howdy World!';}

This means we can now assign a function invocation to a variable, and the value of that variable will be the return value of that function:

const message = howdy();<< 'Howdy World!'

The variable message now points to the return value of the howdy() function, which is the string ‘Howdy World!’. This may seem trivial in this instance (that is, why not just assign the variable to the string directly?), but we can create a more complex function that has different return values depending on certain conditions. This then allows us to assign different values to the message variable depending on those conditions.

Parameters and Arguments

Parametersandargumentsare terms that are often used interchangeably to represent values provided for the function as an input. There is a subtle difference though: any parameters a function needs are set when the function isdefined. When a function isinvoked, it is provided with arguments.

To see an example of a function that uses parameters, we’ll create a function that squares numbers. In the example that follows, the square function takes one parameter, x , which is the number to be squared. In the body of the function, the name of the parameter acts like a variable equal to the value that is provided when the function is invoked. As you can see, it is multiplied by itself and the result is returned by the function:

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

When we invoke this function, we need to provide an argument, which is the number to be squared:

square(4.5);<< 20.25

You can use as many parameters as you like when defining functions. For example, the following function finds the mean of any three numbers:

function mean(a,b,c){    return (a+b+c)/3;}
mean(1, 3, 6);<< 3.3333333333333335

Rounding Errors

You might have noticed that the answer to the last example was slightly incorrect (it should be just 3.3 recurring, with no 5 on the end). This highlights a problem when doing division in JavaScript (or calculations on any computer, for that matter). The problem stems from the fact that computers use base 2 in the background and therefore struggle to represent any fractions where the denominator is not a power of 2. This means that some division calculations can often have slight rounding errors. This usually doesn’t cause a problem, but you should be aware of it.

If a parameter is not provided as an argument when the function is invoked, the function will still be invoked, but the parameter will be given a value of undefined . If we try to invoke the mean function with only two arguments, we can see that it returns NaN because the function cannot do the required operation with undefined :

mean(1,2)<< NaN

If too many arguments are provided when a function is invoked, the function will work as normal and the extra arguments will be ignored (although they can be accessed using the arguments object that is discussed in the next section):

mean(1,2,3,4,5); // will only find the mean of 1,2 and 3<< 2

ES6 also made it possible to createdefault parametersthat will use default values for any parameter that isn’t provided as an argument. This is covered a little later in the chapter.

Variable Numbers of Arguments

As we have seen in the example above, it’s possible to enter as many arguments as you like into a function, even if only some of them are used. There are times when we don’t know how many arguments will be entered. For example, we could improve our mean() function by allowing a user to calculate the mean of any number of values, rather than restricting them to just 3.

Every function has a special variable called arguments . This is an array-like object that contains every argument passed to the function when it is invoked. We can create a simple function called arguments() that will return the arguments object so we can see what it contains:

function arguments(){    return arguments;}

Now let’s invoke the arguments() function a couple of times to see the results:

arguments('hello', NaN);<< { '0': 'hello', '1': NaN }arguments(1,2,3,4,5);<< { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5 }

As you can see, the arguments object that is returned contains every value that was entered. These can then be accessed using an index notation like we did with arrays, so the first argument would be accessed using arguments[0] .

The problem is that arguments is not an array. It has a length property and you can read and write each element using index notation, but it doesn’t have array methods such as slice()join() , and forEach() . There is a way of ‘borrowing’ methods from arrays, but it is clumsy and unnecessary.

A much better option is to use the rest operator. This was introduced in ES6 and can be used to deal with multiple arguments by creating an array of arguments that are available inside the body of the function.

To use the rest operator, simply place three dots in front of the last parameter in a function declaration. This will then collect all the arguments entered into an array. For example, the following function will have access to an array of all the arguments entered:

function rest(...args){    return args;}

The args parameter is an actual array, and has access to the same methods. For example we can use a for-of loop to iterate over each value given as an argument:

function rest(...args){    for(arg of args){        console.log(arg);    }}rest(2,4,6,8);<< 2468

Improved Mean Function

We can use a rest parameter to improve our mean() function so it accepts any number of values:

function mean(...values) {    let total = 0;    for(const value of values) {        total += value;    }    return total/values.length;}

This collects all the arguments that are entered into an array called values . We can then loop over this array and add up all the values. The variable total is used to keep the running total, and the += operator is used to add the next value onto the total. At the end of the function we return the total divide by the number of arguments entered, which we find by calling the length property on the values array. Let’s see if it works:

mean(2,8,13,11,4,2);<< 6.666666666666667

Default Parameters

ES6 introduced a convenient way to specify default parameters for a function. These are values that will be used by the function if no arguments are provided when it is invoked. To specify a default parameter, simply assign the default value to it in the function definition:

function hello(name='World') {    console.log(`Hello ${name}!);}

Now if we call this function without an argument, it will use ‘World’ as the name parameter:

hello();<< 'Hello World!'

We can override the default value, by specifying our own argument:

hello('Universe');<< 'Hello Universe!'

Prior to ES6

The way of assigning default values previous to ES6 was to use a line of code similar to the following in the body of the function:

name = name || "World";

This uses the logical OR operator to check if the name parameter has a truthy value. If it does, that means an argument was provided to the function and name will stay as that value. If there was no argument provided, the value of name will be undefined , which is falsy so it will take the value of ‘World’. This method is still used quite often, but it does have a pitfall in that it relies on undefined being falsy. Unfortunately this is not the only falsy value, however. If the name argument is meant to be a falsy value, such as 0 , for example, it won’t be used, and the value will still be set to the default value of ‘World’ instead. For this reason, you should stick to using the ES6 method of declaring default parameters whenever possible.

Default parameters should always come after non-default parameters, otherwise default values will always have to be entered anyway. Consider the following function for calculating a discounted price in a store:

function discount(price, amount=10) {    return price*(100-amount)/100;}

This function takes two arguments: the price of an item and the percentage discount to be applied. The store’s most common discount is 10%, so this is provided as a default value. This means that the amount argument can be omitted in most cases and a 10% discount will still be applied:

discount(20) // standard discount of 10%<< 18

If a different discount is applied, the amount argument can be provided:

discount(15, 20) // discount of 20%<< 12

This will fail to work, however, if the parameters are reversed:

function discount(amount=10, price) {    return price*(100-amount)/100;}

Now if we try to use the function with just one argument, the function won’t work, because price has not been set:

discount(20); // this sets amount = 20, but doesn't provide a value for price<< NaN

It will work, however, if both values are entered:

discount(10,20);<< 18

This somewhat defeats the object of having default parameters! The golden rule to remember here is to always put default parametersafterall the other parameters.

Arrow Functions

ES6 introduced a new syntax for declaring functions called thearrowsyntax. These make declaring functions much more succinct by using less verbose syntax.

Arrow functions can be identified by the ‘arrow’ symbol, => that gives them their name. The parameters come before the arrow and the main body of the function comes after. Arrow functions are always anonymous, so if you want to refer to them, you must assign them to a variable. For example, the square function we wrote earlier can be written like so:

const square = x => x*x;

Arrow functions have a number of advantages over other ways of declaring functions:

  • They are much less verbose than normal function declarations.
  • Single parameters don’t need putting into parentheses.
  • The body of the function doesn’t need placing inside a block if it’s only one line.
  • The return keyword isn’t required if the return statement is the only statement in the body of the function.
  • They don’t bind their own value of this to the function (we’ll see why this is a particularly useful property when we cover objects later in the book).

In the square example above parameter, x didn’t need to go in parentheses because it’s the only parameter. Multiple parameters need to go inside parentheses, for example, the following function adds two numbers together:

const add = (x,y) => x + y;

If the function doesn’t require any parameters, a pair of empty parentheses must go before the arrow:

const hello = () => alert('Hello World!');

In all the examples, the main body of the function fits onto one line, so there is no need to put it inside a block or explicitly use the return keyword.

Longer functions will still require curly braces to deliminate the body of the function and the return keyword at the end, as can be seen in this (rather simplistic) tax-calculating function:

const tax = (salary) => {    const taxable = salary - 8000;    const lowerRate = 0.25 * taxable;    taxable = taxable - 20000;    const higherRate = 0.4 * taxable;    return lowerRate + higherRate;}

As you can see, a number of the benefits are lost, once the function body becomes longer than one line.

Arrow functions make perfect candidates for short, anonymous functions, and you will often see them used later in the book.

Function Hoisting

Hoisting is the JavaScript interpreter’s action of moving all variable and function declarations to the top of the current scope, regardless of where they are defined.

Functions that are defined using a function declaration are automatically hoisted, meaning they can be invoked before they have been defined. For example, in the following code the function hoist() can be invoked before it is actually defined:

// function is invoked at the start of the code            hoist();// ...// ... lots more code here// ...// function definition is at the end of the codefunction hoist(){    console.log('Hoist Me!');}

This can be quite useful as it means that all function definitions can be placed together, possibly at the end of a program, rather than having to define every function before it is used.

Variable Hoisting

Variable declarations that use the var keyword are automatically moved to the top of the current scope. Variable assignment is not hoisted, however. This means that a variable assigned at the end of a function will have a value of undefined until the assignment is made:

console.log(name); // will return undefined before assignment// variable is defined herevar name = 'Alexa';console.log(name); // will return 'Alexa' after assignment

Variable hoisting can cause quite a bit of confusion and also relies on using var to declare variables. An error will be thrown if you attempt to refer to a variable before it has been declared using const and let . It’s better practice to use const and let to declare any variables at the beginning of a block so hoisting is unnecessary.

A function expression (where an anonymous function is assigned to a variable) is hoisted in a similar way to variables. So if it is declared using var then the declaration will be hoisted, but not the actual function. This means the function cannot be invoked until after it appears in the code:

// the variable helloExpression has a value of undefined, so the function cannot be invokedhelloExpression(); // throws an error// the function declaration can be invoked before it is declaredhelloDeclaration(); // returns 'hello'// assign function expression to a variablevar helloExpression = function() {     console.log('hello') }// declare function declarationfunction helloDeclaration() {    console.log('hello') }// The function expression can only be invoked after assignmenthelloExpression(); // returns 'hello'

This is the major difference between the two ways of defining function literals and it may influence your decision regarding which one to use. Some people like that using function expressions means you’re required to define all functions and assign them to variables prior to using them.

Hoisting can be a tricky concept to get your head around initially – you can read more about it in thisexcellent SitePoint guide.

Callbacks

Remember at the start of this chapter when we said that functions in JavaScript are first-class objects, so they behave in just the same way as every other object? This means that functions can also be given as a parameter to another function. A function that is passed as an argument to another is known as acallback.

Here’s a basic example of a function called sing() , which accepts the name of a song:

function sing(song) {    console.log(`I'm singing along to ${song}`);}sing('Let It Go')<< 'I'm singing along to Let It Go'

We can make the sing() function more flexible by adding a callback parameter:

function sing(song,callback) {    console.log(`I'm singing along to ${song}.`);    callback();}

The callback is provided as a parameter, then invoked inside the body of the function.

But What if the Function Isn’t Provided as an Agrument?

There is nothing to actually define a parameter as a callback, so if a function isn’t provided as an argument, then this code won’t work. It is possible to check if an argument is a function using the following code:

if(typeof(callback) === 'function'){    callback();}

This will only attempt to invoke the callback if it is a function.

Now we can create another function called dance() that can be used as the callback:

function dance() {    console.log("I'm moving my body to the groove."); ( We’re just logging a simple message to the console in these examples, but these functions could be used to do anything in a practical sense.)}

Now we can call our sing function, but we can also dance as well as sing:

sing('Let It Go',dance);<< 'I'm singing along to Let It Go.''I'm moving my body to the groove.'

Note that the callback dance is passed as an argument without parentheses. This is because the argument is only a reference to the function. The actual callback is invoked in the body of the function, where parentheses are used.

Okay, so in these examples, the dance() function doesn’t really do anything, except log another message to the console, but hopefully it shows you could do something very different with the sing() function depending on the callback function that is provided as an argument, making it a much more flexible function.

A function can also take an anonymous function as a callback. For example, say we want to call the sing() function and also want to stand on our head while singing, but we have no standOnHead() function. We can write an anonymous function that does it instead:

sing('Let It Go',()=>{ console.log("I'm standing on my head.");});<< 'I'm singing along to Let It Go.''I'm standing on my head.'

This is only really useful for one-off tasks. It’s often a better idea to keep functions separate and named, so they can be reused. It’s also a bad idea to use this method for long function definitions as it can be confusing where the callback starts and ends. Named functions also make it easier to identify the source of bugs in code. In this case, the fact we only needed a one-line anonymous function made it a good candidate for using the arrow notation.

Callbacks are used extensively in many JavaScript functions and we’ll see much more of them later in the book. In fact, here’s a practical example that solves a problem we encountered in the last chapter:

Sorting Arrays With A Callback

In the last chapter we saw that arrays have a sort() method that sorted the items in the array into alphabetical order. This is fine for strings, but you might recall that it didn’t work so well for numbers:

> [1,3,12,5,23,18,7].sort();<< [1, 12, 18, 23, 3, 5, 7]

The reason for this is that the numbers are converted into strings and then placed in alphabetical order.

So how do you sort an array of numerical values? The answer is to provide a callback function to the sort() method that tells it how to compare two values, a and b . The callback function should return the following:

  • A negative value if a comes before b
  •  0 if a and b are equal
  • A positive value if a comes after b

Here’s an example of a numerically function that can be used as a callback to sort numbers:

function numerically(a,b){    return a-b;}

This simply subtracts the two numbers that are being compared, giving a result that is either negative (if b is bigger than a ), zero (if a and b are the same value), or positive (if a is bigger than b ).

This function can now be used as a callback in the sort() method to sort the array of numbers correctly:

> [1,3,12,5,23,18,7].sort(numerically);<< [1, 3, 5, 7, 12, 18, 23]

Much better!

Overflow Errors

In some rare instances where an array includes some very large and negative numbers, an overflow error can occur and the result of a-b becomes smaller than the smallest number that JavaScript is able to cope with. If this is the case, the following function can be used as a callback instead:

function numerically (a,b) {    if (a < b) {        return -1;    } else if (a> b) {        return 1;    } else {        return 0;    }}

Array Iterators

Arrays have a number of methods that utilize callbacks to make them more flexible.

Use of Arrow Functions

You’ll notice that arrow functions are frequently used to declare the callbacks in these examples. This because they are short functions, often only taking up one line, making them a good candidate for using the arrow notation.

 forEach()

In the last chapter, we saw that a for loop could be used to loop through each value in an array like so:

const colors = ['Red', 'Green', 'Blue']for(let i = 0, max = colors.length ; i < max ; i++ ) {    console.log(`Color at position ${i} is ${colors[i]}`);}<<  'Color at position 0 is Red'    'Color at position 1 is Green'    'Color at position 2 is Blue'

An alternative is to use the forEach() method. This will loop through the array and invoke a callback function using each value as an argument. The callback function takes three parameters, the first represents the value in the array, the second represents the current index and the third represent the array that the callback is being called on. The example above could be written as:

colors.forEach( (color,index) =>    console.log(`Color at position ${index}  is ${color}`) );<<  "Color at position 0 is Red"    "Color at position 1 is Green"    "Color at position 2 is Blue"

 map()

The map() method is very similar to the forEach() method. It also iterates over an array, and takes a callback function as a parameter that is invoked on each item in the array. This is often used to process data returned from databases in array form, such as adding HTML tags to plain text. The difference is that it returns a new array that replaces each value with the return value of the callback function. For example, we can square every number in an array using the square function we wrote previously as a callback to the map() method:

[1,2,3].map( square )<< [1, 4, 9]

An anonymous function can also be used as a callback. This example will double all the numbers in the array:

[1,2,3].map( x => 2 * x);<< [2,4,6]

The next example takes each item in the array and places them in uppercase inside paragraph HTML tags:

['red','green','blue'].map( color => `<p> ${color.toUpperCase()}</p>` );<< ['<p>RED</p>', '<p>GREEN</p>', '<p>BLUE</p>']

Notice in this and the previous example, the anonymous function takes a parameter, color , which refers to the item in the array. This callback can also take two more parameters ― the second parameter refers to the index number in the array and the third refers to the array itself. It’s quite common for callbacks to only used the first, index, parameter, but the next example shows all three parameters being used:

['red','green','blue'].map( (color, index, array) => `Element ${index} is ${color}. There are ${array.length} items in total.` );<< [ 'Element 0 is red. There are 3 items in total.','Element 1 is green. There are 3 items in total.','Element 2 is blue. There are 3 items in total.' ]

 Reduce()

The reduce() method is another method that iterates over each value in the array, but this time it cumulatively combines each result to return just a single value. The callback function is used to describe how to combine each value of the array with the running total. This is often used to calculate statistics such as averages from data stored in an array. It usually takes two parameters. The first parameter represents the accumulated value of all the calculations so far, and the second parameter represents the current value in the array. The following example shows how to sum an array of numbers:

[1,2,3,4,5].reduce( (acc,val) => prev + val );<< 15

In the example above, value of acc starts as 1 (the first item in the array) then keeps track of the accumulated total. The next item in the array is then added to this running total, and so on, until every item in the array has been added together and the result is returned.

The reduce() method also takes a second parameter after the callback, which is the initial value of the accumulator, acc . For example, we could total the numbers in an array, but starting at 10 , instead of zero:

[1,2,3,4,5].reduce( (acc,val) => acc + val,10); // <---- second parameter of 10 here<< 25

Another example could be to calculate the average word length in a sentence:

const sentence = 'The quick brown fox jumped over the lazy dog'<< 'The quick brown fox jumped over the lazy dog'

The sentence can be converted into an array using the split() method:

<< ['The', 'quick', 'brown', 'fox', 'jumped', 'over', 'the', 'lazy', 'dog']

Now we can use the reduce() function to calculate the total number of letters in the sentence, by starting the count at 0 and adding on the length of each word in each step:

const total = words.reduce( (acc,word) => acc + word.length,0 );<< 36

And a simple division sum tells us the average word length:

const average = total/words.length;<< 4

 Filter()

The filter() method returns a new array that only contains items from the original array that return true when passed to the callback. For example, we can filter an array of numbers to just the even numbers using the following code:

const numbers = [ 2, 7, 6, 5, 11, 23, 12 ]numbers.filter(x => x%2 === 0 ); // this returns true if the number is even<< [ 2, 6, 12 ]

The filter() method provides a useful way of finding all the truthy values from an array:

const array = [ 0, 1, '0', false, true, 'hello' ];array.filter(Boolean);<< [ 1, '0', true, 'hello' ]

This uses the fact that the Boolean() function will return the boolean representation of a value, so only truthy values will return true and be returned by the filter() method.

To find all the falsy values, the following filter can be used:

array.filter(x => !x);[ 0, false ]

This uses the not operator, ! to return the compliment of a value’s boolean representation. This means that any falsy values will return true and be returned by the filter.

There are other array methods that use callbacks that are worth investigating such as reduceRight()every()find() and some() . More information about them can be found at theMozilla Developer Network.

Chaining Iterators Together

The various iterator functions can be used in combination to create some powerful transformations of data stored in arrays. This is achieved by a process calledchainingmethods together.

Chaining works because the iterator functions return an array, which means that another iterator function can then be chained on to the end and it will be applied to the new array.

For example, we can calculate the sum of square numbers using the map() method to square each number in the array and then chain the reduce() method on the end to add the results together:

[1,2,3].map( x => x*x ).reduce((acc,x) => acc + x );<< 14

Another more complex example could be used to take an array of orders, apply a sales tax to them using map() and then use reduce() to find the total:

const sales = [ 100, 230, 55];totalAfterTaxSales = sales.map( (amount) => amount * 1.15 ).reduce( (acc,val) => acc + val );<< 442.75

There are some good examples of chaining iterators together inthis SitePoint article.

Improving the mean() Function

Earlier in the chapter we created a mean() function that would calculate the mean of any number of arguments. We can improve on this by using the reduce() method to add up all the values provided:

function mean(array) {    const total = array.reduce((a, b) => a + b);    return total/array.length;}

Our next improvement will be to add a callback as the last parameter that specifies a function to be applied to all the numbers before the mean is calculated. This will allow us to work out things such as the mean of all numbers if they were doubled or squared.

Here is the code for the improved function that accepts a callback:

function mean(array,callback) {    if (callback) {    array.map( callback );    }     const total = array.reduce((a, b) => a + b);    return total/array.length;}

This code is similar to our previous mean() function, except in the following if block where we check to see if a callback has been provided. If it has, then the callback is applied to each value before being added to the total; otherwise, the total is calculated using just the values from the array given as the first argument:

Let’s have a go at using it:

mean([2,5,7,11,4]); // this should just calculate the mean<< 5.8

Now let’s use an anonymous arrow function to double all the numbers before calculating the mean:

mean([2,5,7,11,4],x => 2*x);<< 11.6

This is the equivalent of calculating the mean of 2*2, 2*5, 2*7, 2*11, and 2*4 .

Last of all, let’s use the square function we wrote earlier in this chapter as a callback to square all the numbers before calculating the mean:

mean([2,5,7,11,4],square);<< 43

This is the equivalent of calculating the mean of 2^2, 5^2, 7^2, 11^2, and 4^2 .

Hopefully, these examples help to show how using callbacks can make functions more powerful and flexible.

Quiz Ninja Project

Now we have a good understanding of functions, we’re going to have a go at refactoring the code for our Quiz Ninja project so it uses functions to describe the main parts of the program. Refactoring is the process of improving the code’s structure and maintainability without changing its behavior.

What we’re going to do is replace some of the chunks of code with functions. This will make the code easier to follow and maintain because if we want to make a change to the functionality, all we need to do is change the code inside the relevant function.

Open up the main.js file and replace all the code with the following:

const quiz = [    ["What is Superman's real name?","Clark Kent"],    ["What is Wonder Woman's real name?","Diana Prince"],    ["What is Batman's real name?","Bruce Wayne"]];function start(quiz){    let score = 0;    // main game loop    for(const [question,answer] of quiz){        const response = ask(question);        check(response,answer);    }    // end of main game loop    gameOver();    // function declarations    function ask(question){        return prompt(question);    }    function check(response,answer){        if(response === answer){        alert('Correct!');        score++;        } else {        alert(`Wrong! The correct answer was ${answer}`);        }    }    function gameOver(){        alert(`Game Over, you scored ${score} point${score !== 1 ? 's' : ''}`);    }}start(quiz);

The first part of this code remains the same ― we create a map of questions and answers and store it in the quiz variable.

Next we create a function called start() . This is the main game function that contains all the steps of playing the game. This function also contains a number of functions that help to describe how the game runs. It starts by initializing a variable called score to 0.

After this, it iterates over the quiz array and invokes the ask() function for each question. We then, invoke the check() function to check if the player’s response is correct. After we have looped through every question in the quiz array, the game is over, so the gameOver() function is invoked.

This shows how code can be simplified by abstracting it into separate functions that are descriptively named. Another benefit is that it allows us to change the content of the functions at a later time. If we decide that the way to check a question will change, for example, all we need to do is edit the check() function.

The ask()check() and gameOver() functions are defined at the end of the body of the start() function. They need to be placed inside the start() function as nested functions, as this gives them access to any variables defined inside the start() function’s scope. Because they are defined using function declarations, they are hoisted, so they can be definedafterthey are invoked.

The ask() function accepts a question parameter. This combination of function name and parameter name is used to make the code very descriptive ― it reads almost like an English sentence: ‘Ask the question’. It uses a prompt dialog and returns the text entered by the player, which is then saved in a variable called answer .

The check() function is written after the ask() function and has two parameters: response and answer . This combination of function name and parameter name again make the code read more like an English sentence. Naming functions in this way means we don’t need to use comments to explain what the code does ― it’s self-explanatory.

This function uses the same logic we used in the last chapter to check if the answer entered by the player is the same as the answer stored in the map. If it is, then we increase the score by 1 and if it isn’t, we show an alert dialog to tell them what the answer should have been.

When all the questions have been asked, and all the answers have been checked, the loop terminates and the gameOver() function is invoked. This uses an alert dialog to give some feedback about how many questions were answered correctly, using the same code that we used in the previous chapter.

There is an important line at the end of the file:

start(quiz);

This invokes the start() function with the quiz variable passed to it as an argument. This is required to actually start the quiz!

Once you’ve made these changes, have a go at playing the quiz by opening the index.html file in your browser.

While you play, you might notice there’s been no change to the functionality of the quiz. This is an example ofrefactoringcode ― the functionality of the application remains the same, but the underlying code has become more flexible and easier to maintain, as well as being more readable and descriptive due to the use of functions. We have abstracted much of the internal game logic out into separate functions, which means we can change the mechanics of different aspects of the quiz by updating the relevant functions.

Quiz Ninja

You can see a live example onCodePen.

I hope this helps to demonstrate how descriptively named functions can make your code more flexible, maintainable, reusable and easier to read.

Chapter Summary

  • Functions are first-class objects that behave the same way as other values.
  • Function literals can be defined using the function declaration, or by creating afunction expressionby assigning an anonymous function to a variable.
  • All functions return a value. If this is not explicitly stated, the function will return undefined.
  • A parameter is a value that is written in the parentheses of a function declaration and can be used like a variable inside the function’s body.
  • An argument is a value that is provided to a function when it is invoked.
  • The arguments variable is an array-like object that allows access to each argument provided to the function using index notation.
  • The rest operator can be used to access multiple arguments as an array.
  • Default arguments can be supplied to a function by assigning them to the parameters.
  • Arrow functions are a new shorthand notation that can used for writing anonymous functions in ES6.
  • Function declarations can be invoked before they are defined because they are hoisted to the top of the scope, but function expressions cannot be invoked until after they are defined.
  • Acallbackis a function that is provided as an argument to another function.

Everything that isn’t a primitive data type in JavaScript is an object ― which is the topic of our next chapter.

Pages: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16