Advanced Web Programming

Functions, Closures, Arrays and Objects

PGCP-AC

Functions make behavior reusable, closures connect behavior to remembered state, arrays organize ordered data and objects model named properties and relationships. These features form the working vocabulary of modern JavaScript applications.

1. Functions and lexical scope

A function packages behavior with parameters and a return value. Arguments are passed by value; when a value is an object reference, multiple bindings can refer to the same object. Reassigning a parameter does not reassign the caller's variable, but mutating that shared object is observable. A closure retains access to the lexical environment in which the function was created. This supports counters, callbacks and private state without global variables.

Regular functions obtain this according to how they are called. Arrow functions capture this lexically and cannot be invoked as constructors. Default parameters supply values when arguments are omitted or undefined. Rest syntax gathers arguments into an array; spread expands an iterable or copies enumerable object properties. Such copying is shallow: nested objects can remain shared.

2. Collections and prototype behavior

Arrays are ordered objects with specialized indexing and length behavior. map transforms each element, filter selects elements and reduce combines them. forEach performs an action and does not return a transformed array. Numeric sorting requires a numeric comparator because the default sort compares string representations. Mutating methods such as push and splice should be distinguished from methods such as slice that produce a separate array.

function makeCounter() {
  let count = 0;
  return () => ++count;
}
const next = makeCounter();
console.log(next(), next()); // 1 2
const sorted = [10, 2, 8].sort((a, b) => a - b);

Property lookup can follow an object's prototype chain. JavaScript classes provide syntax for constructing objects and defining prototype methods; inheritance still relies on prototypes. Encapsulation controls access to state, abstraction exposes useful operations and polymorphism lets different objects respond to a common operation. Choose object composition when independent responsibilities do not form a genuine inheritance relationship.

3. Object inspection and copying

Object.keys returns an array of an object's own enumerable string-keyed property names. Object.values returns corresponding values and Object.entries returns key-value pairs. These differ from for...in, which can also enumerate inherited enumerable properties. A method invoked as account.deposit() normally receives account as this; passing account.deposit as an unbound callback can lose that receiver relationship. Bind creates a callable with a chosen receiver.

Consider const b = {...a} where a has an address object. The outer objects differ, but a.address and b.address can still be the same object. Changing b.address.city may therefore affect what a observes. Copying the address as well makes that level independent, but a true deep-copy policy must define how to treat dates, cycles and class instances. A constructor function used with new initializes a fresh instance whose prototype participates in later property lookup.

4. Function forms and invocation

JavaScript functions are values: they can be stored, passed as arguments and returned. A declaration is available throughout its scope through declaration instantiation, while an expression is evaluated when execution reaches it.

function add(a, b) { return a + b; }

const multiply = function (a, b) { return a * b; };
const square = value => value * value;

Arrow syntax is concise and captures this, arguments and super from the surrounding lexical context. It has no constructor behavior and should not be used as an object method when a dynamic receiver is required.

Functions without an explicit return produce undefined. A return statement ends that invocation immediately. Code after an unconditional return is unreachable.

5. Parameters, rest, spread and defaults

Default parameters apply when an argument is omitted or explicitly undefined, but not when it is null.

function createUser(name, role = 'student', ...skills) {
  return { name, role, skills };
}

const user = createUser('Mira', undefined, 'HTML', 'CSS');

Rest syntax collects remaining arguments into a real array and must appear last. Spread performs the inverse-looking operation at a call site by expanding an iterable:

const values = [8, 3, 11];
console.log(Math.max(...values)); // 11

JavaScript passes every argument value by value. For an object, that value is a reference. A function receives its own parameter binding containing the same reference; it can mutate the shared object, but assigning a new object to the parameter does not replace the caller's binding.

6. Lexical scope and closures

A function resolves free variables where it was defined, not where it is called. A closure is the function together with access to that lexical environment.

function makeAccount(openingBalance = 0) {
  let balance = openingBalance;
  return {
    deposit(amount) {
      if (amount <= 0) throw new RangeError('Amount must be positive');
      balance += amount;
    },
    getBalance() { return balance; }
  };
}

Each call creates a separate environment, so two accounts do not share balances. The returned methods keep the environment reachable after makeAccount returns. Closures support data privacy, event handlers, configuration, memoization and function factories. They can also retain large objects longer than intended, so remove unused listeners and references when lifetime matters.

7. Callbacks and higher-order functions

A callback is a function supplied for later invocation. A higher-order function receives or returns functions.

function repeat(count, action) {
  for (let index = 0; index < count; index += 1) action(index);
}

repeat(3, index => console.log(`Step ${index + 1}`));

Pass the function value when the receiver should invoke it. Writing repeat(3, logStep()) invokes logStep immediately and passes its return value. Asynchronous callbacks add timing and error-handling concerns covered in later topics.

