Fundamentals of Web Application Development · DRAFTFreeman

Functions

Functions are a core building block of any JavaScript program. Functions are considered first-class – that is, they are just objects in memory and can be treated like any other object. They can be passed as arguments, returned from other functions, declared in a given scope or created on-the-fly. What differentiates functions from other objects is that they are callable, meaning that they can be invoked in program execution (this is normally done with parentheses).

JavaScript provides a few different ways to create functions. How a function is created affects its availability in other parts of the code.

Function Declarations

From some of the previous examples, you’ve probably picked up on how functions are declared in JavaScript. Generally, the anatomy of a function1 appears as

function name(param_0, param_1, /* …, */ param_n) {
  /* statements */
  return /* value or expression */;
}

The statement starts with the keyword function, followed by a space, then the function’s identifier name, followed by a set of parentheses (()). The parentheses enclose the function’s formal parameter definition, a set of zero or more named function parameters separated by commas. Named parameters are available as function-scoped variables inside the body of the function.

Finally, the function body is enclosed in curly braces ({}). The body can be made up of zero or more statements.

Arguments are passed to functions by value; changing the immediate value of an argument inside of a function body does not affect the value of any variables outside of the function’s scope.

function foo(b) {
 console.log('foo starts with b =', b);
  b = 84; // reassign variable b
 console.log('foo ends with b =', b);
}

let a = 42;
a; // 42

foo(a);
// "foo starts with b = 42"
// "foo ends with b = 84"

a; // 42 <-- value of a is not changed

However, consider the case in which we pass an object as a parameter to a function. In this case, the value that is being passed to the function is the reference to the object in memory. Code inside of the function body will then be able to use the object reference to mutate the properties of that object. This can consequently affect the program state outside of the function itself.

function foo(param) {
 console.log('foo starts with param.someProp = ', param.someProp);
  param.someProp = 100;
 console.log('foo ends with param.someProp = ', param.someProp);
}

let myObj = { someProp: 42 };

myObj.someProp; // 42

foo(myObj);
// "foo starts with param.someProp = 42"
// "foo ends with param.someProp = 100"

myObj.someProp; // 100 <-- value of nested property changed

Functions can return a single value using a return statement. If the return keyword is followed by an expression, the result of that expression is the value that will be returned. If the return keyword stands alone, it will return the value undefined. If the function does not have a return statement, it will implicitly return undefined after its entire body has finished execution.

function foo1() {
  // intentionally empty
}

foo1(); // undefined
function foo2() {
  return;
}

foo2(); // undefined
function foo3() {
  return 123;
}

foo3(); // 123

arguments

As we saw earlier, every function defines a set of zero or more formal named parameters, or just parameters. When speaking about the invocation of a function at run-time, we refer to the values passed to it as arguments. It is not required that the number of passed arguments match the number of named parameters – any function may be invoked with zero or more arguments, regardless of its number of named parameters.

If the number of arguments is fewer than the number of named parameters, the values of the missing parameters inside of the invoked function default to undefined.

function foo(a, b, c) {
 console.log(a);
 console.log(b);
 console.log(c);
}

foo(1, 2);
// 1
// 2
// undefined

Inside of every “normal” (i.e. non-arrow) function, we have access to a special variable arguments, which is an array-like object containing all of the arguments passed to the function in its current invocation. Through arguments we can access not only all of the named parameters, but any number of additional arguments that were passed by the caller of the function.

The arguments object is array-like because it can, in some ways, be treated as an array. All of the arguments passed to the function are properties whose keys correspond to their argument index position as an integer. The object has a property length representing the total number of arguments passed. However, it does not have any of the Array functions we typically associate with JavaScript Arrays (objects which inherit from the global built-in Array.prototype).

Consider the code sample below. The resulting values shown in the comments inside of myFunc are specific to its invocation from the last line.

function myFunc(param0, param1) {
  param0; // Object {a: 42, b:84 }
  param1; // true
  arguments; /* Object { length: 5,
                              0: Object{ ... }, 1: true,
                              2: 100, 3: 99, 4: "asdf" } */
  arguments[0] === param0; // true
  arguments[1] === param1; // true
  arguments.length; // 5
  arguments[arguments.length - 1]; // "asdf"
  arguments instanceof Array; // false
}

let arg0 = { a: 42, b: 84 };
let arg2 = 100;

// Invoke myFunc, pass in 5 arguments
myFunc(arg0, true, arg2, 99, 'asdf');

Function Declaration Hoisting

In JavaScript, function declarations undergo a process commonly referred to as hoisting. During compilation, the JavaScript engine will find function declarations and virtually “hoist” them to the beginning of their containing function scope (or the global scope, if not inside of another function). This allows us to invoke named functions anywhere we would normally have access to them through lexical scope, regardless of the order in which the functions are actually declared.

