2-5 Functions as objects and callbacks

Defining function objects

Functions in JavaScript are often described as first-class values. (In fact, functions are objects, with their own properties and methods.) This means that, in many cases, if you can do something on numbers or strings, you can also do the same on functions. In particular, you can save a function in a variable, pass a function to another function, or return a function from a function.

There are three ways to define a function: function declaration, function expression and arrow function. We’ve introduced function declaration in section 2-2. (For more info, read reference.) The function name in a function declaration is a variable that refers to a function object. You can assign a function object to another variable, and call the function through that variable.

function add (a,b) { return a+b; }
add(1,2); // returns 3
console.log(add);
dir(add);  // shows the structure of the function object

// assign another name to the function
let f = add;
f(1,2); // returns 3. this is the same as add(1,2)
// dir(f)

The second way to define a function is function expression. The syntax of function expression is similar to that of function declaration, but function expressions appear in locations that the JavaScript language would expect a value, for example, the right side of an assignment, argument to a function call, and value in a JavaScript data structure (more about this in the next section.) Usually, we don’t provide a name in a function expression, and therefore sometimes it’s called an anonymous function.

Technical note: It’s possible to provide a name for function expression. See reference for its use cases.

// define a function object, and assign it to a variable
let f = function (a,b) { return a+b }
f(1,2); // returns 3

// define a function object, and call it immediately
(function (a,b) { return a+b })(1,2); // returns 3

// define four functions for the arithmetic operators +, -, * and /. 
// store them in an array
let op = [
  function (a,b) { return a+b },
  function (a,b) { return a-b },
  function (a,b) { return a*b },
  function (a,b) { return a/b }
];
// calculate 1+2*3
op[0](1, op[2](2,3)); // return 1+2*3

A recent update of JavaScript introduces a new way to define function objects, known as arrow functions. Similar to function expression, an arrow function produces a function object. However, arrow functions support a more concise syntax, and allow some further shorthand notation to write simple functions.

Technical note: there is an important difference between arrow functions and function expressions on the handling of this, which we’ll explain in the next section. See mozilla reference for more differences between arrow functions and function expressions.

// a function expression
let f1 = function (a,b) { return a+b; } ;
// the 'same' function, written as an arrow function
let f2 = (a,b) => { return a+b; } ;

// when the function body contains only a return statement, 
// you only need to write the function return value after the fat arrow
let f3 = (a,b) => a+b;

// when there is only 1 parameter, you can omit the parenthesis
let f4 = a => a+1;

Function objects as parameters and return values

Passing a function object to another function is possible. Such usage is sometimes called callback: a programmer defines a callback function , but doesn’t write code to call the callback directly. Instead, some other code will call this function back. Callback can change the behavior of another function.

// use a function op(x,y) to combine 3 values
function combine (op, a, b, c) {
  const t = op(a,b);
  const ans = op(t,c);
  return ans;
}

// define an anonymous function, and save it in the const 'add'
const add = function(a,b) { return a+b; }
combine(add, 6, 7, 8); // returns 21

// define an arrow function for 'mul'
const mul = (a,b) => a*b; 
combine(mul, 6, 7, 8); // returns 336

It is also possible to generate a function object, and return it from a function. In the following example, the function createRandomIntegerGenerator defines an arrow function that calculates a random integer in the range [min, max]. Notice that this arrow function can access the local variables min and max from its outer function. This is a useful feature of JavaScript called closure.

// min and max are integers, min < max 
// this function returns a function that returns a random integer between min and max inclusively
function createRandomIntegerGenerator(min, max) {
  return () => Math.floor(Math.random() * (max - min + 1)) + min;
}

let tossDice = createRandomIntegerGenerator(1,6);
tossDice(); // return a random number from 1,2,3,4,5,6
tossDice(); // return a random number from 1,2,3,4,5,6

Map and filter

A common usage of callback functions is manipulation of a collection of data in containers like Array and Map (to be discussed later). In this sections, we demonstrate using several methods from Array objects, namely map, filter, reduce and sort.

Consider the problem of performing some calculation on each element in an array, and collect the results as a new array.

const a = [1/2, 1/3, 1/4, 1/5, 1/6];
let b = [];
// round the numbers to 2 decimal places
for (let n of a) {
  let n2 = Math.round(n*100) / 100;
  b.push(n2);
}
// print a and b

