Lesson 5: Functions

Functions in JavaScript encapsulate functionality. We've already how functions work in some of the built-in objects in JavaScript, like a method within a String:

        let myString = 'brian'
        myString.toUpperCase() // => 'BRIAN'
      

In this example, all of the logic that goes into turning a String into all upper-case letters is encapsulated into the toUpperCase() function. Code that uses this function doesn't need to know anything about its actual implementation, only that using it does the job it's expected to do, and returns its expected result. We've seen this already with the built-in functions we've used.

Before we get to how to create our own functions, let's first discuss why we'd want to. Organizing our code into functions makes our code more reusable, readable, and far more maintainable than code written in a linear fashion, especially as our applications become more complex.

Practically speaking, a function is a like a little mini-program within our application. It usually takes input and usually produces output. Emphasis on usually, because there are certainly cases where a function takes no input and/or produces no output. We'll get to that later.

        function yell(words) {
          return `${words.toUpperCase()}!!!!!`
        } 
      
        yell('tacos') // => 'TACOS!!!!!'
      

Let's break this down:

In short, we've taken the input of the function, performed some processing/transformation/action, and returned an output to the code that's using the function. Let's go deeper:

        function yell(words) {
          return `${words.toUpperCase()}!!!!!`
        } 

        function makeFullName(firstName, lastName) {
          return `${firstName} ${lastName}`
        }
      
        console.log(yell(makeFullName('steph', 'curry'))) // => writes 'STEPH CURRY!!!!!' to the console
        console.log(yell(makeFullName('james', 'harden'))) // => writes 'JAMES HARDEN!!!!!' to the console
      

Now we're using two of our own functions, and one other function that's built-in to JavaScript. And we're calling them all by chaining them together – that is, we're using the return value of makeFullName as the input parameter of yell, and using the return value of yell as the input parameter of console.log.

To illustrate the need for functions, let's consider how we might write the above example without using functions:

        let firstName = 'steph'
        let lastName = 'curry'
        
        let fullName = `${firstName} ${lastName}`
        console.log(`${fullName.toUpperCase()}!!!!!`)

        firstName = 'james'
        lastName = 'harden'
        
        fullName = `${firstName} ${lastName}`
        console.log(`${fullName.toUpperCase()}!!!!!`)
      

We can immediately see that very clear problems have emerged:

  1. Repetition – we've repeated the same logic twice. This is error prone, and will be hard to understand as it grows over time. If we wanted to change the logic, even just slightly, we'd have to remember to change it in both places.
  2. Scalability – if we wanted to process a list of 10, or maybe even 100 different names, using this approach would be next to impossible.
  3. Organization – this code is just plain ugly, and reads more like code than it does like English. Ultimately, we want our code to read like a story, and not like code. By using well-named functions like yell and makeFullName, we've made the intention of our code much more clear to someone reading it.

Declaring Functions

So far, we've seen functions simply declared in the body of our code, using the function keyword. There are couple of other ways that functions can be declared in our code, each serving their own purpose.

First, we can hold the value of a function in a variable, for later use in our code.

        let yell = function(words) {
          return `${words.toUpperCase()}!!!!!`
        }
      
        yell('tacos') // => TACOS!!!!!
      

This is essentially the same as declaring the function as before, using the function keyword. So what's the difference? This is where things get crazy, so hold on tight...

        let yell = function(words) {
          return `${words.toUpperCase()}!!!!!`
        }
        
        let whisper = function(words) {
          return `shhhh... ${words.toLowerCase()}`
        }
        
        let transformPlayerName = function(firstName, lastName, transformation) {
          let fullName = `${firstName} ${lastName}`
          return transformation(fullName)
        }
        
        transformPlayerName('steph', 'curry', yell)
        transformPlayerName('JAMES', 'HARDEN', whisper)

