Functions - Learning JavaScript (2016)

Learning JavaScript (2016)

Chapter 6. Functions

A function is a self-contained collection of statements that run as a single unit: essentially, you can think of it as a subprogram. Functions are central to JavaScript’s power and expressiveness, and this chapter introduces you to their basic usage and mechanics.

Every function has a body; this is the collection of statements that compose the function:

function sayHello() {

// this is the body; it started with an opening curly brace...

console.log("Hello world!");

console.log("¡Hola mundo!");

console.log("Hallo wereld!");

console.log("Привет мир!");

// ...and ends with a closing curly brace

}

This is an example of a function declaration: we’re declaring a function called sayHello. Simply declaring a function does not execute the body: if you try this example, you will not see our multilingual “Hello, World” messages printed to the console. To call a function (also called running,executing, invoking, or dispatching), you use the name of the function followed by parentheses:

sayHello(); // "Hello, World!" printed to the

// console in different languages

NOTE

The terms call, invoke, and execute (as well as run) are interchangeable, and I will use each in this book to get you comfortable with them all. In certain contexts and languages, there may be subtle differences between these terms, but in general use, they are equivalent.

Return Values

Calling a function is an expression, and as we know, expressions resolve to a value. So what value does a function call resolve to? This is where return values come in. In the body of a function, the return keyword will immediately terminate the function and return the specified value, which is what the function call will resolve to. Let’s modify our example; instead of writing to the console, we’ll return a greeting:

function getGreeting() {

return "Hello world!";

}

Now when we call that function, it will resolve to the return value:

getGreeting(); // "Hello, World!"

If you don’t explicitly call return, the return value will be undefined. A function can return any type of value; as a reader’s exercise, try creating a function, getGreetings, to return an array containing “Hello, World” in different languages.

Calling Versus Referencing

In JavaScript, functions are objects, and as such, can be passed around and assigned just like any other object. It’s important to understand the distinction between calling a function and simply referencing it. When you follow a function identifier with parentheses, JavaScript knows that you’re calling it: it executes the body of the function, and the expression resolves to the return value. When you don’t provide the parentheses, you’re simply referring to the function just like any other value, and it’s not invoked. Try the following in a JavaScript console:

getGreeting(); // "Hello, World!"

getGreeting; // function getGreeting()

Being able to reference a function like any other value (without calling it) allows a lot of flexibility in the language. For example, you can assign a function to a variable, which allows you to call the function by another name:

const f = getGreeting;

f(); // "Hello, World!"

Or assign a function to an object property:

const o = {};

o.f = getGreeting;

o.f(); // "Hello, World!"

Or even add a function to an array:

const arr = [1, 2, 3];

arr[1] = getGreeting; // arr is now [1, function getGreeting(), 2]

arr[1](); // "Hello, World!"

This last example should make clear the role of the parentheses: if JavaScript encounters parentheses that follow a value, the value is assumed to be a function, and that function is called. In the preceding example, arr[1] is an expression that resolves to a value. That value is followed by parentheses, which is a signal to JavaScript that the value is a function and should be called.

NOTE

If you try to add parentheses to a value that is not a function, you will get an error. For example, "whoops"() will result in the error TypeError: "whoops" is not a function.

Function Arguments

We’ve now seen how to call functions and get values out of them; how about getting information into them? The primary mechanism to pass information to a function call is function arguments (sometimes called parameters). Arguments are like variables that don’t exist until the function is called. Let’s consider a function that takes two numeric arguments and returns the average of those numbers:

function avg(a, b) {

return (a + b)/2;

}

In this function declaration, a and b are called formal arguments. When a function is called, formal arguments receive values and become actual arguments:

avg(5, 10); // 7.5

In this example, the formal arguments a and b receive the values 5 and 10, and become actual arguments (which are very much like variables, but specific to the function body).

One thing that beginners often stumble over is that the arguments exist only in the function, even if they have the same name as variables outside of the function. Consider:

const a = 5, b = 10;

avg(a, b);

The variables a and b here are separate, distinct variables from the arguments a and b in the function avg, even though they share the same name. When you call a function, the function arguments receive the values that you pass in, not the variables themselves. Consider the following code:

function f(x) {

console.log(`inside f: x=${x}`);

x = 5;

console.log(`inside f: x=${x} (after assignment)`);

}

