Functions - The Principles of Object-Oriented Javascript (2014)

The Principles of Object-Oriented Javascript (2014)

Chapter 2. Functions

As discussed in Chapter 1, functions are actually objects in JavaScript. The defining characteristic of a function—what distinguishes it from any other object—is the presence of an internal property named [[Call]]. Internal properties are not accessible via code but rather define the behavior of code as it executes. ECMAScript defines multiple internal properties for objects in JavaScript, and these internal properties are indicated by double-square-bracket notation.

The [[Call]] property is unique to functions and indicates that the object can be executed. Because only functions have this property, the typeof operator is defined by ECMAScript to return "function" for any object with a [[Call]] property. That led to some confusion in the past, because some browsers also included a [[Call]] property for regular expressions, which were thus incorrectly identified as functions. All browsers now behave the same, so typeof no longer identifies regular expressions as functions.

This chapter discusses the various ways that functions are defined and executed in JavaScript. Because functions are objects, they behave differently than functions in other languages, and this behavior is central to a good understanding of JavaScript.

Declarations vs. Expressions

There are actually two literal forms of functions. The first is a function declaration, which begins with the function keyword and includes the name of the function immediately following it. The contents of the function are enclosed in braces, as shown in this declaration:

function add(num1, num2) {

return num1 + num2;

}

The second form is a function expression, which doesn’t require a name after function. These functions are considered anonymous because the function object itself has no name. Instead, function expressions are typically referenced via a variable or property, as in this expression:

var add = function(num1, num2) {

return num1 + num2;

};

This code actually assigns a function value to the variable add. The function expression is almost identical to the function declaration except for the missing name and the semicolon at the end. Assignment expressions typically end with a semicolon, just as if you were assigning any other value.

Although these two forms are quite similar, they differ in a very important way. Function declarations are hoisted to the top of the context (either the function in which the declaration occurs or the global scope) when the code is executed. That means you can actually define a function after it is used in code without generating an error. For example:

var result = add(5, 5);

function add(num1, num2) {

return num1 + num2;

}

This code might look like it will cause an error, but it works just fine. That’s because the JavaScript engine hoists the function declaration to the top and actually executes the code as if it were written like this:

// how the JavaScript engine interprets the code

function add(num1, num2) {

return num1 + num2;

}

var result = add(5, 5);

Function hoisting happens only for function declarations because the function name is known ahead of time. Function expressions, on the other hand, cannot be hoisted because the functions can be referenced only through a variable. So this code causes an error:

// error!

var result = add(5, 5);

var add = function(num1, num2) {

return num1 + num2;

};

As long as you always define functions before using them, you can use either function declarations or function expressions.

Functions as Values

Because JavaScript has first-class functions, you can use them just as you do any other objects. You can assign them to variables, add them to objects, pass them to other functions as arguments, and return them from functions. Basically, you can use a function anywhere you would use any other reference value. This makes JavaScript functions incredibly powerful. Consider the following example:

function sayHi() {

console.log("Hi!");

}

sayHi(); // outputs "Hi!"

var sayHi2 = sayHi;

sayHi2(); // outputs "Hi!"

In this code, there is a function declaration for sayHi ❶. A variable named sayHi2 is then created and assigned the value of sayHi ❷. Both sayHi and sayHi2 are now pointing to the same function, and that means either can be executed, with the same result. To understand why this happens, take a look at the same code rewritten to use the Function constructor:

var sayHi = new Function("console.log(\"Hi!\");");

sayHi(); // outputs "Hi!"

var sayHi2 = sayHi;

sayHi2(); // outputs "Hi!"

The Function constructor makes it more explicit that sayHi can be passed around just like any other object. When you keep in mind that functions are objects, a lot of the behavior starts to make sense.

For instance, you can pass a function into another function as an argument. The sort() method on JavaScript arrays accepts a comparison function as an optional parameter. The comparison function is called whenever two values in the array must be compared. If the first value is smaller than the second, the comparison function must return a negative number. If the first value is larger than the second, the function must return a positive number. If the two values are equal, the function should return zero.

By default, sort() converts every item in an array to a string and then performs a comparison. That means you can’t accurately sort an array of numbers without specifying a comparison function. For example, you need to include a comparison function to accurately sort an array of numbers, such as:

var numbers = [ 1, 5, 8, 4, 7, 10, 2, 6 ];