There's a lot going on here, so let's break it down:

  1. We're defining three different functions – one named yell, another named whisper, and a third named transformPlayerName.
  2. yell and whisper are straightforward; they both take an input (parameter) of words, perform a transformation of the input, and return back a new String.
  3. transformPlayerName has some magic going on. It takes three parameters – firstName and lastName are self-explanatory – the third parameter, transformation, is a function. This function gets used in the implementation – first, we concantenate the first name and last name, then we use that String as an argument to whatever function was passed in as input – it can be either yell or whisper, in this case.

Passing a function as one of the input parameters allows us to dynamically change the function's behavior based on which function is passed in. What's more, we can use a previously defined function, like we've done, or we can pass a function we define on the fly. These functions don't have names (like ones we declare using the function keyword or ones assigned to variable names do), so they're known as anonymous functions.

Let's look at a practical use-case using the example we've already built. Suppose we have the transformPlayerName function, but we don't want to use yell or whisper as the transformation. Instead, we want to define our own, custom transformation. Anonymous functions to the rescue:

        let transformPlayerName = function(firstName, lastName, transformation) {
          let fullName = `${firstName} ${lastName}`
          return transformation(fullName)
        }
        
        transformPlayerName('candy', 'man', function(words) {
          return `${words} ${words} ${words}`
        }) // => 'candy man candy man candy man'
      

🤯 The Payoff

This turns out to be a pretty commonly used pattern in JavaScript, especially when dealing with the web browser. We'll be looking at this quite a bit in the next unit of the course, but here's a quick intro to how we can apply these concepts to programming for the web.

Client-side web development is mostly centered around this basic workflow:

  1. Wait for an event to happen, such as the page being loaded or some action/input from the end-user.
  2. Determine what to do based on that event.
  3. Change what the end-user is seeing on-screen, by manipulating the HTML of the page.
  4. Repeat!

Let's look at some example code, and dissect it further from there:

        window.addEventListener('DOMContentLoaded', function() {
          for (let i = 0; i < 5; i++) { 
            let outputElement = document.querySelector('.output')
            outputElement.insertAdjacentHTML('beforeend', ` 
              <div class="text-3xl my-8">This is the way.</div>
            `)
          }
        })
      

This code looks daunting at first, especially if we've never seen anything like it before. But it's really a matter of following the pattern as described above, in code:

  1. Wait for an event to happenwindow.addEventListener is a built-in function in JavaScript. It takes two parameters – the event to listen for, and a function. In this case, the event we're waiting for is DOMContentLoaded – simply JavaScript-speak for "when the page is loaded". The second parameter is a function describing what to do when the event happens.
  2. Determine what to do – we've decided that we're going to write "This is the way." in big, bold letters, 5 times.
  3. Manipulate the HTML – also known as "DOM manipulation" – what we're doing here is:
    1. Find an existing HTML element on the page to manipulate. In this case, we've chosen an existing HTML element with the class of output. Grab a reference to that element and store it in a variable, outputElement.
    2. insertAdjacentHTML is a built-in method that's used to insert HTML around or inside the element in question. This function takes two parameters as input – first, a position indicating where we'd like to insert our HTML; we almost always use beforeend, i.e. inside the element, right before the closing tag. And the second parameter is a String of HTML content. We can use template literals (i.e. the backticks) to make it easier to insert full strings of HTML without worrying about the quotes.
    3. Repeat 5 times.

We'll notice that we've written the function that reacts to the page load event as an anonymous function. Alternatively, we can also store this function in a variable – let's call it handlePageLoad.

        let handlePageLoad = function() {
          for (let i = 0; i < 5; i++) { 
            let outputElement = document.querySelector('.output')
            outputElement.insertAdjacentHTML('beforeend', ` 
              <div class="text-3xl my-8">This is the way.</div>
            `)
          }
        }
        window.addEventListener('DOMContentLoaded', handlePageLoad)
      

Some developers prefer the first style, as all code for handling a single event is in one block of code, whereas other developers find the second style more readable. It 100% comes down to personal preference.