Javascript Functions in detail

A deep dive into the workings of Javascript functions.

Most of the work that is possible today with Javascript is because of the flexibility of the language in itself. This flexibility that Javascript provides is highly because of its functions work. I will try to cover most of the important parts that needs to be understood to have a indepth idea of how JS functions work. These are the topics that we are going to cover.

  • Ways to declare and call a function
  • Optional arguments & Default arguments
  • Function bindings & call stack
  • Arguments keyword & strict mode
  • Closures
  • Recursive Functions
  • Pure functions

Ways to declare and call a function

There are three ways a function can be declared.
Step 1: Declaring a function through variable association.
Functions can be declared in a similar way we declare a integer / string in JS.

const sum = function (a, b) {
return a + b;
}

Step 2: As a function definition.
Function definition does not have a assignment operator but its a concise way to declare function.

function sum(a, b) {
return a + b;
}

The difference between step 1 and step 2 is that in the second way of defining a function the function call can be made even before the function is defined like below

sum(1,2);
function sum(a, b) {
return a + b;
}

But the same cannot be done for the first. Each have their own use cases and have different way of working with respect to the this keyword in JS which is something we can cover while discussing JS Objects.

sum(1,2); //This will throw an error
const sum = function (a, b) {
return a + b;
}

Step 3: Arrow function.
Arrow functions are a new addition in ES6 specification into JS. They have some suble differences from traditional way of declaring a function which we will look into further in this blog.

const sum = (a, b) => {
return a + b;
}

If the function has a single argument then the paranthesis can be omitted.

const negate = a => {
return -a;
}

Similarly if the enclosing contents of the function is a statement then we can also omit the curly braces like below

const negate = a => -a;

Optional arguments & Default arguments:

A function can have no, single or multiple arguments to it. If the definition of a function has an argument but it is not passed then the value for that argument will be undefined

function sum(a, b) {
if (b === undefined) {
return a;
}
return a + b;
}
console.log(sum(5)); //Prints 5//You can also do
function sum(a, b) {
b = b || 0;
return a + b;
}
console.log(sum(5)); //Prints 5

From ES6 you will be able to give default parameters for function arguments.

function sum(a, b = 0) {
return a + b;
}
console.log(sum(5)); //Prints 5

You can also use the arguments defined to the left of the current argument in the default value calculation.

function cube(a, b = a * a) {
return a * b;
}
console.log(cube(2)); //Prints 8

You can assign any value as default value of an argument. Like functions too.

function cube(a, b=square) {
return square(a) * a;
}
function square(a) {
return a * a;
}
console.log(cube(2)); //Prints 8

You can also make function calls and have the returned value to the argument. So you can chain argument validations like below

function sum(a, b, c=validate(a, b)) {
if(c) {
return a + b;
}
};
function validate(a, b) {
return (a !== undefined && b !== undefined);
}
console.log(sum(3)) //Prints undefined since b value is not passed.

Function bindings & call stack

When we make a call to a function all the arguments to be passed to the function is binded to the function and the scope of arguments depends on the place of definition of these bindings.
Each time a function call is made new binding instances of the arguments are created. For example

function sum(a, b) {
return a + b;
}
sum (1, 3);
sum (2, 4);

Here the sum function is called twice. In each of these instances the values of a, b are binded separately. So the previous assigned value has no effect on the currently assigned values. So take a look in the following code.

var a = 1, b = 2;
function sum(a, b) {
a = a + 2;
return a + b;
}
sum (a, b);
console.log(sum(a, b)); //Prints 5
console.log(a); //Prints 1

So I am reassigning the value of a inside the sum function. This does not affect the value of a variable defined outside and when a second call is made the value of a is still 1 and not 3. This is because the variable a inside sum is scoped withing that function. This is called local scope. Whereas the variable a outside the sum function is global scoped. These scoped values of variables are context.
But when the argument is an object and you are changing the value of any key inside that object, then it will change the value in the original object.
So whenever a call to a function is made a change in context is done to switch from currently executing function to the newly called function. So the following code will have the following change in contexts.

