This workshop is important because:
Using callback functions is an effective way to write declarative, functional JavaScript. JavaScript was built to deal with asynchronous events; callbacks help appropriately time and coordinate our program.
After this workshop, developers will be able to:
- Pass a function as a callback to another function
- Use iterator methods with callbacks to build more declarative loop structures
- Recognize the best iterator method for a particular use case
- Build iterator methods from scratch
Before this workshop, developers should already be able to:
- Write and call functions in JavaScript
- Explain what a higher order function is
- Use a
for
loop
A callback is a function that is passed into another function as an argument and then used. A function that can take in a callback as an argument is known as a higher order function.
Let's review with an example.
function masterMathFunction(a, b, callback) {
console.log("the inputs are: " + a + " and " + b);
callback(a,b);
}
function multiplyMe(n,m) {
console.log("Product: " + n*m);
}
function raiseMe(n,m) {
console.log("Power: "+ Math.pow(n,m));
}
masterMathFunction(2,3,raiseMe);
masterMathFunction(2,3,multiplyMe);
-
What is the higher order function here?
-
What function(s) may be used as callbacks for the higher order function?
-
What is another possible callback function?
Callbacks allow us to queue up the execution of a function until after some other code completes. They allow for asynchronous behavior, even though JavaScript is a single-threaded language. They also let us customize behaviors inside libraries.
See this awesome video of a talk by Philip Roberts on how JavaScript works.
Let's walk through another example of code that uses callbacks:
var element = document.querySelector("body");
var counter = 0;
element.addEventListener("click", countClicks);
function countClicks(event){
counter += 1;
console.log("clicked " + counter + " times.");
}
Let's run this in the console.
####Anonymous Functions: Review
Often, if a callback will only be used with one higher order function, the callback function definition is written inside the higher order function call.
var element = document.querySelector("body");
var counter = 0;
element.addEventListener("click", function(event){
counter += 1;
console.log("clicked " + counter + " times.");
});
In these cases, the callback often won't be given a name. A function without a name is called an anonymous function.
JavaScript's built-in sort
method for arrays sorts numbers from lowest to highest value and String items as strings, alphabetically.
var arr = [1, 2, 125, 500];
arr.sort();
//=> [1, 125, 2, 500]
Checking the documentation, you should notice there is an optional compareFunction
parameter that can change the sort order rules.
Work in a snippet or in your console. Use JavaScript's sort
function to sort the following objects by price, from lowest to highest:
var items = [
{ name: "trail mix", price: 3.50 },
{ name: "first aid kit", price: 20.00 },
{ name: "water bottle", price: 12.00 },
{ name: "flashlight", price: 8.00 },
{ name: "gps unit", price: 93.00 }
];
Hint: how to start
You'll need to write a custom `compareFunction` and pass it into the `sort` method. Follow the structure of the custom `compareFunction` from the documentation.Answer: the compare function
```js function compareByPrice(item1, item2){ if (item1.price < item2.price) { return -1; } if (item1.price > item2.price) { return 1; } // items must have equal price return 0; } ```Answer: calling `sort` with customized function
```js items.sort(compareByPrice); ```Iteration basically means looping.
var potatoes = ["Yukon Gold", "Russet", "Yellow Finn", "Kestrel"];
for(var i=0; i < potatoes.length; i++){
console.log(potatoes[i] + "!")
}
Iterator methods create more declarative abstractions for common uses of loops.
var potatoes = ["Yukon Gold", "Russet", "Yellow Finn", "Kestrel"];
potatoes.forEach(function(element){
console.log(element + "!")
});
We can combine our knowledge of callbacks & iteration to write better, more declarative code.
We can also illustrate a lot of these with the physical example of a pillbox(array) with items at each index(jelly beans!).
Be thinking about how we could extend this metaphor with each method we discuss here.
The forEach()
method performs whatever callback function you pass into it on each element.
var fruits = ["Apple", "Banana", "Cherry", "Durian", "Elderberry",
"Fig", "Guava", "Huckleberry", "Ice plant", "Jackfruit"];
fruits.forEach(function (value, index) {
console.log(index + ". " + value);
});
// 0. Apple
// 1. Banana
// 2. Cherry
// 3. Durian
// 4. Elderberry
// 5. Fig
// 6. Guava
// 7. Huckleberry
// 8. Ice plant
// 9. Jackfruit
//=> ["Apple", "Banana", "Cherry", "Durian", "Elderberry",
// "Fig", "Guava", "Huckleberry", "Ice plant", "Jackfruit"];
Similar to forEach()
, map()
traverses an array. This method, however performs the callback function you pass into it on each element and then outputs the results inside a new array.
Often we want to do more than just perform an action, like console.log(), on every loop. When we actually want to modify/manipulate our array, map
is the go-to!
####Example: Double every number
var numbers = [1, 4, 9];
var doubles = numbers.map(function doubler(num) {
return num * 2;
});
// doubles is now [2, 8, 18]. numbers is still [1, 4, 9]
####Example: Pluralize all the fruit names
pluralized_fruits = fruits.map(function pluralize(element) {
// if word ends in 'y', remove 'y' and add 'ies' to the end
var lastLetter = element[element.length -1];
if (lastLetter === 'y') {
element = element.slice(0,element.length-1) + 'ie';
}
return element + 's';
});
fruits // ORIGINAL ARRAY IS UNCHANGED!
//=> ["Apple", "Banana", "Cherry", "Durian", "Elderberry",
// "Fig", "Guava", "Huckleberry", "Ice plant", "Jackfruit"];
pluralized_fruits // MAP OUTPUT
//=> [ "Apples", "Bananas", "Cherries", "Durians", "Elderberries",
// "Figs", "Guavas", "Huckleberries", "Ice plants", "Jackfruits" ]
####Example: Square each number
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
numbers.map(function square(element) {
return Math.pow(element, 2);
});
//=> [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Elaine the Etsy Merchant thinks her prices are scaring off customers. Subtracting one penny from every price might help! Use `map` to subtract 1 cent from each of the prices on this receipt's list:
var prices = [3.00, 4.00, 10.00, 2.25, 3.01];
// create a new array with the reduced prices...
var prices = [3.00, 4.00, 10.00, 2.25, 3.01];
// create a new array with the reduced prices...
var prices = [3.00,4.00,10.00,2.25,3.01];
var reducedPrices = prices.map(function(price) {
return price - 0.01;
});
With the filter()
, method you can create a new array filled with elements that pass certain criteria that you designate. This is great for creating a new filtered list of movies that have a certain genre, fruits that start with vowels, even numbers, and so on.
It's important to remember that a filter method on an array requires a boolean
return value for the callback function.
var fruits = ["Apple", "Banana", "Cherry", "Elderberry",
"Fig", "Guava", "Ice plant", "Jackfruit"];
var vowels = ["A", "E", "I", "O", "U"];
function vowelFruit(fruit) {
var result = vowels.indexOf(fruit[0]) >= 0; // indexOf returns -1 if not found
// console.log("result for " + fruit + " is " + result);
return result;
}
var vowelFruits = fruits.filter(vowelFruit);
console.log(vowelFruits);
// ["Apple", "Elderberry", "Ice plant"]
Or alternatively:
var vowels = ["A", "E", "I", "O", "U"];
var vowelFruits = fruits.filter(function vowelFruit(fruit) {
return vowels.indexOf(fruit[0]) >= 0; // indexOf returns -1 if not found
});
console.log(vowelFruits);
// ["Apple", "Elderberry", "Ice plant"]
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
even = numbers.filter(function filterEvens(num) {
var isEven = num%2==0;
var greaterThanFive = num > 5;
return isEven && greaterThanFive;
});
//=> [6, 8, 10]
Is there an interesting trend in birthdays? Do people tend to be born more on even-numbered dates or odd-numbered dates? If so, what's the ratio? In class, let's take a quick poll of the days of the month people were born on. This is a great chance to do some serious science!
var exampleBdays = [1, 1, 2, 3, 3, 3, 5, 5, 6, 6, 8, 8, 10, 10, 12, 12, 13, 13, 15, 17, 17, 18, 20, 20, 26, 31];
// gather an array of all the even birthdays...
var exampleBdays = [1, 1, 2, 3, 3, 3, 5, 5, 6, 6, 8, 8, 10, 10, 12, 12, 13, 13, 15, 17, 17, 18, 20, 20, 26, 31];
// gather an array of all the even birthdays...
var exampleBdays = [1, 1, 2, 3, 3, 3, 5, 5, 6, 6, 8, 8, 10, 10, 12, 12, 13, 13, 15, 17, 17, 18, 20, 20, 26, 31];
var birthDateEvens = exampleBdays.filter(function(birthday) {
return birthday % 2 === 0 ? birthday : false;
});
The reduce()
method is designed to create one single value that is the result of an action performed on all elements in an array. It essentially 'reduces' the values of an array into one single value.
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var sum = numbers.reduce(function add(previous, current) {
return current + previous;
});
//=> 55
In the above examples, notice how the first time the callback is called it receives
element[0]
andelement[1]
.
There is another way to call this function and specify an initial starting value.
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var sum = numbers.reduce(function add(previous, current) {
return current + previous;
}, 100);
//=> 155
In the above example, the first time the callback is called it receives 100
and 1
.
Note: We set the starting value to
100
by passing in an optional extra argument toreduce
.
Roberto has been tracking his test scores over the semester. Use `reduce` to help you find his average test score.
var scores = [85, 78, 92, 90, 98];
var scores = [85, 78, 92, 90, 98];
var scores = [85, 78, 92, 90, 98];
var total = scores.reduce(function(previous, current) {
return previous + current;
});
var average = total/(scores.length);
So how does forEach
work?
Let's think about forEach
again. What's happening behind the scenes?
- What are our inputs?
- What is our output?
- What happens on each loop?
- What does the callback function do?
- What gets passed into our callback function? That is, what are its inputs/parameters?
- Where does the callback come from?
Let's check:
function print(item) {
console.log(item);
}
[0, 100, 200, 300].forEach(function(number) {
print(number);
});
Given the above, how would you build a function that mimics `forEach` yourself? Call it `myForEach`.
function myForEach(collection, callback) {
for(var i = 0; i < collection.length; i++) {
callback(collection[i]);
}
}
// the below should have the same result as the above
myForEach([0, 100, 200, 300], print)
- Callbacks are a very common pattern in JavaScript.
- Iterator methods help us write more conventional, declarative code. Loops (
for
andwhile
) are more imperative and offer greater flexibility (configuration).