foo(); // "Hello, JavaScript!"

function foo() {
  return `Hello, ${bar()}!`;

  function bar() {
    return 'JavaScript';
  }
}

Rest Parameters

There may be some cases where we want a function to accept an arbitrary number of arguments. For example, array objects have a .push() method that takes any number of arguments and appends each one as a new element to the end of the array.

let arr = [];

arr.push(42);
arr.push('a', 'b', 'c', { b: 4 });

console.log(arr);
// [ 42, 'a', 'b', 'c', { b: 4 } ]

While this can be achieved using the arguments object described above, we can use a newer mechanism called a rest parameter ES2015 to collect spillover arguments into a named array. In the function’s formal parameter list, the last parameter name is prefixed with three periods (...).

function foo(p1, p2, ...others) {
  // When foo is called, after binding the values of p1 and p2,
  // "the rest" of the arguments are placed into a new array

 console.log(p1);
 console.log(p2);
 console.log(`+${others.length} more params:`);
 console.log(others);
 console.log(others instanceof Array);
}

foo(42, 'abc', 'more', 'parameters', 'here');
// 42
// "abc"
// "+3 more params:"
// ["more","parameters","here"]
// true

Unlike the arguments object, a rest parameter is a proper Array instance. If the function is called without enough parameters to flow into the rest parameter, it will simply be an empty array.

// (continued)

foo(42);
// 42
// undefined
// "+0 more params:"
// []
// true

Function Expressions

As opposed to a function declaration, a function expression is any non-declaration, inline function creation (named or anonymous); anywhere the newly-created function object is immediately used as a value, such as being passed as a parameter, returned as a value, or assigned to a variable as shown in the code below (again, pretend that errors are immediately caught).

typeof myFuncDec; // "function"
myFuncDec(); // 42

typeof myFuncExp; // "undefined";
myFuncExp(); // throws TypeError: myFuncExp is not a function

// Function declaration
function myFunDec() {
  return 42;
}

// Function expression
var myFuncExp = function() {
  return 42;
};

typeof myFuncExp; // "function"
myFuncExp(); // 42

Function expressions are not hoisted; they are created at the time of execution. As shown in the example above, a function expression does not have to be given a name; it can simply be declared using the keyword function followed by parentheses (), which may or may not contain formal parameters. Functions without a name are said to be anonymous.

Function expressions can be named, but it does not affect how the function can be invoked from within its outer environment – that is, a named function expression has no effect on the lexical scope in which it is created. The main reason for naming function expressions is to aid in debugging, as the function’s name will appear in stack traces.

var myFunc = function funcName() {
  /* ... */
};

typeof myFunc; // "function"
myFunc.name; // "funcName"

typeof funcName; // "undefined"

Arrow Functions

JavaScript also allows for a more terse type of function expression called arrow functions. ES2015 Arrow functions can be very expressive, and are well-suited to certain functional-style use cases.

The easiest way to explain them is to jump in and look at the syntax.

// A function with no parameters requires parentheses:

() => { /* function body */ };


// Leading parentheses are optional with only one parameter:

(singleParam) => { /* function body */ };

singleParam => { /* function body */ };


// Long function body is enclosed in curly braces:

(param_0, param_1, /* …, */ param_n) => {
  /* function body */
};


// A single-expression function body does not require curly braces,
// and its result is implicitly returned:

(param_0, param_1, /* …, */ param_n) => /* expression */;

                  // equivalent to:  => { return /* expression */; }

Consider the following normal function expression:

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

We can re-write the same function using arrow notation:

let sum = (a, b) => a + b;

sum(3, 4); // 7

These kinds of simple operations are great uses cases for arrow functions. As in the example above, if the body of the function is just one expression, we can simply write out that expression and its result will be implicitly returned.

For more complex functions, the function body can be enclosed in curly brackets.

let mysteryFunc = (a, b) => {
  let c = a + b;
  c *= some_magic_number;
  c = Math.cos(c);
  return c;
};

Arrow functions are inherently anonymous and can only be used in expressions – that is, they can be assigned to variables, as shown earlier, but they cannot be declared or exist “stand-alone” in the same way that normal named function declarations can. Without names, arrow functions can also be a bit harder to debug since their contexts are recorded in stack traces (in thrown errors or in interactive debugger tools) under the name of "(anonymous function)".

Also note that arrow functions do not have their own internal arguments object.

let arrow = () => {
  return typeof arguments;
};

arrow(); // "undefined"

Although it may be true that arrow functions require “less typing”, that is not their intended goal. The real benefits to using this syntax come when we wish to use many simple lambda functions together in a more functional style of programming – we’ll look at some use cases in later chapters where the use of arrow functions can help make our code much more readable.