Functions are a fundamental part of any programming language. This lesson will cover function syntax, usage, and some mechanics.
- Know how to declare a function
- Have an understanding of scope and variables inside and outside of functions
- Understand function hoisting
- Understand the difference between returns and side-effects
- Know some of the key differences between ES5 and ES6 syntax
- Learn and explore some functions built-in to JS (like Math)
- Function Definition
- Function Call
- Function Expression syntax
- Function Declaration syntax
- Global variable scope
- Parameters & Arguments
- Local variable scope (function scope)
We use functions to contain code that we want to re-use, or to contain code that we want to call at a later time.
Let's say we have two different numbers:
let smallNum = 6
let bigNum = 15389
Now let's say we want to do some math on these numbers. We could do it the manual way:
let smallResult = (smallNum * 4 + 23 / 8 - 4) % 2
let bigResult = (bigNum * 4 + 23 / 8 - 4) % 2
Now that's not so bad, but what if we had ten numbers? or a hundred? Typing that out would be a huge pain, and we have way better options than to force ourselves to do something this repetitive.
We can instead write a single function to do this for us.
Functions allow us to reuse code so that we don't have to repeat ourselves:
let smallNum = 6
let bigNum = 15389
function doMath(num) {
console.log((num * 4 + 23 / 8 - 4) % 2)
}
We've taken the code that does some several step math process and put it inside a function so that we can reuse it.
Specifically, this function takes in an input (called an argument, in this case the type
is a number
) and console logs a new number after performing all those operations.
So we have a way to declare a function, which creates it. But if we want to execute (or call) that function, we do this:
doMath(smallNum) // => 0.875
doMath(bigNum) // => 0.875
Here we're calling the function twice and passing in a different value each time. The function prints out the result of the math equation.
Let's break down this new syntax with another example:
-
The above syntax is called function declaration. The word
function
tells JS that we are declaring a function. Just likeconst
lets JS know we are about to declare a variable. -
squareNumber is the name of the function. Not all functions need a name. Some functions are anonymous and don't have a name. But for now, we will be dealing with functions that do have a name. Ideally the name describes what the function does.
-
Inside the parentheses are parameters which are used to define a function. When we call a function we replace parameters with arguments. Arguments are the real values that are passed into a function. Often the words parameters, and arguments are used interchangeably.
-
The opening curly brace,
{
indicates the start of the function. The closing curly brace,}
indicates the end of the function. -
Between the braces, is the code that makes up the function. This is called the
function body
. return is the result (what we get back) after we've called the function. If there is no return statement, the function will returnundefined
. Once something is returned from a function, the function has ended. This means, that even if there is code after the return statement, it will not be executed.
A function is like a mini-program inside our main program. Whenever the code inside it is done running, we return to the line from which we called the function. A variable defined inside a function will be forgotten when the function is done running. Every time we call the function logPets
below, the variables will be created anew.
// This function will print 'cat' and then print 'dog'
function logPets() {
let pet = "cat"
console.log(pet)
pet = "dog"
console.log(pet)
}
console.log("start here") // => "start here"
// logPets will do the same thing every time we call it
logPets()
// => 'cat'
// => 'dog'
logPets()
// => 'cat'
// => 'dog'
console.log("okay we're done") // => "okay we're done"
To understand scope, we have to start thinking about where variables are accessible and where they're not.
So far we've only dealt with what's called global scope
, where every variable is accessible from everywhere. Now that we're working with functions, we can start to think of scope as a sort of hierarchy.
let parting = "gooodbye"
function hello() {
let greeting = "hello"
console.log(greeting) // => "hello"
console.log(parting) // => "goodbye"
}
console.log(greeting) // ReferenceError: greeting is not defined
console.log(parting) // => "goodbye"
The key takeaway here is that variables declared in a function are only accessible inside that function. Variables declared outside a function are called global
and they can be accessed and modified from any function.
A variable declared inside a function has local scope, and a variable not declared inside a function has global scope. So a variable with local scope is only available inside that function. A variable with global scope is available inside or outside the function.
If we create a variable inside a function with the same name as a global variable - the function will only be aware of the local one. This, however, will not change the value of the global variable.
let greeting = "hello"
function hello() {
let greeting = "what's up?"
console.log(greeting) // => "what's up?"
}
console.log(greeting) // => "hello"
If we have a function inside of a function, the rules still apply, but now we have more than just two scopes.
let global = "hello global"
function firstLevel() {
let first = "hello first"
console.log(first) // => "hello first"
console.log(global) // => "hello global"
function secondLevel() {
let second = "hello second"
console.log(first) // => "hello first"
console.log(second) // => "hello second"
console.log(global) // => "hello global"
}
console.log(second) // => ReferenceError: second is not defined
}
console.log(global) // => "hello global"
console.log(first) // => ReferenceError: first is not defined
console.log(second) // => ReferenceError: second is not defined
When a function has parameters, it's helpful to think about them like temporary variables that only exist inside the function.
Let's say we have a function with two parameters. When we call the function, we have to provide that function with two values. Then we can access those values inside the function.
// declare the function with two parameters
function iHaveParams(num, str) {
console.log(num, str)
}
// call the function, pass in some values
iHaveParams(22, "why") // => 22, why
// num and str are not accessible outside the function
console.log(num, str) // ReferenceError
This is very valuable! We often use functions to contain our scope, so we don't accidentally affect variables outside of it.
In ES6 we can declare functions with parameters that have a default value. This way we can call the function and provide our own value as usual, or call the function without passing in a value, in which case the default applies.
Example:
function square(n = 5) {
return n * n
}
console.log(square(3)) // => 9
console.log(square(10)) // => 100
console.log(square()) // => 25
Return statements are crucial to understanding how functions work. When you call a function that has a return statement in it, it produces a new value that you can then use later.
A parameter can be considered an input, returns are considered outputs.
Let's review this with another function:
function sayHello() {
return "Hello"
}
let greet = sayHello()
console.log(greet) // => "Hello"
This is an oversimplified example - there's no input and we could just skip the function and let greet
equal "Hello"
. So lets make it a little more complex.
let greet = "hello"
function addName(name) {
return greet + " " + name
}
let namedGreet = addName("jimmy")
console.log(greet) // => "hello"
console.log(namedGreet) // => "hello jimmy"
Here, we've got an input ("jimmy"
) and an output ("hello jimmy"
).
The takeaway from this is that returns produce a new value from the function. To use that value, we have to capture it in a variable, otherwise it disappears.
If we don't return anything from a function, the function automatically returns undefined
.
function logSum(num1, num2) {
console.log(num1 + num2)
}
function sum(num1, num2) {
return num1 + num2
}
Though these two functions look similar, they are very different. Here's how:
console.log("Twice the sum of 4 and 6 is " + logSum(4, 6) * 2) // => NaN
console.log("Twice the sum of 4 and 6 is " + sum(4, 6) * 2) // => 20
This is because logSum(4,6) returns undefined, even though it happens to print out the sum. Just because the value appears in the console doesn't mean it's been returned by the function, they do different things.
We say that a function has side effects when it affects something outside of its own scope. One example is changing the value of a variable that was defined outside the function.
let myNumber = 2
console.log(myNumber) // => 2
const sideEffect = function () {
myNumber += 1
}
sideEffect()
console.log(myNumber) // => 3
sideEffect()
console.log(myNumber) // => 4
Because we're referencing the variable myNumber
directly by name, and because myNumber
was declared outside the function, we can affect the value.
Sometimes you want to do this on purpose - but often times it's better to fix your code so that it doesn't do this.
How could we modify this code so that it doesn't produce a side effect?
We've seen one way to define a function
like this:
function printArray(arr) {
console.log(arr)
}
In JavaScript, functions are values just like arrays and strings, and can be saved in variables.
const printArray = function (arr) {
console.log(arr)
}
The first way is called a function declaration
The second way is called a function expression.
Recently, JavaScript introduced new syntax for a lot of different things, functions among them.
To write that same function in ES6 is just a little bit different. First we declare the function like a variable using the const
declaration.
We then state the name of our function and provide it arguments, a fat-arrow, and then curly braces. Like this:
const printArray = (arr) => {
console.log(arr)
}
This is also called an arrow function.
There are some subtle differences between the three different ways. The biggest one is called hoisting
.
When a javascript file is executed, the JS interpreter reads through the whole file top to bottom, assigning values and figuring out what needs to be run when. Function declarations are then hoisted or lifted to the top of the file, essentially reordering them.
Note: This is not actually what happens behind the scenes, but thinking about it this way is simpler.
This means that you can call a function before it is defined:
console.log(sayHelloDec) // logs: [Function: sayHelloDec]
sayHelloDec() // logs "hello"
function sayHelloDec() {
console.log("hello")
}
A function defined with expression syntax or ES6 arrow function will be assigned to a variable, and any variable used before it is defined will have the value undefined
console.log(sayHelloExp) // logs: undefined
console.log(sayHelloExpTwo) // logs: undefined
sayHelloExp() // TypeError: sayHelloExp is not a function
sayHelloExpTwo() // TypeError: sayHelloExpTwo is not a function
const sayHelloExp = function () {
console.log("hello")
}
const sayHelloExpTwo = () => {
console.log("hello")
}
Now that we know how to write our own functions, let's look at some that javascript has provided us, already built in to the language!
We're going to combine our functions with the JS Math
module. Math gives us a lot of useful tools for working with numbers.
// Gives us a random number between 0 and 1
Math.random()
// Usage:
function getRandom() {
return Math.random()
}
let rand = getRandom() // 0.53727864
// Rounds to the nearest whole number
// Usage:
Math.round(num)
console.log(Math.round(11.23)) // 11
console.log(Math.round(32.92373)) // 33
// Rounds up or down to the nearest whole number
// Floor rounds down, ceil rounds up
// Usage:
Math.floor(num)
Math.ceil(num)
console.log(Math.floor(11.23)) // 11
console.log(Math.ceil(11.23)) // 12
console.log(Math.floor(32.92373)) // 32
console.log(Math.ceil(32.92373)) // 33
// Finds the highest or lowest number out of all the numbers provided
// Pass in as many numbers as you want
// Usage:
Math.max(a, b, c, d)
Math.min(a, b, c, d)
console.log(Math.max(55, 22, 66, 12, 13, 29, 92)) // => 92
console.log(Math.min(55, 22, 66, 12, 13, 29, 92)) // => 12
It is best practice to write your functions to only do ONE thing. If you find yourself needing your function to do multiple things, you should consider breaking it up into multiple functions.
For now, you may not notice the importance because we're still writing very simple functions, but if we were making something more complex you would quickly see the benefit.
Keeping your code small and modular makes it much easier to follow, especially when you are just starting to learn.