let x = 3;

console.log(`before calling f: x=${x}`);

f(x);

console.log(`after calling f: x=${x}`);

If you run this example, you will see:

before calling f: x=3

inside f: x=3

inside f: x=5 (after assignment)

after calling f: x=3

The important takeaway here is that assigning a value to x inside the function doesn’t affect the variable x that’s outside the function; that’s because they’re two distinct entities that happen to have the same name.

Whenever we assign to an argument inside a function, there will be no effect on any variables outside of the function. It is, however, possible to modify an object type in a function in such a way that the object itself changes, which will be visible outside of the function:

function f(o) {

o.message = `set in f (previous value: '${o.message}')`;

}

let o = {

message: "initial value"

};

console.log(`before calling f: o.message="${o.message}"`);

f(o);

console.log(`after calling f: o.message="${o.message}"`);

This results in the following output:

before calling f: o.message="initial value"

after calling f: o.message="set in f (previous value: 'initial value')"

In this example, we see that f modified o within the function, and those changes affected the object o outside of the function. This highlights the key difference between primitives and objects. Primitives can’t be modified (we can change the value of a primitive variable, but the primitivevalue itself doesn’t change). Objects, on the other hand, can be modified.

Let’s be clear here: the o inside the function is separate and distinct from the o outside of the function, but they both refer to the same object. We can see that difference again with assignment:

function f(o) {

o.message = "set in f";

o = {

message: "new object!"

};

console.log(`inside f: o.message="${o.message}" (after assignment)`);

}

let o = {

message: 'initial value'

};

console.log(`before calling f: o.message="${o.message}"`);

f(o);

console.log(`after calling f: o.message="${o.message}"`);

If you run this example, you will see:

before calling f: o.message="initial value"

inside f: o.message="new object!" (after assignment)

after calling f: o.message="set in f"

The key to understanding what’s going on here is understanding that the argument o (inside the function) is different than the variable o (outside the function). When f is called, both point to the same object, but when o is assigned inside f, it points to a new, distinct object. The o outside the function still points to the original object.

NOTE

Primitives in JavaScript are considered value types in computer science parlance, because when they are passed around, the value is copied. Objects are called reference types because when they are passed around, both variables refer to the same object (that is, they both hold a reference to the same object).

Do Arguments Make the Function?

In many languages, a function’s signature includes its arguments. For example, in C, f() (no arguments) is a different function than f(x) (one argument), which is a different function than f(x, y) (two arguments). JavaScript makes no such distinction, and when you have a function named f, you can call it with 0 or 1 or 10 arguments, and you’re calling the same function.

The implication of this is that you can call any function with any number of arguments. If you fail to provide arguments, they will implicitly receive the value undefined:

function f(x) {

return `in f: x=${x}`;

}

f(); // "in f: x=undefined"

Later in this chapter, we’ll see how to handle the situation where you pass more arguments than the function defines.

Destructuring Arguments

Just as we can have destructured assignment (see Chapter 5), we can have destructured arguments (arguments are, after all, very much like variable definition). Consider destructuring an object into individual variables:

function getSentence({ subject, verb, object }) {

return `${subject} ${verb} ${object}`;

}

const o = {

subject: "I",

verb: "love",

object: "JavaScript",

};

getSentence(o); // "I love JavaScript"

As with destructuring assignment, property names must be identifier strings, and a variable that doesn’t have a matching property in the incoming object will receive the value undefined.

You can also destructure arrays:

function getSentence([ subject, verb, object ]) {

return `${subject} ${verb} ${object}`;

}

const arr = [ "I", "love", "JavaScript" ];

getSentence(arr); // "I love JavaScript"

Finally, you can use the spread operator (...) to collect any additional arguments:

function addPrefix(prefix, ...words) {

// we will learn a better way to do this later!

const prefixedWords = [];

for(let i=0; i<words.length; i++) {

prefixedWords[i] = prefix + words[i];

}

return prefixedWords;

}

addPrefix("con", "verse", "vex"); // ["converse", "convex"]

Note that if you use the spread operator in a function declaration, it must be the last argument. If you put arguments after it, JavaScript wouldn’t have a reasonable way to distinguish between what should go in the spread argument and what should go in the remaining arguments.

