More Functions in Javascript

Last time, we looked at the basic uses of functions in Javascript, the different types of functions, and the basic differences between them. However, in order to debug issues in our (and other peoples') code, we need to understand some of the more advanced uses of functions.

Functions in Javascript are extremely flexible and powerful because they are first class objects. What this means is that we can treat functions like any other variable by passing them around our code, assigning them to variables, and doing many other things.

We're going to look at some more advanced and powerful uses of functions in this class.

Recursion

Functions have an interesting property in Javascript and most other languages: they can call themselves! We call this recursion.

function factorial(n) {
  if (n == 1) return 1
  return n * factorial(n-1)
}

// 5 x 4 x 3 x 2 x 1 = 120
console.log(factorial(5))

Notice that we need a way for our recursion to terminate, like when n is 1 in a factorial. This is called the base case.

Another thing worth noticing is that we could just as easily write this as an iterative (non-recursive) function:

function factorial(n){
  let total = n
  for (let i = n - 1; i > 1; i--){
    total = i * total
  }
  return total
}

console.log(factorial(5))

While it's a bit longer, we can usually rewrite recursive functions as regular functions.

Higher-Order Functions

Sometimes, we want to be able to use code that's mostly the same, but has a few changes. For example, let's say we want to find an item in an array with a particular value, or null if we couldn't find it. However, we don't yet know the exact value we want to find.

An obvious way to do this might be:

function findItem(list, target) {
  for (const el of list) {
    if (el == target) return el
  }
  return null
}

But there's a problem here. What if we want to find an array whose second element is some value? Suddenly our code has to change:

function findItem(list, target) {
  for (const el of list) {
    if (el == target[1]) return el
  }
  return null
}

Clearly this version won't work for numbers. What we really want is a way to use a function as a template. Luckily, higher-order functions provide a simple way to do this.

Instead of passing a target to the function, we can pass another function. This function will always return true if the element is the one we want, or false if it doesn't. This lets us call the same parent function and insert different pieces of code into it, like a template.

function findItem(list, condition) {
  for (const el of list) {
    if (condition(el)) return el
  }
  return null
}

// normally arrow functions are used for functions you want to pass around
// these are sometimes called "lambda functions" or "lambdas"
const findNumber = (el) => el == 5
const findSpecialArray = (el) => el[1] == 5

const mySimpleArray = [1,2,3,4,5]
const myNestedArray = [[0,1],[2,3],[4,5]]

let el1 = findItem(mySimpleArray,findNumber)
let el2 = findItem(myNestedArray,findSpecialArray)

console.log(el1)
console.log(el2)

In practice, most higher-order functions you want to write will fall into one of a few basic types, which are built into Javascript.

forEach acts like a for loop, and can be helpful for when you want to do things like print out each element in a list, or some attribute of an object.

let myList = [1,2,3,4,5]

myList.forEach((el) => console.log(el))

Map takes a list and converts it into another list (i.e. it maps the original values to new values). Whatever's returned by the child function will become the element in the same position in the new list.

const myList = [1,2,3,4,5,6]

const even = myList.map( (el) => (el % 2) == 0 )

console.log(even)

Filter takes a list of elements and returns a new list, returning only the elements which return true from the child function.

const myList = [1,2,3,4,5,6]

const odd = myList.filter( (el) => (el % 2) == 1 )

console.log(odd)

Reduce takes a list of elements and returns a single element. It does this by keeping an accumulator, which is set when you call reduce, and is the value returned by each function.

// return the initial value of the accumulator minus every element of the list
const myList = [1,2,3,4,5]

const minus100 = myList.reduce( (acc, el) => acc - el, 100 )

Exercise

  1. Write a factory function which takes a string as an argument and returns a function which, when called, prints that string.
  2. Using map, write a function which takes a list of numbers and returns each of them multiplied by two.
  3. Using filter, write a function which takes a list of numbers in the range 1-100, and returns a new list with all the numbers under 10 removed.
  4. Using reduce, write a function which takes a list of numbers and returns the mean average of that list.
  5. Write your own implementation of map or reduce. They must take a list of numbers and a child function as arguments, and return a new list (or in the case of reduce, a single output).

Extension: Write an implementation of whichever of map or reduce you didn't previously implement.

Scope

As we've worked through the assignments you will have noticed errors that look like this:

for (const i = 0; i < 10; i++) {
    let total = total + i
}
console.log(total) // undefined?!

What went wrong? Well, we didn't pay attention to the scope of our variables. Different variables have different scopes, depending on how they're declared.

let and const variables are scoped to the current block, which could be an if statement, a function block, or a loop - anything with {} curly braces. They'll exist for code that's in a child block relative to them, but not for code that's in a parent block.

function myFn() {
    if (1==1) {
        let myLet = 0
        if (2==2) {
            // all good
            console.log(myLet)
        }
        // great
        console.log(myLet)
    }
    console.log(myLet)
}

var variables are scoped to the current function, not the current block. This can cause some strange behaviour, which is why we rarely use var in modern Javascript.

function myFn() {
    if (1==1) {
        if (2==2) {
            // currently undefined
            console.log(myLet)
            var myLet = 10
        }
        // huh?
        console.log(myLet)
    }
    // and it still exists here
    console.log(myLet)
}

With our new knowledge of scope, we can easily fix our original code.

let total = 0
for (let i = 0; i < 10; i++) {
    total = total + i
}
console.log(total)

Closures

Sometimes we want to use a factory function for something a bit more complex than our previous examples.

function makeCounter () {
    let count = 0
    return function() {
        count++
        return count
    }
}

const count = makeCounter()
for (let i = 0; i < 5; i++) {
    console.log(count())
}

It works! But doesn't it violate the scope rules we discussed above? By the time we actually call count(), makeCounter() has already finished. So why isn't count undefined?

Javascript encapsulates the variables of an outer function like makeCounter when an inner function is created. The inner function has access to all the same variables that makeCounter does. This type of inner function, which uses variables from the outer function, is called a closure.

Closures can be extremely helpful on the web, because we're often working with external systems we don't know about until we run the program and load up our config files. Let's say we want to access a database, but we don't know if it's on our computer or a remote server. We can use closures to create functions which point to them.

function makeDbConnection(url) {
    return function(table) {
        console.log(`Getting ${table} from ${url}`)
    }
}

const getLocal = makeDbConnection("localhost:5000")
const getRemote = makeDbConnection("https://db.mysecureserver.com")

getLocal("users")
getRemote("users")

In this case, the inner functions getLocal and getRemote have access to the url parameter of makeDbConnection. This lets them make a call to the correct address while looking the same after they're created.

We can use this to create complex architectures, libraries and frameworks, but keep a user-friendly interface for other developers to work with.

Knowledge Check

Why do we prefer to use let over var in modern Javascript?

What does the following code output? What's the name for this structure?

function outerFunction() {
    let a = 10
    return () => console.log(a)
}

outerFunction()()

What does the following code output?

function myFunction(a) {
    if (a >= 10) {
        let output = true
    }
    return output
}

console.log(myFunction(20))

Assignment

The Sieve of Erastothenes is an ancient way to compute prime numbers between 1 and a given number, n. It's also a fairly simple algorithm:

  • Take all the numbers between 1 and n.
  • For every number between 1 and the square root of n which hasn't already been ruled out:
    • Count in multiples of that number until reaching the square root of n, and remove them from the final list.
  • Return the final list. ```

Using any of the techniques we've covered so far, implement the sieve of Erastothenes up to a given number, n, and return an array of all the prime numbers up to n.