8. Array transformation and selection

MethodPurposeResult
mapTransform every elementNew array of equal length
filterRetain matching elementsNew array of zero or more elements
findLocate first matchElement or undefined
someTest whether any matchBoolean
everyTest whether all matchBoolean
reduceAccumulate one resultAny chosen accumulator type
forEachPerform side effectsundefined
const orders = [
  { id: 1, total: 400, paid: true },
  { id: 2, total: 250, paid: false },
  { id: 3, total: 600, paid: true }
];

const paidTotal = orders
  .filter(order => order.paid)
  .map(order => order.total)
  .reduce((sum, total) => sum + total, 0);

The initial value in reduce makes the accumulator type explicit and ensures that an empty input produces zero rather than throwing. Clear intermediate variables can be easier to debug than an unnecessarily long method chain.

9. Mutation, copying and sorting

Methods such as push, pop, shift, unshift, splice, sort and reverse mutate their array. map, filter, slice and concat return new outer arrays.

Default sort converts elements to strings and compares them lexicographically. Use a comparator for numbers:

const ascending = [...values].sort((a, b) => a - b);

The spread prevents the original outer array from being reordered. It is shallow: if elements are objects, both arrays still refer to the same element objects.

10. Objects and property access

Object literals collect named properties. Dot notation is convenient for fixed identifier-like names; bracket notation accepts computed keys.

const field = 'email';
const profile = { name: 'Asha', email: 'asha@example.com' };
console.log(profile[field]);

Object.keys, Object.values and Object.entries inspect own enumerable string-keyed properties. for...in may also reach inherited enumerable keys, so own-property checks are necessary when that distinction matters.

Destructuring extracts selected values and can rename or default them:

const { name: displayName, city = 'Unknown' } = profile;
const [first, ...remaining] = values;

11. this, call-site rules and binding

For a regular function, this depends on invocation. In account.deposit(10), the base object is the receiver. Extracting the function loses that call-site relationship:

const deposit = account.deposit;
// deposit(10); // receiver is missing or different
const safeDeposit = account.deposit.bind(account);
safeDeposit(10);

call invokes with a chosen receiver and individual arguments; apply accepts an array-like argument collection; bind returns a new callable. Arrow functions ignore attempts to replace their lexical this through these mechanisms.

12. Prototypes and classes

When an own property is absent, lookup can continue through the prototype chain. Class syntax provides a clearer form for constructors and prototype methods:

class Rectangle {
  constructor(width, height) {
    if (width <= 0 || height <= 0) throw new RangeError('Positive dimensions required');
    this.width = width;
    this.height = height;
  }
  area() { return this.width * this.height; }
}

Instances usually share the single area method through Rectangle.prototype; the method is not copied into every instance. extends establishes an inheritance relationship, while super accesses parent construction or behavior. Prefer composition when one object merely uses another capability and no genuine “is-a” relationship exists.

13. Shallow and deep-copy policies

const original = { name: 'Ravi', address: { city: 'Pune' } };
const copy = { ...original };
copy.address.city = 'Mumbai';
console.log(original.address.city); // Mumbai

Only the outer object was copied. A deeper operation such as structuredClone supports many built-in data forms and cycles, but functions and some platform objects are outside its supported model. JSON serialization is not a general clone mechanism: it loses unsupported values and class prototypes and transforms dates into strings. Define which types, identities and cycles a domain copy must preserve.

14. Integrated data-processing example

function createReportBuilder(taxRate) {
  return function buildReport(items) {
    const lines = items.map(({ name, price, quantity = 1 }) => ({
      name,
      amount: price * quantity
    }));
    const subtotal = lines.reduce((sum, line) => sum + line.amount, 0);
    return { lines, subtotal, tax: subtotal * taxRate, total: subtotal * (1 + taxRate) };
  };
}

const buildReport = createReportBuilder(0.18);

The outer function closes over one tax rate. The inner function transforms each item, aggregates a subtotal from a safe initial value and returns a new report without mutating the input array.

15. Practical considerations

  1. Arrow functions capture this and cannot be constructors.
  2. Reassigning a parameter does not reassign the caller's variable.
  3. Mutating a shared referenced object is visible through every alias.
  4. Rest collects; spread expands or shallow-copies.
  5. forEach returns undefined, not a transformed array.
  6. Default sort is lexicographic unless a comparator is supplied.
  7. slice does not delete source elements; splice can.
  8. Object spread does not recursively clone nested objects.
  9. Extracting a regular method can lose its receiver.
  10. Class instance methods normally live on the prototype.

Continue learning

Related notes

Put this topic into timed practice

Open mock tests when you want full-exam pacing, or keep drilling in practice mode.