Let’s talk about functions. Not the easy peasy stuff that you can grasp in a few hours or minutes. This post will delve a little deeper into the concept of functions, from the perspective of the JavaScript programming language.
The Basics
Functions are an abstraction, a way to manage complexity by encapsulating related logic and naming it for later reuse. Every language defines a way of creating functions.
Functions in JavaScript, just like in any other language, have their own local scope. This is a local environment specific to a given function where all computation takes place.
There are three main ways of creating functions in JavaScript. Let’s review them.
Function declaration method
Below is a function declaration:
function add(x, y) {
return x + y;
}
Function expression
In JavaScript, we can define functions as expressions.
const add = function (x, y) {
return x + y;
};
Arrow functions
Lastly, we have arrow functions.
const add = (x, y) => {
return x + y;
};
Arrow functions are the new kid on the block. They have their own peculiarities which we’ll look into later.
x
and y
are the parameters to the functions.
Callling a function
To call a function, we use the following expression:
const sum = add(2, 3);
Let’s go through what happens when the you run this expression.
First, a binding is created in the local environment/scope of the function that
associates each named parameter with the corresponding value. Here, x
will be
bound to 2
and y
to 3
. A binding is simply a way to associate a name to a
value. The x
and y
variables are internal to the function, they get defined
when you call the function and are initialized with the values that you pass in
as parameters.
Finally, when the return expression gets executed, x
is replaced by the value
it points to. Same for y
. A result is calculated and returned to the scope where
the function was called. This would be the global scope for this particular
example. The result is assigned to the sum variable.
Beyond the basics
In this section, I’m going to present a series of examples that highlight particular concepts in functions, ask you to guess how the execution would proceed, and try to explain as simply as I can what’s going on. I find this method to be more instructive when I’m trying to learn something new. Learning by doing and taking things apart to discover their essence and understand them bottom up.
Let’s start with a simple one.
function jump(y) {
y = null;
}
let x = 10;
jump(x);
console.log(x);
What is x?
If you think it’s the number 10, you are absolutely right. jump
cannot change
the value of x. When called, the value of x is what is passed to the function,
not the reference. So x will keep pointing to its original value (10),
and y will now have the value of 10. This is what’s referred to as passing
parameters by value. It’s usually what happens when you pass primitives to
functions (as opposed to objects). This is not peculiar to JavaScript alone, but
is common in most programming languages.
Here is another example.
const add = (x, y) => x + y;
function addf(first) {
return function (second) {
return first + second;
};
}
// addf(2)(3) => 5
Here, we have defined a function addf
that takes one parameter, and returns
another function, which takes a second parameter and adds them. It basically
performs addition from two invocations.
In JavaScript, we can define functions inside other functions or pass them as values, just like we would some primitive value.
When you first call addf
passing to it the number 2, say, a new function is
returned. When you again call this new function passing to it 3 as a paramter,
the sum of the first value you initially passed to addf
and the value you
passed to the returned function defined inside addf
, gets calculated and
returned.
JavaScript functions keep a memory of the scope where they were defined. They thus have access to all variables that are defined in that scope or in the parent of that scope or in the parent of the parent of that scope…you get the picture.
In this example, the function defined inside addf
has access to the scope of
addf
, even if it is called outside of this scope. This is what is called
Lexical scope. Lexical scope simply means that locally defined functions
have access to the name bindings in the scope in which they are defined. Inner
functions will have access to the names in the environment where they are
defined (not where they are called).
For more, check out this excellent article.
Yet another example.
const add = (x, y) => x + y;
const mul = (x, y) => x * y;
function liftf(binary) {
return function (first) {
return function (second) {
return binary(first, second);
};
};
}
// Invocations
const addf = liftf(add);
addf(3)(4); // => 7
liftf(mul)(5)(6); // => 30
The liftf
function takes a binary function(takes two params) and makes it
callable with two invocations as shown in the examples. It takes a binary
function and returns a function which takes the first argument which returns
a function that takes the second argument which returns the result of passing
the first and second argument to the binary function.
This example can also be explained using similar principles as the previous ones. It’s all about lexical scopes and closures.
When you initially invoke liftf
passing to it the add
binary function, it
returns the first nested function which gets assigned to addf
. Calling addf
and
passing it the first argument (3) returns another function, the innermost
nested function. This innermost function has only one expression. When called
with the second argument (4), the return expression gets executed. The first and
second arguments are passed to the add binary function and their sum is
returned. The innermost function accesses the first argument by looking inside
out. This is how variables are resolved in JavaScript. The initial search starts
in the current scope, and if the variable is defined there, the search goes one
step higher to the immediate parent scope, and so on and so forth till the value
of the variable is resolved. An error results if the variable is not defined in
any of environments/scopes that get searched.
Still some more.
Let’s define a function called curry that takes a binary function and an argument and returns a function that can take a second argument.
function curry(binary, first) {
return function (second) {
return binary(first, second);
};
}
// Invocations
let add2 = curry(add, 2);
add2(5); // --> 7
curry(mul, 5)(6); // --> 6
The curry
function takes multiple arguments and returns a function that takes
a single argument. The first argument that’s passed to curry
will be used
every time we call the returned function. The nested function has access to the first
argument passed to the function that encloses it. This is closure. We have
already touched on it.
The process of transforming a function with multiple arguments into multiple
functions that take a single argument is called currying. For example f(a, b, c)
being transformed into f(a)(b)(c)
.
Check out the example below.
const add = (x, y) => x + y;
function curry(f) {
return function (a) {
return function (b) {
return f(a, b);
};
};
}
// Invocations
const curryAdd = curry(add);
curryAdd(1)(2); // --> 3
This example follows the principles we have already discussed.
You can find more advanced implementations in libraries such as lodash.