Functions in Javascript

So far, we've covered the basics of writing programs in Javascript, many of the different methods we can use for manipulating data, and ways to manage control flow with conditional statements and loops.

However, some of our programs are a little too... long. One of the reasons for this is that we don't have an easy way to deal with complicated conditional logic.

Here's an example which isn't too far away from code that I wrote as a beginner:

let input = ""
while(input != "exit") {
  console.log("Press 1 for a menu. Or, write exit to exit.")
  input = prompt()

  if (input == "1") {
    console.log("Press 1 for the next menu. Or write something else to go back.")
    input = prompt()
    if (input == "1") {
      console.log("Congratulations, you made it to the inner menu!")
    }
  }
}

We only ask the player for 2 inputs, and already our program is looking extremely messy.

Functions give us a way to deal with this complexity and give our code a simpler and more orderly structure. They are also one of the main ways for us to interact with HTML and other UI code. By attaching functions to different elements of our webpages we can create all the interactions that we expect from a webpage.

There are three main ways to create a function in Javascript. The first is to directly declare a named function. This is commonly seen in older Javascript, but works perfectly well.

function myFunction() {
  console.log("hello world!")
}

Named functions are hoisted by Javascript interpreters. This means that we can use them by name, even before they are reached in the code. However, they can be changed:

function myFunction() {
  console.log("hello world!")
}

myFunction()

myFunction = function() {
  console.log("nani?!")
}

myFunction()

The property of being able to be changed can lead to subtle bugs when working in complex codebases, and so modern Javascript often prefers to use anonymous functions which are tied to const variables.

myFunction()

const myFunction = function () {
  console.log("What? You're approaching me?")
}
myFunction()

myFunction = function () {
  console.log("Yes.")
}

myFunction()
// error! this is what we wanted to happen

The final type of function that we see a lot in modern Javascript is the arrow function. We'll get onto how exactly it's different to anonymous functions later, but for now we just need to understand its syntax. Arrow functions are very amenable to writing one-liners.

const myFunction = () => { 
  console.log("Hello") 
}

myFunction()

const another = () => console.log("hello")

another()

Notice that both of these are pretty much identical to our anonymous functions above.

Input and Outputs

The main feature of functions is that they take zero or more inputs and yield a single output (although the output can be a list).

Most operations in Javascript can be thought of as functions. Here's an example:

const add = function (a,b) {
  return a + b
}

// console.log is also a function!
let result = add(10,5)
console.log(result)

This function takes two inputs, or arguments, called a and b. It adds them together and returns the result by using the special keyword return.

Arrow functions are special, and don't always need to use return:

// if you leave out the curly braces, the expression you write is the return value
const add = (a,b) => a + b

let result = add(10,5)
console.log(result)

If an argument isn't given to a function, then by default it's undefined. This is often not what we want.

We can set up a function so that it has default values for one or more inputs. These inputs must be the final inputs in the function declaration.

const isBig = function(number, bigness=1000) {
  return number > bigness
}

console.log(isBig(10))
// change the value of bigness
console.log(isBig(10, 5))

Exercises

  1. Write a function which takes a number and multiplies it by five.
  2. Write a function which sums every number in a list and returns the total as its output. Run this across several different lists to check it works as intended.
  3. Write a function that takes a list of numbers, multiplies each by two, and returns the list.
  4. Write a function that takes a list of numbers and two parameters, min and max. Create a new list without the numbers that are less than min or more than max and return this list.
  5. Write a slice function which takes a string and two parameters, start and end. It should return the substring between indexes start and end, not including end. By default the whole string should be returned.
  6. Extend the slice function to take an additional parameter, step. This should change the amount the function increments. For example, for string="hello", start=0, end=string.length and step=2, the function should return hlo.

Rewriting Conditional Logic Using Functions

Let's try rewriting my messy code from earlier. If you're confident try doing this before checking the answer below!

let input = ""
while(input != "exit") {
  console.log("Press 1 for a menu. Or, write exit to exit.")
  input = prompt()

  if (input == "1") {
    console.log("Press 1 for the next menu. Or write something else to go back.")
    input = prompt()
    if (input == "1") {
      console.log("Congratulations, you made it!")
    }
  }
}

As it turns out, we can substitute a function cleanly at each level of conditional logic.

const innerMenu = function() {
  let input = prompt()
  if (input == "1") {
    console.log("Congratulations, you made it!")
  }
}

const topMenu = function() {
  let input = prompt()
  if (input == "1") {
    innerMenu()
  }
  return input
}

const loop = function () {
  let input = ""
  while (input != "exit") {
    input = topMenu()
  }
}

loop()

One useful property of this is that we've separated the logic for the inner functions from the logic of the outer function, which is helpful for testing our code. Try running innerMenu() or topMenu() and they should run smoothly (but not loop).

A very helpful application of this is when writing complicated logic that looks like this:

function doSomething () {
  if (condition1) {
    if (condition2) {
      if (condition3) {
        // do something
      }
    }
  }
}

As these are all within a function, we can reverse the logic and write:

function doSomething() {
  if (!condition1) return
  if (!condition2) return
  if (!condition3) return
  // do something
}

This is known as a guard clause and is frequently used in professional code to maintain good structure and readability.

Review Questions

  1. Why do we prefer anonymous functions over named functions in modern Javascript?
  2. What's a special feature of named functions?
  3. Do arrow functions need a return statement to yield output?
  4. Do named functions need a return statement to yield output?
  5. What's the default value of a parameter if no argument is supplied in Javascript?
  6. Give me an example of a guard clause in a function.

Assignment

Write a calculator that takes a string like "1 + 1" with exactly 3 parts, separated by a space, and calculates the result. It should keep the total that they previously entered and allow them to enter subsequent operations later by using the value m, e.g. "m + 10". Write a function for each method: +-*/ as well as a function to split the string into parts and decide what the best course of action is.

Extension: Allow the calculator to take a single number as input. This calculator should be the new total.

Extension: Write other operators for advanced operations such as ^ (to the power of), root (to the root of), or % (modulus).

This is a very basic example of a 'parser' which is used to translate code into instructions that a computer can follow (e.g. to make HTML appear as interactive elements in your browser).

Answers to Review

  1. Anonymous functions can be assigned to const variables and protected, while named functions can't.
  2. They are hoisted and can be called before they're declared.
  3. Yes, they do.
  4. No, they don't.
  5. undefined
  6. if (!condition) return