Practice Questions

1. With an example, explain what it means for functions to be “first-class values” in JavaScript.

Solution

First-class values can be stored in variables, passed as arguments to other functions, and returned from functions. In JavaScript, functions are first-class values, meaning they can be treated like any other data.

// Storing a function in a variable
const greet = function (name) {
  return `Hello, ${name}!`;
};

// Passing a function as an argument
function callTwice(fn, value) {
  console.log(fn(value));
  console.log(fn(value));
}

callTwice(greet, "Alice");
// Output: Hello, Alice!
// Output: Hello, Alice!

2. What is the output of the following code? Explain why.

function greet(name) {
  return `Hello, ${name}!`;
}

console.log(greet);
console.log(greet("Sam"));
Solution

Output:

[Function: greet]
Hello, Sam!

In Node.js, console.log(greet) often displays something like [Function: greet]. In a browser console, it may display the function’s source instead. The key point is that you are logging the function value itself.

The first console.log(greet) prints a representation of the function itself because we are referencing the function without calling it (no parentheses). The function is treated as a value.

The second console.log(greet("Sam")) calls the function with the argument "Sam", so it executes and returns the string "Hello, Sam!", which is then logged.

The key distinction is:

  • greet references the function as a value.
  • greet("Sam") invokes the function and evaluates to its return value.

3. Provide any two interesting facts about default parameters in JavaScript.

Solution

Any two of the following:

  • Default parameters are only used when an argument is undefined (either missing or explicitly passed as undefined).
  • Default values can reference earlier parameters in the same function signature, e.g., function avg(x, y = x).
  • Default parameters are evaluated at call time, not at function definition time, so they can include function calls or expressions.
  • You can have multiple parameters with default values in the same function.
// Example showing defaults can reference other parameters
function createRange(start, end = start + 10) {
  return { start, end };
}

console.log(createRange(5)); // { start: 5, end: 15 }

4. If you want to write a function that accepts any number of arguments, will you use the arguments object or rest parameters? Justify your decision.

Solution

You should use rest parameters (...args). The arguments object is a legacy feature from early JavaScript and has several limitations:

  • It is not a real array, so you cannot use array methods like map, filter, or reduce directly on it.
  • It does not work in arrow functions.
  • It makes code less readable since it is an implicit variable.

Rest parameters provide a real array with all standard array methods available.

// Modern approach with rest parameters
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

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

5. A student writes this code and is confused about why it does not work. Explain what is happening and how to fix it.

const result = multiply(3, 4);
console.log(result);

const multiply = (a, b) => a * b;
Solution

The code fails because the variable binding multiply exists but has not been initialized yet. With const, the identifier is in the temporal dead zone until the assignment runs, so calling it first throws a ReferenceError.

Function declarations can be called before they appear in the source, but function expressions (including arrow functions) only become callable after the assignment executes.

To fix this, either move the function definition before its use:

const multiply = (a, b) => a * b;

const result = multiply(3, 4);
console.log(result); // 12

Or use a function declaration instead:

const result = multiply(3, 4);
console.log(result); // 12

function multiply(a, b) {
  return a * b;
}

6. When would you choose a named function expression over an anonymous function expression? Give a practical example.

Solution

Named function expressions are useful in two main scenarios:

  1. Recursion: The internal name allows the function to call itself without depending on external variable references.
  2. Debugging: The function name appears in stack traces, making errors easier to track down.
// Recursion example - the internal name 'factorial' is reliable
const fact = function factorial(n) {
  return n <= 1 ? 1 : n * factorial(n - 1);
};

console.log(fact(5)); // 120

// Even if we reassign the variable, the internal name still works
const original = fact;
// fact = null; // This won't break the recursion in 'original'

For most cases where you are writing simple callbacks (like in filter or map), anonymous functions are sufficient and more concise.

7. Rewrite this function using arrow function syntax, making it as concise as possible:

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(function (num) {
  return num * 2;
});
Solution
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(num => num * 2);

The arrow function can be simplified because:

  • Single parameter does not need parentheses
  • Single expression body does not need curly braces
  • The return is implicit for single-expression bodies

8. Write an arrow function called createUser that takes a username and email as parameters and returns an object with those properties. Use the most concise arrow function syntax possible.

Solution
const createUser = (username, email) => ({ username, email });

// Usage:
console.log(createUser("alice", "alice@example.com"));
// { username: "alice", email: "alice@example.com" }

You have to wrap the object literal in parentheses ({ ... }). Without the parentheses, JavaScript reads the curly braces as the function body, not as an object literal.

9. Explain with an example what the spread operator does when calling a function, and how it relates to rest parameters.

Solution

The spread operator (...) expands an iterable (like an array) into individual elements when calling a function. Rest parameters do the opposite—they collect multiple arguments into an array.

// Rest parameters: collect arguments into an array
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

// Spread operator: expand array into arguments
const values = [1, 2, 3, 4];
console.log(sum(...values)); // 10

// Equivalent to:
console.log(sum(1, 2, 3, 4)); // 10

The two go together. Spread expands an array into individual arguments, and rest collects individual arguments into an array.

10. Write a function called findMax that accepts any number of arguments and returns the largest value. Use rest parameters.

Solution
function findMax(...numbers) {
  if (numbers.length === 0) return undefined;
  let max = numbers[0];
  for (const num of numbers) {
    if (num > max) {
      max = num;
    }
  }
  return max;
}

console.log(findMax(3, 7, 2, 9, 1)); // 9
console.log(findMax(42)); // 42
console.log(findMax()); // undefined

Alternative solution using Math.max with spread:

const findMax = (...numbers) =>
  numbers.length === 0 ? undefined : Math.max(...numbers);

11. A colleague suggests that since functions are objects, we could add custom properties to them. Is this a good idea? What are the implications?

Solution

You can add custom properties to functions, because functions are objects. It works, but for most use cases it is not a good idea. Here is what it looks like:

function counter() {
  counter.count++;
  return counter.count;
}
counter.count = 0;

console.log(counter()); // 1
console.log(counter()); // 2

Implications and concerns:

  • Unexpected behavior: Other developers may not expect functions to have state stored as properties.
  • Testing difficulties: Functions with mutable state are harder to test in isolation.
  • Better alternatives: Closures or classes provide cleaner patterns for stateful behavior.

However, the built-in properties like name, length, and toString() are useful for introspection and debugging:

function add(x, y) {
  return x + y;
}
console.log(add.name); // "add"
console.log(add.length); // 2