The World of Functions
Quick update
It's been almost a month since I published the first part of the Eloquent JavaScript Book Club series. I enjoyed the feedback I received from the Dev.to community and was happy to see folks wanting to join the book club and read along. However, as it has been almost a month, I'm sure many of you continued and finished the book without me. It's cool; I get it. Personally, a lot has happened in my life.
I quit my current job and am very happy to have accepted an excellent offer at a great company. I received an unconditional offer for a Masters in Computer Science & Artificial Intelligence, where I'll be studying part-time for the next two years. I learned a heck load of data structures, algorithms, systems design, and everything in between.
It's been a wild month, but I'm ready to sit back a bit. Drink a nice cold brew. Open up the dusty textbook and get into some eloquent JavaScript. Before we start, I want to quickly mention that I also completed the Just JavaScript book/workshop/mini-course by Dan Abramov. I've already seen some parallels between eloquent JS and that course. I would wholeheartedly recommend getting into both. It helped solidify my mental model of how things like variables (and functions) work. There should be a blog post to analyse the two texts.
Right, functions.
People think that computer science is the art of geniuses, but the actual reality is the opposite, just many people doing things that build on each other, like a wall of mini stones.
- Donald Knuth
Without functions, our code wouldn't function well. It will still do its job. Expressions and statements will continue to execute whilst our bindings (variables) will continue latching onto data. But without some order or a way of keeping related code together, it'd be complicated to manage.
We can create functions with a function expression. It's similar to how we have defined variables.
const addTwo = function (num) {
return num + 2;
};
The num
is a parameter, whilst the curly braces encapsulate the body of the function. The code above creates a constant called addTwo
and binds it to a function that takes in a number and adds two to it.
Some functions have a return statement. Others return nothing at all. Yet just because it seems like it returns nothing in the code, in reality, all operations with no explicit return statement return undefined.
Another example is to open your browser's console, and type in console.log('hello')
. You'll see hello being printed, but you'll also get the type undefined
returned. That's because the .log()
is a function that doesn't return anything. It just runs a side effect, which is the printed message.
Side note, the same thing happens when we execute an expression like let x =
as variable declarations do not produce a value but returns something.
42;
Understanding Scope
A crucial advantage that a function has is being able to have it's own scope. It's a mechanism that allows a function to deal with its internal state and prevent other functions from manipulating state. It creates separation of scope, where you have the global scope (outside the function), and the inner scope. Global scope is like setting some variables at the top of your file.
let time = 9;
let closingTime = 11;
Functions have the ability to read those variables, and even manipulate them (we will discuss why this is not necessarily good). However, we can't reach into functions and control the variables.
const personalSchedule = function () {
let doctorsAppointment = 12;
};
console.log(doctorsAppointment);
// doctorsAppointment is not defined
These variables are known as local variables (or local bindings). They only exist for a limited amount of time, when the function is called. Then, once the the function has finished executing, they cease to exist. It's quite melancholic.
A key thing to note is that variables declared with let
or const
are local to the block they are called in, and therefore can not be called outside the block, unlike var
. A great example is a for loop:
for (let i = 0; i < 5; i++) {
// execute code
}
console.log(i); // undefined
for (var i = 0; i < 5; i++) {
// execute code
}
console.log(i); // 5
Notice the difference in the highlights
Another thing to note is that whilst we can't look inside a function to get its variables, we can look outside the scope of the function.
const x = 10;
const halve = function () {
const divided = x / 2;
const print = function () {
console.log(x); // 10
console.log(divided); // 5
};
print();
};
halve();
The print
function inside halve can interact with both the x
variable in the global scope, as well as the divided
variable within the scope of the halve
function. This is also known as lexical scoping, where each local scope can also see all the local scopes that contain it. On top of that, all scopes can see the global scope.
Declaring Functions
We've seen functions declared as an expression. We can also assign them in a shorter way through what is known as function declarations.
function booDeclare(name) {
console.log(`BOO! Did I scare you ${name}?`);
}
// how we would write it before
const boo = function (name) {
// ...
};
There is a difference between the two, and it's primarily due to something called hoisting (we won't get into this right now). If you were to call booDeclare
before it was declared, you would see that it still works. However, we can't say the same for the other function. This is due to function declarations being hoisted up to the top of the conceptual page, and thus is able to be used anywhere in the code.
This kind of makes sense, as the second function is more like how we declare a variable, and that we are unable to know what the variable binds to before it is declared.
console.log("I am walking through a haunted house", booDeclare("Alex")); // works
function booDeclare(name) {
return `BOO! Did I scare you ${name}?`;
}
console.log(boo); // Cannot access 'boo' before initialization
const boo = function (name) {
return `BOO! Did I scare you ${name}?`;
};
console.log(ghost); // Cannot access 'ghost' before initialization
const ghost = "nice ghost";
Arrow functions
You might be familiar with arrow functions as well. They are newer syntax, and they provide us a way of writing small function expressions in a (my opinion) cleaner manner.
const owedMoney = (sum) => {
return sum;
};
// can be written as
const owedMoney = (sum) => sum;
The code is less verbose, as it now implicitly returns the value that sum is bound to, and there are no curly braces. There is another difference between the arrow function and a function expression, and that is regarding the keyword this. We will talk about it more once we get to Chapter 6 (can't wait).
Optional Arguments
The beauty of JavaScript is that it's quite lenient in what you can do (compared to other languages).
function ages() {
console.log("I have no args");
}
ages(11, 23, 52, 59, 32, 53, 99, 29, 48, 75, 49, 23); // I have no args
No errors! What happens here is that JavaScript will ignore all these arguments if they're not being used. Simple. Even if you specified the arguments, and didn't provide any parameters, JavaScript will still not error out.
function ages(person1, person2, person3) {
console.log(person1, person2, person3);
}
ages(19); // 19 undefined undefined
JavaScript assigns missing parameters to undefined
(similar to when you declare let x;
). It also dismisses any parameters provided if there's no explicit use for them. As you can tell, this is not so beautiful. The downside here is that you can accidentally pass the wrong number of arguments, or none at all, and you might not realise that you have a bug.
One way to assign a value to an argument even when it's not passed is to use optional arguments.
function ages(person1 = 23, person2 = 99) {
console.log(person1, person2);
}
ages(22); // 22 99
Again, this is not the ultimate solution as it will only assign the parameters in order. So if you don't pass anything in the second argument, person2
will always default to 99. That's why it's common to see code like this (albeit this is very contrived).
function fetchPosts(url, method = "GET") {
const data = fetch(url, { method });
}
Functions and Side Effects
As we've seen, functions can be split into two types. Functions that execute other functions or side effects, and functions that have return values. At times, you will have functions that do both. Each have their own use cases, and their own advantages. Functions with return values will almost always be called more often, since we rely on the values returned to execute more code.
There are pure
functions, that have the pleasure of always being reliable. The purity comes from:
- relying on global variables whose values might change
- always returning/producing the same value
- can easily be replaced with a simple value:
const return5 = () => 5;
let total = 5 + return5(); // 10
total = 5 + 5; // 10
They are easily testable, making unit tests a breeze to write. They usually are quick to understand, as you don't need to scour other parts of the codebase to see what's being called. In essence, they're great. Yet, that ease comes with a bit of difficulty. Whilst you can write primarily pure functions, you'll realise quickly that some side effects are needed. So unless you're a total purist who despises side effects, I'd say it's fine to have a mixture of both. Like the author says:
"There'd be no way to write a pure version of console.log, for example, and console.log is good to have."
Summary
So, functions. A brilliant addition to our JavaScript tool-belt that allows us to manage multiple kinds of scope, separating code logic, not repeating ourselves, and understanding side effects. The chapter gave us a lot of information, and I think it's an important fundamental to really grasp. The author also brings up concepts like the Call Stack, and Recursion. I decided not to include that in this chapter as I felt it deserved a separate snack-esque
post. You can read more about it on my website, although the blog post is still "growing."
Thanks for reading! The next chapter will be about some rather essential data structures, Objects and Arrays.
If you'd like to attempt the exercises for the chapter, you can find them at the bottom of the chapter. Let me know how you get on. I definitely recommend going through them, to help solidify your knowledge.
Top comments (1)
Nitpick:
let
is a statement. Assignment/binding is an expression. That is whylet x = y = 42;
works.x = 42
evaluates to42
as doesx = y = 42
.
global
(Node.js), globalThis (evergreen browser windows, web workers and Node.js (12.0+)).var
declarations inside of inline scripts (type="text/javascript"
, nottype="module"
) attach themselves as properties to the global object. That doesn't happen inside functions, module scripts and ECMAScript modules.import
names are completely hoisted throughout that module ("module-global") but that doesn't affect other modules.While true in your particular example that isn't the full story as you explore in your Chapter 3 bonus.
Given the right circumstances the local variables of that particular invocation will continue to exist inside the closure.
Compare this to
If you squint you can see the similarities. Hence quote:
You can use a closure like an implicit object.
That difference (their
this
being bound to thethis
of their site of creation rather than being dynamically set at run time based on the manner of invocation) is the reason why arrow functions were introduced in the first place.You can use a default parameter by deliberately passing
undefined
.null
doesn't work as it is considered an intentional value.The two types that people actually get hot and bothered about are "synchronous" vs. "asynchronous" (i.e. returns a promise)
Professor Frisby's Mostly Adequate Guide to Functional Programming - Chapter 03: Pure Happiness with Pure Functions:
A pure function is free to call other pure functions.
Functional Geekery Episode 08 - Jessica Kerr isolation:
Probably was supposed to be
more like
This property is referred to as referential transparency.
… so that a function invocation with a specific set of arguments can be simply replaced with its return value.
See Functional Core - Imperative Shell (related to Hexagonal Architecture).
Is that what you mean?
filter
is a method of the Array instance (Array.prototype.filter()). But it doesn't usesearchQuery
.filter
is a higher order function (HOF) - i.e. it is passed a function value as an argument, in this case(post) => post.includes(searchQuery)
. It's that argument (function value) that accessessearchQuery
via the closure it (the function value) was created in.The fact that
searchQuery
is outside the scope offilter
is irrelevant asfilter
only uses the passed function value. It's the passed function value that accessessearchQuery
outside of its own function scope but inside its closure.You can use the declared function name directly for
filter
- no need to create another intermediate arrow function.