❶ numbers.sort(function(first, second) {

return first - second;

});

console.log(numbers); // "[1, 2, 4, 5, 6, 7, 8, 10]"

❷ numbers.sort();

console.log(numbers); // "[1, 10, 2, 4, 5, 6, 7, 8]"

In this example, the comparison function ❶ that is passed into sort() is actually a function expression. Note that there is no name for the function; it exists only as a reference that is passed into another function (making it an anonymous function). Subtracting the two values returns the correct result from the comparison function.

Compare that to the second call to sort() ❷, which does not use a comparison function. The order of the array is different than expected, as 1 is followed by 10. This is because the default comparison converts all values to strings before comparing them.

Parameters

Another unique aspect of JavaScript functions is that you can pass any number of parameters to any function without causing an error. That’s because function parameters are actually stored as an array-like structure called arguments. Just like a regular JavaScript array, arguments can grow to contain any number of values. The values are referenced via numeric indices, and there is a length property to determine how many values are present.

The arguments object is automatically available inside any function. This means named parameters in a function exist mostly for convenience and don’t actually limit the number of arguments that a function can accept.

NOTE

The arguments object is not an instance of Array and therefore doesn’t have the same methods as an array; Array.isArray(arguments) always returns false.

On the other hand, JavaScript doesn’t ignore the named parameters of a function either. The number of arguments a function expects is stored on the function’s length property. Remember, a function is actually just an object, so it can have properties. The length property indicates the function’s arity, or the number of parameters it expects. Knowing the function’s arity is important in JavaScript because functions won’t throw an error if you pass in too many or too few parameters.

Here’s a simple example using arguments and function arity; note that the number of arguments passed to the function has no effect on the reported arity:

function reflect(value) {

return value;

}

console.log(reflect("Hi!")); // "Hi!"

console.log(reflect("Hi!", 25)); // "Hi!"

console.log(reflect.length); // 1

reflect = function() {

return arguments[0];

};

console.log(reflect("Hi!")); // "Hi!"

console.log(reflect("Hi!", 25)); // "Hi!"

console.log(reflect.length); // 0

This example first defines the reflect() function using a single named parameter, but there is no error when a second parameter is passed into the function. Also, the length property is 1 because there is a single named parameter. The reflect() function is then redefined with no named parameters; it returns arguments[0], which is the first argument that is passed in. This new version of the function works exactly the same as the previous version, but its length is 0.

The first implementation of reflect() is much easier to understand because it uses a named argument (as you would in other languages). The version that uses the arguments object can be confusing because there are no named arguments, and you must read the body of the function to determine if arguments are used. That is why many developers prefer to avoid using arguments unless necessary.

Sometimes, however, using arguments is actually more effective than naming parameters. For instance, suppose you want to create a function that accepts any number of parameters and returns their sum. You can’t use named parameters because you don’t know how many you will need, so in this case, using arguments is the best option.

function sum() {

var result = 0,

i = 0,

len = arguments.length;

while (i < len) {

result += arguments[i];

i++;

}

return result;

}

console.log(sum(1, 2)); // 3

console.log(sum(3, 4, 5, 6)); // 18

console.log(sum(50)); // 50

console.log(sum()); // 0

The sum() function accepts any number of parameters and adds them together by iterating over the values in arguments with a while loop. This is exactly the same as if you had to add together an array of numbers. The function even works when no parameters are passed in, becauseresult is initialized with a value of 0.

Overloading

Most object-oriented languages support function overloading, which is the ability of a single function to have multiple signatures. A function signature is made up of the function name plus the number and type of parameters the function expects. Thus, a single function can have one signature that accepts a single string argument and another that accepts two numeric arguments. The language determines which version of a function to call based on the arguments that are passed in.

As mentioned previously, JavaScript functions can accept any number of parameters, and the types of parameters a function takes aren’t specified at all. That means JavaScript functions don’t actually have signatures. A lack of function signatures also means a lack of function overloading. Look at what happens when you try to declare two functions with the same name:

function sayMessage(message) {

console.log(message);

}

function sayMessage() {

console.log("Default message");

}

sayMessage("Hello!"); // outputs "Default message"

If this were another language, the output of sayMessage("Hello!") would likely be "Hello!". In JavaScript, however, when you define multiple functions with the same name, the one that appears last in your code wins. The earlier function declarations are completely removed, and the last is the one that is used. Once again, it helps to think about this situation using objects:

var sayMessage = new Function("message", "console.log(message);");

sayMessage = new Function("console.log(\"Default message\");");

sayMessage("Hello!"); // outputs "Default message"

Looking at the code this way makes it clear why the previous code didn’t work. A function object is being assigned to sayMessage twice in a row, so it makes sense that the first function object would be lost.

The fact that functions don’t have signatures in JavaScript doesn’t mean you can’t mimic function overloading. You can retrieve the number of parameters that were passed in by using the arguments object, and you can use that information to determine what to do. For example:

function sayMessage(message) {

if (arguments.length === 0) {

message = "Default message";

}

console.log(message);

}

sayMessage("Hello!"); // outputs "Hello!"

In this example, the sayMessage() function behaves differently based on the number of parameters that were passed in. If no parameters are passed in (arguments.length === 0), then a default message is used. Otherwise, the first parameter is used as the message. This is a little more involved than function overloading in other languages, but the end result is the same. If you really want to check for different data types, you can use typeof and instanceof.

NOTE

In practice, checking the named parameter against undefined is more common than relying on arguments.length.

Object Methods

As mentioned in Chapter 1, you can add and remove properties from objects at any time. When a property value is actually a function, the property is considered a method. You can add a method to an object in the same way that you would add a property. For example, in the following code, the person variable is assigned an object literal with a name property and a method called sayName.

var person = {

name: "Nicholas",

sayName: function() {

console.log(person.name);

}

};

person.sayName(); // outputs "Nicholas"

Note that the syntax for a data property and a method is exactly the same—an identifier followed by a colon and the value. In the case of sayName, the value just happens to be a function. You can then call the method directly from the object as in person.sayName("Nicholas").

The this Object

You may have noticed something strange in the previous example. The sayName() method references person.name directly, which creates tight coupling between the method and the object. This is problematic for a number of reasons. First, if you change the variable name, you also need to remember to change the reference to that name in the method. Second, this sort of tight coupling makes it difficult to use the same function for different objects. Fortunately, JavaScript has a way around this issue.

Every scope in JavaScript has a this object that represents the calling object for the function. In the global scope, this represents the global object (window in web browsers). When a function is called while attached to an object, the value of this is equal to that object by default. So, instead of directly referencing an object inside a method, you can reference this instead. For example, you can rewrite the code from the previous example to use this:

var person = {

name: "Nicholas",

sayName: function() {

console.log(this.name);

}

};

person.sayName(); // outputs "Nicholas"

This code works the same as the earlier version, but this time, sayName() references this instead of person. That means you can easily change the name of the variable or even reuse the function on different objects.

function sayNameForAll() {

console.log(this.name);

}

var person1 = {

name: "Nicholas",

sayName: sayNameForAll

};

var person2 = {

name: "Greg",

sayName: sayNameForAll

};

var name = "Michael";

person1.sayName(); // outputs "Nicholas"

person2.sayName(); // outputs "Greg"

sayNameForAll(); // outputs "Michael"

In this example, a function called sayName is defined first. Then, two object literals are created that assign sayName to be equal to the sayNameForAll function. Functions are just reference values, so you can assign them as property values on any number of objects. When sayName() is called on person1, it outputs "Nicholas"; when called on person2, it outputs "Greg". That’s because this is set when the function is called, so this.name is accurate.

The last part of this example defines a global variable called name. When sayNameForAll() is called directly, it outputs "Michael" because the global variable is considered a property of the global object.

Changing this

The ability to use and manipulate the this value of functions is key to good object-oriented programming in JavaScript. Functions can be used in many different contexts, and they need to be able to work in each situation. Even though this is typically assigned automatically, you can change its value to achieve different goals. There are three function methods that allow you to change the value of this. (Remember that functions are objects, and objects can have methods, so functions can, too.)

The call() Method

The first function method for manipulating this is call(), which executes the function with a particular this value and with specific parameters. The first parameter of call() is the value to which this should be equal when the function is executed. All subsequent parameters are the parameters that should be passed into the function. For example, suppose you update sayNameForAll() to take a parameter:

function sayNameForAll(label) {

console.log(label + ":" + this.name);

}

var person1 = {

name: "Nicholas"

};