NOTE

In ES5, similar functionality can be accomplished with a special variable that exists only within function bodies: arguments. This variable was not actually an array, but an “array-like” object, which often required special handling, or conversion to a proper array. Spread arguments in ES6 address this weakness, and should be preferred over using the arguments variable (which is still available).

Default Arguments

New in ES6 is the ability to specify default values for arguments. Normally, when values for arguments aren’t provided, they get a value of undefined. Default values allow you to specify some other default value:

function f(a, b = "default", c = 3) {

return `${a} - ${b} - ${c}`;

}

f(5, 6, 7); // "5 - 6 - 7"

f(5, 6); // "5 - 6 - 3"

f(5); // "5 - default - 3"

f(); // "undefined - default - 3"

Functions as Properties of Objects

When a function is a property of an object, it is often called a method to distinguish it from a normal function (we’ll learn a more nuanced distinction between function and method shortly). We already saw in Chapter 3 how we could add a function to an existing object. We can also add a method in the object literal:

const o = {

name: 'Wallace', // primitive property

bark: function() { return 'Woof!'; }, // function property (method)

}

ES6 introduces a new shorthand syntax for methods. The following is functionally equivalent to the preceding example:

const o = {

name: 'Wallace', // primitive property

bark() { return 'Woof!'; }, // function property (method)

}

The this Keyword

Inside a function body, a special read-only value called this is available. This keyword is normally associated with object-oriented programming, and we will be learning more about its use in that context in Chapter 9. However, in JavaScript, it can be used in multiple ways.

Normally, the this keyword relates to functions that are properties of objects. When methods are called, the this keyword takes on the value of the specific object it was called on:

const o = {

name: 'Wallace',

speak() { return `My name is ${this.name}!`; },

}

When we call o.speak(), the this keyword is bound to o:

o.speak(); // "My name is Wallace!

It’s important to understand that this is bound according to how the function is called, not where the function is declared. That is, this is bound to o not because speak is a property of o, but because we called it directly on o (o.speak). Consider what happens if we assign that same function to a variable:

const speak = o.speak;

speak === o.speak; // true; both variables refer to the same function

speak(); // "My name is !"

Because of the way we called the function, JavaScript didn’t know that the function was originally declared in o, so this was bound to undefined.

NOTE

If you call a function in such a way that it’s not clear how to bind this (such as calling the function variable speak as we did previously), what this gets bound to is complex. It depends on whether you’re in strict mode or not, and where the function is being called from. We are intentionally glossing over these details because it’s best to avoid this situation. If you want to know more, see the MDN documentation for code formatting.

The term method is traditionally associated with object-oriented programming, and in this book, we’ll use it to mean a function that is a property of an object and is designed to be called directly from an object instance (such as o.speak()) above. If a function doesn’t use this, we will generally refer to it as a function, no matter where it’s declared.

One detail about the this variable that often trips people up is when you need to access it in a nested function. Consider the following example, where we use a helper function inside a method:

const o = {

name: 'Julie',

greetBackwards: function() {

function getReverseName() {

let nameBackwards = '';

for(let i=this.name.length-1; i>=0; i--) {

nameBackwards += this.name[i];

}

return nameBackwards;

}

return `${getReverseName()} si eman ym ,olleH`;

},

};

o.greetBackwards();

Here we’re using a nested function, getReverseName, to reverse the name. Unfortunately, getReverseName won’t work as we expect it to: when we call o.greetBackwards(), JavaScript binds this as we expect. However, when we call getReverseName from insidegreetBackwards, this will be bound to something else.1 A common solution to this problem is to assign a second variable to this:

const o = {

name: 'Julie',

greetBackwards: function() {

const self = this;

function getReverseName() {

let nameBackwards = '';

for(let i=self.name.length-1; i>=0; i--) {

nameBackwards += self.name[i];

}

return nameBackwards;

}

return `${getReverseName()} si eman ym ,olleH`;

},

};

o.greetBackwards();

This is a common technique, and you will see this get assigned to self or that. Arrow functions, which we’ll see later in this chapter, are another way of addressing this problem.

Function Expressions and Anonymous Functions

So far, we’ve been dealing exclusively with function declarations, which give functions both a body (which defines what the function does) and an identifier (so we can call it later). JavaScript also supports anonymous functions, which don’t necessarily have an identifier.

