DEV Community

Cover image for Book Club: Eloquent JavaScript - Chapter 3
Alex Kharouk
Alex Kharouk

Posted on • Originally published at kharo.uk

Book Club: Eloquent JavaScript - Chapter 3

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;
};
Enter fullscreen mode Exit fullscreen mode

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 =
42;
as variable declarations do not produce a value but returns something.

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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) {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (1)

Collapse
peerreynders profile image
peerreynders

like let x = 42;

Nitpick: let is a statement. Assignment/binding is an expression. That is why let x = y = 42; works. x = 42 evaluates to 42 as does x = y = 42
.


Global scope is like setting some variables at the top of your file.

  • There is a global object that is available via the "global scope" but that is pretty much the extent of the "global scope". No new global names can be created. It just seems that way because properties on the global object can be directly accessed without referencing them through the global object - that is why they seem to exist in the "global scope". So if you need to access new names globally then you need to add them to the global object as properties.
  • Global objects: window (browser window), self (browser window and web worker), global (Node.js), globalThis (evergreen browser windows, web workers and Node.js (12.0+)).
  • The issue is that script level (outside of functions) var declarations inside of inline scripts (type="text/javascript", not type="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.

Then, once the the function has finished executing, they cease to exist.

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.

const [a, r] = scheduleAppointment(12);
console.log(a()); // 12
r(10);
console.log(a()); // 10

function scheduleAppointment(initial) {
  let time = initial;
  return [appointment, reschedule];

  function appointment() {
    return time;
  }

  function reschedule(other) {
    time = other;
  }
}

Enter fullscreen mode Exit fullscreen mode

Compare this to

const o = scheduleAppointment(12);
console.log(o.a()); // 12
o.r(10);
console.log(o.a()); // 10

function scheduleAppointment(initial) {
  return {
    time: initial,
    a: appointment,
    r: reschedule,
  };

  function appointment() {
    return this.time;
  }

  function reschedule(other) {
    this.time = other;
  }
}
Enter fullscreen mode Exit fullscreen mode

If you squint you can see the similarities. Hence quote:

  • Objects are merely a poor man's closures.
  • Closures are a poor man's object.

You can use a closure like an implicit object.


There is another difference

That difference (their this being bound to the this 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.


Again, this is not the ultimate solution as it will only assign the parameters in order.

You can use a default parameter by deliberately passing undefined.

ages(undefined, 22); // 23, 22

function ages(person1 = 23, person2 = 99) {
  console.log(person1, person2);
}
Enter fullscreen mode Exit fullscreen mode

null doesn't work as it is considered an intentional value.


As we've seen, functions can be split into two types.

The two types that people actually get hot and bothered about are "synchronous" vs. "asynchronous" (i.e. returns a promise)


Functions that execute other functions or side effects, and functions that have return values.

Professor Frisby's Mostly Adequate Guide to Functional Programming - Chapter 03: Pure Happiness with Pure Functions:

A pure function is a function that, given the same input, will always return the same output and does not have any observable side effect.

A pure function is free to call other pure functions.

Functional Geekery Episode 08 - Jessica Kerr isolation:

When the only information a function has about the external world is passed into it via arguments.


relying on global variables whose values might change

Probably was supposed to be

not relying on global variables whose values might change


always returning/producing the same value

more like

the value returned only depends on the arguments supplied and will always be the same for the same arguments.

This property is referred to as referential transparency.

can easily be replaced with a simple value:

… so that a function invocation with a specific set of arguments can be simply replaced with its return value.


I'd say it's fine to have a mixture of both.

See Functional Core - Imperative Shell (related to Hexagonal Architecture).


outside the scope of the filter method

Is that what you mean?

filter is a method of the Array instance (Array.prototype.filter()). But it doesn't use searchQuery. 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 accesses searchQuery via the closure it (the function value) was created in.

The fact that searchQuery is outside the scope of filter is irrelevant as filter only uses the passed function value. It's the passed function value that accesses searchQuery 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.

const blogPosts = [
  'Just Graduated a Boot Camp!',
  'What is a Stack?',
  'Growth Mindsets: A Fever Dream?',
  'Working at FAANG! An Ex-FAANG Tale',
  'Working at a Startup! An Ex-CEO Tale',
];
const searchQuery = 'Ex';
console.log(blogPosts.filter(findPostByQuery));
/* ["Working at FAANG! An Ex-FAANG Tale",
  "Working at a Startup! An Ex-CEO Tale"];
*/

function findPostByQuery(post) {
  return post.includes(searchQuery);
}
Enter fullscreen mode Exit fullscreen mode