We already learnt that we can write functions like function doThing () { ... }, but there are other ways too, and it's also important to understand how variables are 'scoped'.
Try the code below:
let x = 5
function foo () {
let y = 3
console.log(x)
}
console.log(y)
You'll get an error Can't find variable: y. This is because when we're trying to console.log(y) it is no longer in scope - it is local to function foo() and only exists within that function. Meanwhile, variable x is global and can be accessed anywhere in the program.
This doesn't apply just to functions, but to loops and selection too. In this code:
for (let i = 0; i < 5; i++) console.log(i) // works
console.log(i) // i is out of scope
... the 2nd line will throw an error as i is out of scope - it only exists until the end of the for loop.
In this code:
if (true) {
let x = 'hi'
}
console.log(x)
... again x only lives within the if block, so isn't accessible when we try to print it.
The previous examples could be 'fixed' by declaring the variables outside the blocks, ie in the global scope:
let i
for (i = 0; i < 5; i++) console.log(i)
console.log(i)
let x
if (true) x = 'hi'
console.log(x)
Something slightly strange happens when we name a local variable the same as a global variable:
let x = 5
doThing()
function doThing () {
let x = 3
console.log('Inside the function', x) // outputs: 3
}
console.log('Outside the function', x) // outputs: 5
... now we have 2 variables both with identifier x - one of them is global to the program, the other is local to the doThing() function. But they are not the same and are independent of each other. Try not to do this in your code as it can be confusing. Also, the global variable is no longer accessible inside the function, as it has been shadowed by the local variable.
Functions can have parameters which support default values.
doThing(5) // outputs: 5 4
doThing() // outputs: 10 4
function doThing (num = 10, num2 = 4) {
console.log(num, num2)
}
Above, the first call to doThing() uses 5 as an argument, which doThing() receives as its 1st parameter. Often programmers use the words 'argument' and 'parameter' interchangeably, but technically a parameter is the variable into which the function receives the argument passed.
Some languages allow you to choose whether a function takes parameters by value or reference. In JavaScript, numbers and strings are always by value while arrays and objects are always by reference. Think of 'by value' as being a clone of a variable, while 'by reference' refers to the same memory location as the argument passed. You'll learn more about this later.
Functions can be thrown around just like numbers and strings:
multiRun(sayHi, 5)
function sayHi () {
console.log('hi world')
}
function multiRun (func, num = 3) {
for (let i = 0; i < num; i++) func()
}
... here the actual function sayHi() is being sent as the first argument to multiRun(), which is then running that function 5 times.
It is quite common in JavaScript - especially on the web - to want something to run at some point in the future, or on repeat every so often.
console.log('Begins')
setTimeout(someFunc, 2_000)
console.log('Ends?')
function someFunc () {
console.log('When does this get logged?')
}
The setTimeout() function queues a function to run after a certain number of milliseconds. It's first argument is a function. Run the above code and note the order in which the strings are logged to the console.
While setTimeout() runs a function once, setInterval() runs it on repeat, every x milliseconds.
console.log('Begins')
setInterval(someFunc, 2_000)
console.log('Ends?')
function someFunc () {
console.log('On repeat')
}
You'll need to exit the above program by using CTRL+C.
Both setTimeout() and setInterval() takes functions as arguments, as do other event handlers you'll learn about later when we use JavaScript with the web.
When used solely as arguments to other functions, functions don't even need to be named, and can stay anonymous:
multiRun(function () { console.log('hi world') }, 5)
function multiRun (func, num = 3) {
for (let i = 0; i < num; i++) func()
}
There's an even shorter way to write it, using an 'arrow function':
multiRun(() => console.log('hi world'), 5)
function multiRun (func, num = 3) {
for (let i = 0; i < num; i++) func()
}
Arrow functions are used a lot in modern JavaScript, where passing functions around as arguments is common practice.
It is quite common to use an anonymous arrow function when setting a timeout or interval or when setting event handlers:
console.log('Begins')
setTimeout(() => console.log('When does this get logged?'), 2_000)
console.log('Ends?')
In Python, you may be familiar with time.sleep() to wait a number of seconds before executing the next line. The equivalent in JavaScript involves 'promisifying' the setTimeout() function, to create a new function called sleep(). This is another example of anonymous arrow functions:
let sleep = t => new Promise(r => setTimeout(r, t))
console.log('Begins')
await sleep(2_000)
console.log('When does this get logged?')
console.log('Ends?')
Note the use of the keyword await before sleep(2_000). Without await, the next line is executed straight away.
In Bun, you can also use await Bun.sleep(2_000) (remembering again to await), but this won't work on the web where 'Bun' dodesn't exist.
So far I've been using the word 'function' loosely. Technically, a function must return a value, while a procedure doesn't. In addition, a function is 'pure' if it always returns the same output for given input, without anything else happening (such as printing or changing/mutating global variables). JavaScript only has the function keyword though.
let z = 3
// a 'function' (or 'non-pure function')
function add (x, y) {
console.log(`Adding ${x} and ${y}`) // not a 'pure' function as this is a side-effect
z++ // also not 'pure' as it mutates a global variable
return x + y
}
// a 'procedure' as it doesn't return
function subtract (x, y) {
z--
console.log(x - y)
}
// a 'pure function' as only returns with no side effects
function multiply (x, y) {
return x * y
}
Where possible, pure functions are preferred over procedures or non-pure functions. Think of a function as a black box that turns input (arguments) into output (return values) without anything else happening.
Arrow functions, although anonymous, can be assigned to variables. In addition, for single-line arrow functions you can omit the keyword return as this is implied.
let add = (x, y) => x + y // 'return' omitted as implied
let subtract = (x, y) => {
return x - y // need 'return' keyword due to the { braces }
}