This is a common scenario in programming. Therefore, the Array objects provide built-in methods to iterate their content. One of the simplest is .map(), which accepts a callback function as a parameter. The callback function takes one input, performs some processing, and returns the result. .map() collects these results into an output array.

function round2(n) { return Math.round(n*100) / 100; }

const a = [1/2, 1/3, 1/4, 1/5, 1/6];
let b = a.map(round2);

The callback function round2 is not called directly by your code. Instead, after definition, you pass the callback function to someone else’s code, and their code will call back your function later.

const a = [1/2, 1/3, 1/4, 1/5, 1/6];

// using an anonymous function expression as callback
let b = a.map(function(n) {
   return Math.round(n*100) / 100;
});

// using an arrow function as callback
let c = a.map(n => Math.round(n*100) / 100);

Another way to look at this is that a callback function can customize / modify the functionality of an existing function. For example, to calculate areas of rectangles in an array, we can write another callback function and pass it to .map(). This allows us to simplify an explicit for loop iteration …

// an array of objects, each one is a rectangle with given width and height
const rects = [ { w: 4, h: 5 }, { w: 2, h: 3 } ];
let areas = [];
// calculate the area of the rectangles
for (let k=0; k<rects.length; k++) {
  let area = rects[k].w * rects[k].h;
  areas.push(area);
}

… to a map method call with callback function.

// using function expression
let areas = rects.map(function (r) {
  return r.w * r.h;
});
// or using arrow function
let areas = rects.map(r => r.w * r.h);

In addition to .map(), another popular iteration function in Array is .filter(). This function iterates the content of an array, and runs the callback function to determine whether to keep or discard each element. filter returns the selection as a new array. It does not modify the source array.

let emo = ['joy', 'sadness', 'anger', 'disgust', 'fear'];
let emo2 = [];
for (let s of emo) {
  if (s.length<5) emo2.push(s);
}

// same as the for loop above
// using function expression
emo2 = emo.filter(function(s) { return (s.length<5); });
// using arrow function
emo2 = emo.filter(s => (s.length<5));

Sorting

JavaScript arrays have a built-in sort method. It can sort numbers and strings, but it does not know how to sort other objects.

let a = [5, 4, 1, 2, 3];
a.sort();  // a becomes [1,2,3,4,5]
let b = ['apple', 'orange', 'banana'];
b.sort(); // b becomes ['apple', 'banana', 'orange']

let p = [
  { name: 'Peter', age: 10 }, { name: 'Mary', age: 9 },
  { name: 'John', age: 11 } ];
p.sort(); // sort by name? by age?

You can teach .sort() how to compare two objects with a callback function.

let p = [
  { name: 'Peter', age: 10 }, { name: 'Mary', age: 9 },
  { name: 'John', age: 11 } ];

// compare two persons by age
function cmp (p1, p2) {
  if (p1.age<p2.age) return -1;  // p1 should be put before p2
  if (p1.age==p2.age) return 0;  // order not important
  return 1;                      // p1 should be put after p2
}

// sort by age
p.sort(cmp);

Exercise

  1. Given an array let N = [ 32, 53, 42, 25, 48, 10 ], write a program to do the following. You should use callback functions. (sample answer)
  • Obtain an array of hexadecimal representation of the numbers in N. (Hints: use n.toString(16) to convert a number n to hexadecimal)
  • Obtain an array of the numbers in N between 20 and 40 inclusively
  • Count how many of the numbers in N are odd
  • Calculate the sum of the numbers in N. Try to use .reduce() in Array. (online reference)
  1. The variable comp312 is an array that contains records of student marks in the comp312 class. Each student record has three fields: name, test and exam. Write programs to do the following. Use an Array iteration method (i.e. .map(), .filter(), .reduce(), etc) at least once in each case. (sample answer)
    let comp312 = [
      { name: 'Peter', test: 80, exam: 70 },
      { name: 'John', test: 60, exam: 65 },
      { name: 'Mary', test: 90, exam: 85 },
      { name: 'Christine', test: 70, exam: 76 }
    ];
    
    • Get a list of student names in the comp312 class
    • Get a list of student record with test mark >= 75
    • Get a list of student name whose test mark is greater than exam mark
    • Assume final mark = 0.6 * test + 0.4 * exam. Make an array with records showing the final mark and name for each student.
    • Sort the list in descending order of exam mark
    • Show the student record with the highest exam mark
    • Calculate the average test mark