function func1() {
console.log(1);
func2()
}
function func2() {
console.log(2);
}
func1();
//The following change in context happens when this code is executed
global context
-->func1 context
-->func2 context
-->func1 context
global context

These change in context is depicted as a call stack. It is the switch in context happening whenever a function is called. Each function will have its own local scoped variables and function and it will inturn have its own binded values and so on. So these binded values are the context of that particular function.

arguments keywork & Strict mode

All the arguments of the function are initialized in an array object called arguments when a function is called. The value in arguments keyword are local copy inside the function. So if you make any changes to the local variable it will change the value in arguments keyword too. So we can rewrite

function test1(a) {
console.log(arguments[0]);
a = 3;
console.log(arguments[0]);
}
test1(5); //Prints 5 then 3

There are some subtle changes when you are executing your functions in strict mode. The arguments keyword will always refer to the original values of the arguments that is in the context of the caller function.

'use strict';
function test1(a) {
console.log(arguments[0]);
a = 3;
console.log(arguments[0]);
}
test1(5); //Prints 5 then 5

The arguments keyword will have a reference to the callee function(the function that is currently getting executed). From the callee you will be able to retrieve the caller function.

function func1() {
func2();
}
function func2() {
console.log(arguments.callee);
console.log(arguments.callee.caller);
}
func1(); //Prints func2 then func1

The same code under stric mode will not work. Since arguments.callee has been removed. Any attempts to set the arguments.callee will throw an error.

'use strict';
function func1() {
func2();
}
function func2() {
console.log(arguments.callee);
}
func1(); //throws error

Closures

It is possible to nest one function inside another function like below.

function cube(a) {
const square = () => a * a;
return square() * a;
}

Here the function square is called Nested function. And it is locally scoped to the function cube. So you can’t be able to access the function square outside of the function cube.
1. The nested functions have access to all the locally scoped values of the outer function cube and also to all the globally scoped values.
2. But the outer function cube does not have access to any values local to the function square.
3. The nested function have access to all the locally scoped values of the outer function even after the outer function has completed its execution
These three concepts are the fundamentals of closures in javascript. Whenever a function is called a closure environment for the function is also created. The main use cases of closures in javascript is data privacy, callbacks and event loops.
Take the following example.

function addOne(n) { 
var local = n;
return () => local+ 1;
}
var func1 = addOne(1);
var func2 = addOne(2);

Here new contexts will be created every time addOne is called and the value for the variable local will be specific for those each context. This is particularly useful in asynchronous callbacks that require maintaining some states until the asynchronous callbacks are available. So that code would look like this.

function asyncFunc(a, b) {
var localState = 'abc';
asyncURLCall(() => {
console.log(a, b, localState);
})
}
asyncFunc(1,2);
//Prints 1 2 abc
//after the asyncCall is complete
asyncFunc(2,3); //Prints 2 3 abc
//after the asyncCall is complete

Here the local variables a, b, localState will retain its values until the asyncURLCall is complete. And no matter how many times you call this function the context will not be shared between these calls.

Recursive Functions

Like a function calling another function a function call itself. If you are coming from other computer programming languages then this would be fairly simple to understand. One common case where we use recursion is for implementing ideas that are based on divide and conquer method.

function power(base, exp) {
if(exp === 0) {
return 1;
}
return base * power(base, exp - 1);
}

Pure functions

Pure functions are functions that give the same output for the same set of inputs without modifying the input values. Other functions that produce different outputs for same inputs depending on the current state of the system are called functions with side effects. Pure functions tend to be lot more reusable than functions that produce side effects. It is better to refactor your code to move all the code that can be pure functions on its own to increase the reusability of your code. The sum function that we say is an example of pure function.

Thank you.

Senior Software Engineer — Frontend @Freshworks.