You might reasonably be wondering what use is a function without an identifier. Without an identifier, how are we to call it? The answer lies in understanding function expressions. We know that an expression is something that evaluates to a value, and we also know that functions are values like any other in JavaScript. A function expression is simply a way to declare a (possibly unnamed) function. A function expression can be assigned to something (thereby giving it an identifier), or called immediately.2

Function expressions are syntactically identical to function declarations except that you can omit the function name. Let’s consider the example where we use a function expression and assign the result to a variable (which is effectively equivalent to a function declaration):

const f = function() {

// ...

};

The outcome is the same as if we had declared the function in the usual way: we’re left with an identifier f that refers to a function. Just as with regular function declaration, we can call the function with f(). The only difference is that we are creating an anonymous function (by using a function expression) and assigning it to a variable.

Anonymous functions are used all the time: as arguments to other functions or methods, or to create function properties in an object. We will see these uses throughout the book.

I said that the function name is optional in a function expression…so what happens when we give the function a name and assign it to a variable (and why would we want to do this)? For example:

const g = function f() {

// ...

}

When a function is created this way, the name g takes priority, and to refer to the function (from outside of the function), we use g; trying to access f will give you an undefined variable error. Taking this into account, why would we want to do this? It can be necessary if we want to refer to the function from within the function itself (called recursion):

const g = function f(stop) {

if(stop) console.log('f stopped');

f(true);

};

g(false);

From inside the function, we use f to reference the function, and from outside we use g. There’s no particularly good reason to give a function two separate names, but we do so here to make it clear how named function expressions work.

Because function declaration and function expressions look identical, you might be wondering how JavaScript tells the two apart (or if there’s even any difference). The answer is context: if the function declaration is used as an expression, it is a function expression, and if it isn’t, it’s a function declaration.

The difference is mostly academic, and you don’t normally have to think about it. When you’re defining a named function that you intend to call later, you’ll probably use a function declaration without thinking about it, and if you need to create a function to assign to something or pass into another function, you’ll use a function expression.

Arrow Notation

ES6 introduces a new and welcome syntax called arrow notation (also called “fat arrow” notation because the arrow uses an equals sign instead of a dash). It is essentially syntactic sugar (with one major functional difference we’ll get to shortly) that reduces the number of times you have to type the word function, as well as the number of curly braces you have to type.

Arrow functions allow you to simplify syntax in three ways:

§ You can omit the word function.

§ If the function takes a single argument, you can omit the parentheses.

§ If the function body is a single expression, you can omit curly braces and the return statement.

Arrow functions are always anonymous. You can still assign them to a variable, but you can’t create a named function like you can with the function keyword.

Consider the following equivalent function expressions:

const f1 = function() { return "hello!"; }

// OR

const f1 = () => "hello!";

const f2 = function(name) { return `Hello, ${name}!`; }

// OR

const f2 = name => `Hello, ${name}!`;

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

// OR

const f3 = (a,b) => a + b;

These examples are a bit contrived; usually, if you need a named function, you would simply use a regular function declaration. Arrow functions are most useful when you’re creating and passing around anonymous functions, which we’ll see quite often starting in Chapter 8.

Arrow functions do have one major difference from regular functions: this is bound lexically, just like any other variable. Recall our greetBackwards example from earlier in the chapter. With an arrow function, we can use this inside the inner function:

const o = {

name: 'Julie',

greetBackwards: function() {

const getReverseName = () => {

let nameBackwards = '';

for(let i=this.name.length-1; i>=0; i--) {

nameBackwards += this.name[i];

}

return nameBackwards;

};

return `${getReverseName()} si eman ym ,olleH`;

},

};

o.greetBackwards();

Arrow functions have two additional minor differences from regular functions: they can’t be used as object constructors (see Chapter 9), and the special arguments variable isn’t available in arrow functions (which is no longer necessary thanks to the spread operator).

call, apply, and bind

We’ve already seen the “normal” way this is bound (which is consistent with other object-oriented languages). However, JavaScript allows you to specify what this is bound to no matter how or where the function in question is called. We’ll start with call, which is a method available on all functions that allows you to call the function with a specific value of this:

const bruce = { name: "Bruce" };

const madeline = { name: "Madeline" };

// this function isn't associated with any object, yet

// it's using 'this'!

function greet() {

return `Hello, I'm ${this.name}!`;

}

greet(); // "Hello, I'm !" - 'this' not bound

greet.call(bruce); // "Hello, I'm Bruce!" - 'this' bound to 'bruce'

greet.call(madeline); // "Hello, I'm Madeline!" - 'this' bound to 'madeline'

You can see that call allows us to call a function as if it were a method by providing it an object to bind this to. The first argument to call is the value you want this bound to, and any remaining arguments become arguments to the function you’re calling:

function update(birthYear, occupation) {

this.birthYear = birthYear;

this.occupation = occupation;

}

update.call(bruce, 1949, 'singer');

// bruce is now { name: "Bruce", birthYear: 1949,

// occupation: "singer" }

update.call(madeline, 1942, 'actress');

// madeline is now { name: "Madeline", birthYear: 1942,

// occupation: "actress" }

apply is identical to call except the way it handles function arguments. call takes arguments directly, just like a normal function. apply takes its arguments as an array:

update.apply(bruce, [1955, "actor"]);

// bruce is now { name: "Bruce", birthYear: 1955,

// occupation: "actor" }

update.apply(madeline, [1918, "writer"]);

// madeline is now { name: "Madeline", birthYear: 1918,

// occupation: "writer" }

apply is useful if you’ve got an array and you want to use its values as arguments to a function. The classic example is finding the minimum or maximum number in an array. The built-in Math.min and Math.max functions take any number of arguments and return the minimum or maximum, respectively. We can use apply to use these functions with an existing array:

const arr = [2, 3, -5, 15, 7];

Math.min.apply(null, arr); // -5

Math.max.apply(null, arr); // 15

Note that we simply pass null in for the value of this. That’s because Math.min and Math.max don’t use this at all; it doesn’t matter what we pass in here.

With the ES6 spread operator (...), we can accomplish the same result as apply. In the instance of our update method, where we do care about the this value, we still have to use call, but for Math.min and Math.max, where it doesn’t matter, we can use the spread operator to call these functions directly:

const newBruce = [1940, "martial artist"];

update.call(bruce, ...newBruce); // equivalent to apply(bruce, newBruce)

Math.min(...arr); // -5

Math.max(...arr); // 15

There’s one final function that allows you to specify the value for this: bind. bind allows you to permanently associate a value for this with a function. Imagine we’re passing our update method around, and we want to make sure that it always gets called with bruce as the value forthis, no matter how it’s called (even with call, apply, or another bind). bind allows us to do that:

const updateBruce = update.bind(bruce);

updateBruce(1904, "actor");

// bruce is now { name: "Bruce", birthYear: 1904, occupation: "actor" }

updateBruce.call(madeline, 1274, "king");

// bruce is now { name: "Bruce", birthYear: 1274, occupation: "king" };

// madeline is unchanged

The fact that the action of bind is permanent makes it potentially a source of difficult-to-find bugs: in essence, you are left with a function that cannot be used effectively with call, apply, or bind (a second time). Imagine passing around a function that gets invoked with call or apply in some distant location, fully expecting this to get bound accordingly. I’m not suggesting that you avoid the use of bind; it’s quite useful, but be mindful of the way bound functions will be used.

You can also provide parameters to bind, which has the effect of creating a new function that’s always invoked with specific parameters. For example, if you wanted an update function that always set the birth year of bruce to 1949, but still allowed you to change the occupation, you could do this:

const updateBruce1949 = update.bind(bruce, 1949);

updateBruce1949("singer, songwriter");

// bruce is now { name: "Bruce", birthYear: 1949,

// occupation: "singer, songwriter" }

Conclusion

Functions are a vital part of JavaScript. They do far more than just modularize code: they allow for the construction of incredibly powerful algorithmic units. This chapter has mainly been about the mechanics of functions—a dry but important introduction. Armed with this foundation, we’ll see the power of functions unlocked in coming chapters.

1It will either get bound to the global object or it will be undefined, depending on whether or not you are in strict mode. We’re not covering all the details here because you should avoid this situation.

2A so-called “immediately invoked function expression” (IIFE), which we will cover in Chapter 7.