Functions may seem simple in JavaScript, but beneath this simplicity lies enormous power. Gaining an understanding of this power is one of the keys to mastering the JavaScript language.
In Lesson 11, you created simple functions and invoked them from the console. For instance:
function isPositive(num) {
return num >= 0;
}
In JavaScript, functions are objects so it is possible to assign them to variables:
f1 = function isPositive(num) {
return num >= 0;
}
If you ask JavaScript the type of f1
, it will respond as follows:
> typeof f1
"function"
This is another example of JavaScript being slightly disingenuous. Functions are not a distinct data-type; they are objects and therefore support all the features you will learn about in the next lesson, such as the ability to invoke methods on them.
Once you have assigned a function to a variable, you can invoke it via its variable name by appending brackets and parameters:
> f1(9)
true
In fact, you can use this variable wherever you can use any other variable in JavaScript; for instance, you can pass it as a parameter to another function.
Consider an example where you want to write a function that counts how many positive numbers are in an array. With the power of functions, you can do this by writing a generic algorithm as follows:
function countForArray(array, condition) {
var result = 0;
for (var i = 0; i < array.length; i++) {
var element = array[i];
if (condition(element)) {
result++;
}
}
return result;
}
This algorithm accepts an array and a function. It then loops through every element in the array and passes it to the function provided. If the function returns true
, the count is incremented by one. You can then call this as follows:
> a = [1,2,-3,2,-5]
> countForArray(a, f1)
3
Notice that you are passing a reference to the function you defined earlier. It may not looked like you have gained much in this example; after all, the countForArray
method could have very easily checked if the number was positive without using the function passed in.
The beauty of countForArray
, however, is that it is a generic function that can be made to behave differently very easily. For instance, if you want to count the negative numbers you can use the following code:
> countForArray(a, function(num) {
return num < 0;
})
2
Notice that in this case you did not even create the function in advance; you simply declared it as part of the call to countForArray
. The function that you have created does not even have a name; it is therefore called an anonymous function. Its scope is limited to the duration of this function call.
Once you have a function such as countForArray
, you can use it for a whole variety of tasks that you may not even have thought about when you originally wrote it.
Functions that are passed to other functions are often called callback functions because they allow another function to “call back” to them at the appropriate time.
JavaScript arrays natively support a number of functions that can be used for performing common operations. For instance, one of the most common operations performed on arrays is to filter out a set of elements that do not meet a set of criteria. For instance, you might want to filter all the negative numbers out of an array, leaving only the positive numbers.
JavaScript arrays provide a filter
method for performing this operation. Just like countForArray
, this passes each element in the array in turn to a function provided, and retains those that return true
:
> a.filter(f1)
[1, 2, 2]
Likewise, you can filter out positive numbers with the following code:
> a.filter(function(num) {
return num < 0;
})
[-3, -5]
You may have noticed that the filter
function is not actually modifying the underlying array; instead, it is returning a new array with the relevant elements filtered out.
Another common operation performed on arrays is to transform each element in some way. With this particular array, you might want to transform the elements so that all the numbers are positive; this can be achieved with the map
function.
The map
function works in exactly the same way: You pass it a function, and the map
function invokes it with each element in the array. The function you provide is responsible for modifying the element in some way and returning the modified version as a result.
The following returns an array of elements where each number has been converted to a positive value:
> a.map(function(num) {
return Math.abs(num);
})
[1, 2, 3, 2, 5]
Because functions such as map
and filter
both operate on arrays and return arrays, it is possible to chain together a whole set of function calls. Imagine that you want to return the absolute value of all even numbers in an array. This can be achieved as follows:
> a1 = [-2,1-3,5,6]
> a.filter(function(num) {
return num%2==0;
}).map(function(num) {
return Math.abs(num);
});
[2, 2, 6]
This example is a bit harder to follow, so start by breaking out its component parts. It starts out by performing a filter
operation:
> a1.filter(function(num) {
return num%2==0;
})
[-2, -2, 6]
It then performs a map
operation on the result: you can simulate this as follows:
> [-2, -2, 6].map(function(num) {
return Math.abs(num);
})
[2, 2, 6]
When writing JavaScript code it is often a good idea to think in terms of simple functions that perform a single task, and do not store or modify any global state. These functions can then be combined together to create more advanced functionality. Building software in this way tends to be simpler because it is very easy to understand, develop, and test each function in isolation.
Closures can be a difficult concept to explain, so I will explain them through examples.
Imagine a case where you want to write a function that can produce unique, incrementing numbers that can be used by other code in your web application. The only condition of this functionality is that if the last call to the function returned 10, the next call must return 11.
It is possible to write this functionality with a global variable:
> count = 0;
> function getNextCount() {
return count++;
}
> getNextCount()
0
> getNextCount()
1
As has already been mentioned, however, global variables should be avoided because any other code can modify them. For instance, any other code could reset the count
variable:
count = -1;
or set it to a nonsense value:
count = 'hello';
These may look like contrived examples, but as web applications grow in size, global variables such as this become the source of difficult to find bugs. Closures provide an alternative.
Before looking at the solution, consider what happens to local variables inside a function when it finishes executing. The function that follows declares a local variable called myCount
.
function counter() {
var myCount = 0;
return myCount++;
}
If you execute this and then attempt to access the myCount
variable, you will find it does not exist:
> counter()
0
> myCount;
ReferenceError: myCount is not defined
The variable is created inside the function each time it is invoked, and it is automatically destroyed when the function completes. This is why the counter
function always returns 0:
> counter()
0
> counter()
0
Now, consider this slight variation on the preceding function:
function getCounter() {
var myCount = 0;
return function() {
return myCount++;
}
}
Rather than returning a number, this function returns another function. The function that it returns has the following body:
function() {
return myCount++;
}
You can now assign a variable to refer to this function:
counter = getCounter();
There is something strange about this function though: It is referring to the local variable myCount
that was defined inside the getCounter
function. Based on my previous explanation, this should have been destroyed when the call to getCounter
finished. Therefore you might expect that if you invoke the function returned by getCounter
, it will fail.
Not only does it not fail, it gives you exactly the behavior you want:
> counter();
0
> counter();
1
The anonymous function created inside getCounter
is referred to as a closure. When it is created, it “closes” over all the variables in scope at the time, and obtains a reference to them. When the call to getCounter
finished, therefore, JavaScript recognized that the anonymous function still might need to use the myCount
variable and did not destroy it.
Although the anonymous function can continue to use the myCount
variable, it is completely hidden from all other code. This means that it is not possible for any other code to interfere with the value of this variable:
> myCount = 10;
10
> counter()
2
The preceding code created a global variable called myCount
, but this does not have any impact on your counter, which continues to use the local variable of the same name.
In addition, if you were to create a second counter, it will have its own local myCount
variable that will not impact your original counter. Instead, the new counter will also start counting from 0.
The beauty of this solution is that you have created private data. The function performing the counting is using a variable that only it has access to. This is an important technique in JavaScript because it does not support many of the mechanisms found in other languages for creating private data.
One interesting feature in JavaScript is the scope of variables inside functions. In most programming languages it is possible to declare variables within a sub-block (a loop for instance) and limit their scope to this block. Consider the following example:
function iterate(array) {
var count = 0;
for (var i = 0; i < array.length; i++) {
var count = 10;
}
return count;
}
If you were to invoke this function with an array, it would always return 10 because the count
variable declared inside the for
loop overwrites the count
variable declared before the loop.
Part of the reason JavaScript operates in this manner is a concept called hoisting. Although it is possible to declare variables anywhere in a function, when JavaScript executes a function, it first searches for all the local variables in it and moves their declaration to the top of the function. They are, however, left undefined until they are explicitly given a value in the body of the function. In order to demonstrate this, create the following function:
function testHoisting() {
var num = num1 + num2;
var num1 = 10;
var num2 = 10;
return num;
}
If you call this function, you will notice it does not fail, even though it is using local variables before they are defined:
> testHoisting()
NaN
If you tried the same thing with global variables, however, the code will fail because global variables are not hoisted:
> function testHoisting() {
var num = num1 + num2;
num1 = 10;
num2 = 10;
return num;
}
> testHoisting()
ReferenceError: num1 is not defined
As discussed many times, JavaScript functions can accept parameters. When you invoke a function, however, the number of arguments you pass does not need to be constrained by the number of parameters defined.
For instance, you can pass a single argument to a function accepting two parameters. In this case the second parameter will have a value of undefined
.
Likewise, you can pass three arguments to a function accepting two parameters. This may not sound useful, but in fact JavaScript makes these arguments available in a special array called arguments
.
Consider a case where you want to write a function to add together an arbitrary set of numbers. Obviously, you could pass an array to the function, but you can also write it as follows:
function add() {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result = result + arguments[i];
}
return result;
}
Notice that this function declares no parameters: Instead, it uses the arguments
array to extract the arguments passed to it. It is now possible to call this function with an arbitrary number of arguments:
> add(3,7,8,10)
28
Since JavaScript functions are actually objects, it is possible to invoke methods on them. This section looks at a widely used method called bind
.
You have already seen how functions “close” over all variables in scope when they are created. This set of variables can be thought of as the environment in which the function executes.
JavaScript is even more powerful than this: It is possible to provide the environment to the function in the form of an object, therefore allowing the function to use an entirely different set of variables.
Imagine that you want to create a counter that is capable of starting from any number, not just 0. One way to achieve this is to create the function as follows:
function getCount() {
return this.myCount++;
}
Notice in this case that you have not provided a starting value for myCount
, and you are accessing the variable with this invoke getCount().myCount
rather than just myCount
. You will look at the meaning of this
in the next lesson.
If you were to create a counter function, it would not work because it does not have a value for myCount
.
You can instead bind this function to a new environment by providing a set of name/value pairs for the variables in the environment:
> var counter2 = getCount.bind({myCount:100});
undefined
> counter2()
100
> counter2()
101
As you can see, the bind
function returns a new version of the function, permanently bound to the new environment. You can then invoke this function and obtain the appropriate results for the environment it is bound to.
In this Try It, you will start by writing a function that takes advantage of the techniques you have learned in this lesson. This function will implement a stand-alone version of the map
method.
Next, you will look at another of the methods provided by arrays called reduce
. This can be used to aggregate the values in an array—for instance, to sum them.
In order to complete this lesson, you will need the Chrome web browser. You may, however, want to complete these exercises in a text editor and copy the results to the console.
map
that accepts two parameters: an array and a function for performing the map operation.for
loop to iterate through all the elements in the array. Remember to use a counter variable and declare that the loop should continue while this counter is less than the length of the array.for
loop, extract the element that is at the position of the counter. Store this in a local variable.push
method—for example, result.push(value)
.map
function you created earlier with the array, and the variable referring to the function to convert numbers to even. The result should be an array of even numbers.In the second section of this Try It, you will look at the reduce
function. This is similar to map
and filter
, but slightly more complex. This function is used to aggregate the data in an array to a single value such as a sum or an average (the single value can also be an object or an array if required). This function is more complex because it needs to keep track of a running total as it executes.
addToTotal
and will accept two parameters, a current total and a new value to add to this total.addToTotal
so you can see what is happening: print both parameters to the console.reduce
method on the array. This accepts two arguments, a function and an initial value for the aggregation: therefore pass in addToTotal
and 0.addToTotal
, you should only include its name; you should not call it with a set of parameters. [6, 2, 3].reduce( addToTotal, 0);
Current total:
Value: 6
-----------------
Current total:
Value: 2
-----------------
Current total:
Value: 3
-----------------
11
3.143.254.90