var person2 = {

name: "Greg"

};

var name = "Michael";

sayNameForAll.call(this, "global"); // outputs "global:Michael"

sayNameForAll.call(person1, "person1"); // outputs "person1:Nicholas"

sayNameForAll.call(person2, "person2"); // outputs "person2:Greg"

In this example, sayNameForAll() accepts one parameter that is used as a label to the output value. The function is then called three times. Notice that there are no parentheses after the function name because it is accessed as an object rather than as code to execute. The first function call uses the global this and passes in the parameter "global" to output "global:Michael". The same function is called two more times, once each for person1 and person2. Because the call() method is being used, you don’t need to add the function directly onto each object—you explicitly specify the value of this instead of letting the JavaScript engine do it automatically.

The apply() Method

The second function method you can use to manipulate this is apply(). The apply() method works exactly the same as call() except that it accepts only two parameters: the value for this and an array or array-like object of parameters to pass to the function (that means you can use an arguments object as the second parameter). So, instead of individually naming each parameter using call(), you can easily pass arrays to apply() as the second argument. Otherwise, call() and apply() behave identically. This example shows the apply() method in action:

function sayNameForAll(label) {

console.log(label + ":" + this.name);

}

var person1 = {

name: "Nicholas"

};

var person2 = {

name: "Greg"

};

var name = "Michael";

sayNameForAll.apply(this, ["global"]); // outputs "global:Michael"

sayNameForAll.apply(person1, ["person1"]); // outputs "person1:Nicholas"

sayNameForAll.apply(person2, ["person2"]); // outputs "person2:Greg"

This code takes the previous example and replaces call() with apply(); the result is exactly the same. The method you use typically depends on the type of data you have. If you already have an array of data, use apply(); if you just have individual variables, use call().

The bind() Method

The third function method for changing this is bind(). This method was added in ECMAScript 5, and it behaves quite differently than the other two. The first argument to bind() is the this value for the new function. All other arguments represent named parameters that should be permanently set in the new function. You can still pass in any parameters that aren’t permanently set later.

The following code shows two examples that use bind(). You create the sayNameForPerson1() function by binding the this value to person1, while sayNameForPerson2() binds this to person2 and binds the first parameter as "person2".

function sayNameForAll(label) {

console.log(label + ":" + this.name);

}

var person1 = {

name: "Nicholas"

};

var person2 = {

name: "Greg"

};

// create a function just for person1

var sayNameForPerson1 = sayNameForAll.bind(person1);

sayNameForPerson1("person1"); // outputs "person1:Nicholas"

// create a function just for person2

var sayNameForPerson2 = sayNameForAll.bind(person2, "person2");

sayNameForPerson2(); // outputs "person2:Greg"

// attaching a method to an object doesn't change 'this'

❸ person2.sayName = sayNameForPerson1;

person2.sayName("person2"); // outputs "person2:Nicholas"

No parameters are bound for sayNameForPerson1() ❶, so you still need to pass in the label for the output. The function sayNameForPerson2() not only binds this to person2 but also binds the first parameter as "person2" ❷. That means you can call sayNameForPerson2()without passing in any additional arguments. The last part of this example adds sayNameForPerson1() onto person2 with the name sayName ❸. The function is bound, so the value of this doesn’t change even though sayNameForPerson1 is now a function on person2. The method still outputs the value of person1.name.

Summary

JavaScript functions are unique in that they are also objects, meaning they can be accessed, copied, overwritten, and generally treated just like any other object value. The biggest difference between a JavaScript function and other objects is a special internal property, [[Call]], which contains the execution instructions for the function. The typeof operator looks for this internal property on an object, and if it finds it, returns "function".

There are two function literal forms: declarations and expressions. Function declarations contain the function name to the right of the function keyword and are hoisted to the top of the context in which they are defined. Function expressions are used where other values can also be used, such as assignment expressions, function parameters, or the return value of another function.

Because functions are objects, there is a Function constructor. You can create new functions with the Function constructor, but this isn’t generally recommended because it can make your code harder to understand and debugging much more difficult. That said, you will likely run into its usage from time to time in situations where the true form of the function isn’t known until runtime.

You need a good grasp of functions to understand how object- oriented programming works in JavaScript. Because JavaScript has no concept of a class, functions and other objects are all you have to work with to achieve aggregation and inheritance.