1. Courses
  2. Developer Foundations
  3. JavaScript Deep Dive
  4. Stop 2

35 minIntermediate

Functions, scope and closures

Functions are the main building block of JavaScript, and most of the language's surprising behaviour comes from three rules: where a name is visible, when it exists, and what this means. This lesson shows the mechanism behind each, then uses closures to build discount rules and a cache.

By the end you can

  • Pick between a function declaration, a function expression and an arrow function
  • Trace how JavaScript finds a name through nested scopes, and explain the temporal dead zone
  • Explain what a closure keeps alive, and use one to build a counter, a formatter and a memoize helper
  • Predict what this is in a normal function and in an arrow function, and fix the classic lost-this bug

The everyday picture

At the start of a shift, the shift manager opens each counter and puts a float in the cash drawer: say $100 in small notes and coins for change. Then the manager goes back to the office or goes home. The drawer does not forget its float. Counter 3's drawer keeps its own $100, and every sale adds to that drawer only. Counter 4 has a separate drawer with a separate float, and the two never mix.

A closure is that drawer. A function that creates another function is the shift manager. When it finishes, the inner function still holds the variables it was given, and each call makes a fresh drawer. To understand why that works, you first need to see where names live.

Three ways to write a function

A starts with the keyword function and a name. A creates a function as a value and stores it in a variable. An is a shorter expression. All three can do the same job:

JavaScript
function lineTotal(qty, unitPrice) { return qty * unitPrice; }           // declaration
const lineTotal2 = function (qty, unitPrice) { return qty * unitPrice; }; // expression
const lineTotal3 = (qty, unitPrice) => qty * unitPrice;                  // arrow

// Default values and a rest parameter
function receiptLine(articleCode, qty = 1, ...notes) {
  return `${articleCode} x${qty}${notes.length ? ' (' + notes.join(', ') + ')' : ''}`;
}
console.log(lineTotal(2, 1250), lineTotal2(2, 1250), lineTotal3(2, 1250));
console.log(receiptLine('100234'));
console.log(receiptLine('100234', 3, 'festival offer', 'gift wrap'));
console.log(receiptLine('100234', undefined));   // undefined triggers the default
console.log(receiptLine('100234', null));        // null does not
console.log(lineTotal(2));                       // a missing argument is undefined

const toObj = (siteId) => ({ siteId });          // returning an object: wrap it in ( )
const broken = (siteId) => { siteId };           // braces mean a body; returns nothing
console.log(toObj('S014'), broken('S014'));
Output
2500 2500 2500
100234 x1
100234 x3 (festival offer, gift wrap)
100234 x1
100234 xnull
NaN
{ siteId: 'S014' } undefined

Four details from that output matter in real code. A default value is used only when the argument is undefined, so an API that sends null skips it. A missing argument does not cause an error; it is undefined, and 2 * undefined is NaN. The ...notes collects the extra arguments into a real array. And an arrow function returns its expression automatically only when there are no braces, so an object literal must be wrapped in parentheses.

Which to use? Teams usually write top-level, named helpers as declarations or const arrows (both are fine; pick one in the style guide), and use arrows for short callbacks such as sales.map((s) => s.qty). Arrow functions also differ in one big way: they have no this of their own. That is covered below, and it is the reason arrows are the right choice for callbacks inside methods.

Quick check

A checkout calls receiptLine('100234', null) because the API sent qty: null. What does the receipt line say?

Scope: where a name is visible

is the region of code where a name can be used. JavaScript has three kinds you meet every day. Block scope: let and const inside { } of an if, a loop or a bare block exist only there. Function scope: parameters and variables declared in a function exist only during that call. Module scope: top-level names in a file are visible to the whole file, and to other files only if you export them (Prototypes, classes and modules).

Scopes nest, and lookup always goes outward, never inward. When code uses a name, JavaScript looks in the current block first, then the enclosing function, then the module, then the global scope. The first match wins. If nothing matches, you get a ReferenceError. Outer code can never see names declared inside an inner block.

JavaScript
const storeName = 'Riverside';            // module scope: the whole file sees it

function closeShift(sales) {
  let total = 0;                          // function scope: only inside closeShift
  for (const sale of sales) {
    const line = sale.qty * sale.unitPrice;   // block scope: only inside the loop body
    total += line;                        // "total" is found one scope outward
  }
  try { console.log(line); }              // outside the block: not visible
  catch (e) { console.log(e.name + ': ' + e.message); }
  return `${storeName}: ${total} cents`;  // "storeName" is found two scopes out
}
console.log(closeShift([{ qty: 2, unitPrice: 1250 }, { qty: 1, unitPrice: 450 }]));

if (true) { var leaky = 'var ignores blocks'; let tight = 'let respects blocks'; }
console.log(leaky, typeof tight);
Output
ReferenceError: line is not defined
Riverside: 2950 cents
var ignores blocks undefined

The last two lines show why var is banned: it belongs to the whole function (or the whole script), so it leaks out of the if block. let stays where it was declared.

Quick check

A file declares const total = 99 at the top, and closeShift declares its own let total = 0. Which total does the loop inside closeShift add to?

Module scope storeName closeShift Function: closeShift sales total Block: the for loop sale line total += line 1. own block 2. function 3. module line: not visible here
Nested scopes. Code inside the loop looks for a name in its own block, then the function, then the module, and stops at the first match. Lookup only goes outward, so after the loop the function cannot see line.

Hoisting and the temporal dead zone

Before any line of a scope runs, JavaScript scans the scope and creates every name declared in it. This is called . What differs is the state each name starts in:

  • A function declaration is created complete, so you can call it above the line where it is written.
  • A var is created with the value undefined, so reading it early gives undefined without any error. That silence hides bugs.
  • A let, const or class is created but left uninitialised. From the top of the scope until its declaration line runs, it is in the (TDZ), and touching it throws a ReferenceError.
JavaScript
console.log(addTax(1000));           // works: a function declaration is ready early
console.log(oldStyle);               // var exists early, but is undefined

try { console.log(total); }          // let exists, but is in the dead zone
catch (e) { console.log(e.name + ': ' + e.message); }

try { console.log(formatUsd(5000)); }    // a const arrow function: same dead zone
catch (e) { console.log(e.name + ': ' + e.message); }

function addTax(cents) { return cents + Math.round(cents * 0.15); }
var oldStyle = 'set now';
let total = 0;
const formatUsd = (cents) => `USD ${(cents / 100).toFixed(2)}`;
Output
1150
undefined
ReferenceError: Cannot access 'total' before initialization
ReferenceError: Cannot access 'formatUsd' before initialization

The message "Cannot access … before initialization" is the TDZ. The message "… is not defined" means the name does not exist in any enclosing scope at all. Knowing which one you are looking at tells you whether the problem is order or spelling.

Closures: a function keeps its scope alive

Each time a function runs, JavaScript creates a fresh set of its local variables, called its environment. Normally that environment is thrown away when the call returns. But if the call creates an inner function that uses those variables, and the inner function survives (it is returned, stored, or passed as a callback), then the environment survives too. The inner function plus the variables it keeps is a .

JavaScript
function makeTokenCounter(prefix) {
  let next = 1;                            // survives after makeTokenCounter returns
  return function issue() {
    const token = `${prefix}-${String(next).padStart(3, '0')}`;
    next += 1;
    return token;
  };
}
const counter1 = makeTokenCounter('C1');   // two calls, two separate "next" variables
const counter2 = makeTokenCounter('C2');
console.log(counter1(), counter1(), counter2(), counter1());

function makePriceFormatter(currency, decimals) {
  return (cents) => `${currency} ${(cents / 100).toFixed(decimals)}`;
}
const usd = makePriceFormatter('USD', 2);
const usdWhole = makePriceFormatter('USD', 0);
console.log(usd(1250), usdWhole(1250));
Output
C1-001 C1-002 C2-001 C1-003
USD 12.50 USD 13

Three facts explain that output. The closure keeps the variable itself, not a copy of its value, which is why next keeps going up. Each call to the outer function makes a new environment, so counter2 starts again at 1. And the variable is private: no code outside can read or reset next, because no outside scope contains it. That is how JavaScript did private state for about twenty years before classes got #private fields (Prototypes, classes and modules).

Quick check

After the code above, you create const counter3 = makeTokenCounter('C3') and call it once. What does it return?

makeTokenCounter() call finished, env kept Variable Function Environment counter1 issue() closure 1 prefix: 'C1' next: 4 counter2 issue() closure 2 prefix: 'C2' next: 2 keeps alive Console C1-001 C1-002 C2-001 C1-003
A closure keeps its environment alive. The call to makeTokenCounter has finished, but each returned issue function still holds its own prefix and next. After three calls, counter 1's next is 4; counter 2's separate one is 2.
Think first: this loop schedules three receipts to print later. Why does it print "4" three times?
JavaScript
for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log('var receipt', i), 0);
}
for (let j = 1; j <= 3; j++) {
  setTimeout(() => console.log('let receipt', j), 0);
}
Output
var receipt 4
var receipt 4
var receipt 4
let receipt 1
let receipt 2
let receipt 3

All three arrow functions close over the same var i, because var has one variable for the whole function. The timers run after the loop has finished (The event loop shows why), and by then i is 4. With let, each loop iteration gets its own fresh j, so each closure keeps a different variable.

this: decided by the call, unless it is an arrow

Inside a normal function or method, this is not fixed when you write the function. It is decided each time the function is called, by how it is called. Call it as store.label() and this is store. Copy the same function into a variable and call label(), and this is undefined in (which ES modules and classes always use). In an old non-strict script it is worse: this becomes the global object, and your code writes to globals without any error.

An arrow function has no this of its own. It uses the this of the scope it was written in, exactly like any other outer variable. That makes arrows the right tool for callbacks inside methods, and the wrong tool for the methods themselves.

JavaScript
// this-lost.mjs
const store = {
  siteId: 'S014',
  label() { return `Store ${this.siteId}`; },
};
console.log(store.label());                  // called on store: this = store
const label = store.label;                   // the same function, cut off
try { label(); } catch (e) { console.log(e.name + ': ' + e.message); }
const bound = store.label.bind(store);       // bind fixes "this" for good
console.log(bound());
Output
Store S014
TypeError: Cannot read properties of undefined (reading 'siteId')
Store S014

Passing a method as a callback, as in rows.map(this.toDto) or button.onPress = this.save, is the same as cutting it off: the receiver does not call it on your object. Wrap it in an arrow (rows.map((r) => this.toDto(r))) or bind it.

Higher-order and pure functions

In JavaScript a function is a value: you can pass it in, return it, and keep it in an array. A takes a function, returns one, or both. You already use them: sales.filter((s) => !s.voided) passes a function to filter. The factories above return one.

A gives the same output for the same input and changes nothing outside itself. (total, sale) => total + sale.qty * sale.unitPrice is pure. A function that adds to a module-level todayTotal is not: calling it twice with the same sale gives two different answers. Pure functions are easy to test (no setup, no mocks), safe to cache, and safe to run in any order. Keep pricing, discount and tax rules pure, and push the impure parts (reading the database, writing logs, sending the receipt by text message) to the edges of the code.

Build it

Build a discount-rule factory from closures, a memoize helper, and fix a lost-this bug in a service class. Each file runs with node. The Practice section repeats the factory and the counter as exercises you can run in your browser.

  1. A discount-rule factory

    Each factory returns a small rule function, a closure over its settings. bestOf is higher-order: it takes rules and returns one rule that picks the biggest discount. All rules are pure, so they are easy to test.

    JavaScript
    // discount-rules.js
    function percentOff(percent, { minCents = 0 } = {}) {
      // percent and minCents stay alive inside the returned function
      return (cart) =>
        cart.totalCents >= minCents ? Math.round((cart.totalCents * percent) / 100) : 0;
    }
    
    function flatOff(amountCents, ...articleCodes) {          // rest: any number of codes
      return (cart) =>
        cart.lines.some((l) => articleCodes.includes(l.articleCode)) ? amountCents : 0;
    }
    
    function bestOf(...rules) {                                // takes functions, returns one
      return (cart) => Math.max(0, ...rules.map((rule) => rule(cart)));
    }
    
    const festivalOffer = bestOf(
      percentOff(10, { minCents: 5000 }),     // 10% off carts of $50 or more
      flatOff(500, '200871', '200872'),       // $5 off if the cart has a rice cooker
    );
    
    const carts = {
      big:    { totalCents: 7500, lines: [{ articleCode: '100234' }] },
      cooker: { totalCents: 3000, lines: [{ articleCode: '200871' }] },
      small:  { totalCents: 2400, lines: [{ articleCode: '100234' }] },
    };
    for (const [name, cart] of Object.entries(carts)) {
      console.log(`${name.padEnd(6)} discount: ${festivalOffer(cart)} cents`);
    }
    Terminal
    node discount-rules.js
    Output
    big    discount: 750 cents
    cooker discount: 500 cents
    small  discount: 0 cents

    Marketing's next offer becomes one more line, such as percentOff(5), with no change to checkout code.

  2. A memoize helper

    memoize wraps a slow pure function. The cache Map lives in the closure, so only the wrapper can see it.

    JavaScript
    // memoize.js
    function memoize(fn) {
      const cache = new Map();                // private: only the inner function sees it
      return function (key) {
        if (cache.has(key)) return cache.get(key);
        const result = fn(key);
        cache.set(key, result);
        return result;
      };
    }
    
    let realCalls = 0;
    function taxRateFor(articleCode) {        // imagine a slow lookup in a big rate table
      realCalls += 1;
      return articleCode.startsWith('1') ? 0 : 15;   // made-up rule for the example
    }
    
    const taxRate = memoize(taxRateFor);
    const basket = ['100234', '200871', '100234', '200871', '100234'];
    console.log(basket.map(taxRate));
    console.log('real lookups:', realCalls);
    Output
    [ 0, 15, 0, 15, 0 ]
    real lookups: 2

    Two limits to say out loud in review. Only memoize pure functions: if the tax table can change, the cache serves stale rates. And this cache never shrinks; with thousands of keys in a long-running server it becomes a memory leak (Performance and memory in JavaScript builds a bounded version).

  3. Reproduce a lost-this bug

    A service class maps database rows to response objects. Passing this.toDto to map hands over the bare function, so inside it this is undefined. Class bodies are always strict, so it fails loudly.

    JavaScript
    // stock-service.mjs
    class PriceService { format(cents) { return `USD ${(cents / 100).toFixed(2)}`; } }
    
    class StockService {
      constructor(prices) { this.prices = prices; }
      toDto(row) { return { articleCode: row.articleCode, price: this.prices.format(row.unitPrice) }; }
      listBroken(rows) { return rows.map(this.toDto); }              // passes the bare method
      list(rows) { return rows.map((row) => this.toDto(row)); }      // arrow keeps "this"
    }
    
    const svc = new StockService(new PriceService());
    const rows = [{ articleCode: '100234', unitPrice: 1250 }];
    try { svc.listBroken(rows); } catch (e) { console.log(e.name + ': ' + e.message); }
    console.log(svc.list(rows));
    Terminal
    node stock-service.mjs
    Output
    TypeError: Cannot read properties of undefined (reading 'prices')
    [ { articleCode: '100234', price: 'USD 12.50' } ]
  4. See the same bug with a plain callback

    The forEach version is the most common form of this bug in app code. The normal function callback gets its own this (undefined), while the arrow uses the method's this.

    JavaScript
    // scan-counter.mjs
    const counter = {
      siteId: 'S014',
      scanned: 0,
      scanAllBroken(codes) {
        codes.forEach(function (code) {
          this.scanned += 1;          // a normal function: "this" is undefined here
        });
        return this.scanned;
      },
      scanAll(codes) {
        codes.forEach((code) => {
          this.scanned += 1;          // an arrow: "this" is scanAll's "this", the counter
        });
        return this.scanned;
      },
    };
    
    try { counter.scanAllBroken(['100234', '200871']); }
    catch (e) { console.log(e.name + ': ' + e.message); }
    console.log(counter.scanAll(['100234', '200871']));
    Output
    TypeError: Cannot read properties of undefined (reading 'scanned')
    2

    Rename it to scan-counter.cjs and run it again: a CommonJS file is not strict by default, so there is no error at all. The only output is 2. The broken method returned 0 and quietly created a global scanned set to NaN. A loud error is the better outcome.

Bring it to your team

Closures and this problems rarely show up in a quick demo; they show up under load or months later. Ask:

  • "Is any per-request or per-user data stored in a module-level variable in this service?"
  • "Are the pricing, discount and tax rules pure functions with unit tests, separate from the code that reads the database?"
  • "This cache lives in a closure. What is its maximum size, and what happens when the source data changes?"
  • "Do we pass any class methods as callbacks without an arrow or bind?"
  • "Is the code in ES modules or strict mode, so a lost this fails loudly?"

Red flags

  • var anywhere in new code, or loops creating callbacks with var.
  • let current… at the top of a server file, set inside a request handler.
  • An unbounded memoize or "simple cache" Map in a long-running API.
  • const self = this; or .bind(this) scattered everywhere, a sign the code fights this instead of using arrows for callbacks.

Practice

Build two discount-rule factories

Write percentOff(percent, { minCents }), which returns a rule: a function that takes a cart such as { totalCents: 7500 } and returns the discount in cents, rounded with Math.round, or 0 when the cart is below minCents (default 0). Then write bestOf(...rules), which returns one rule that gives the biggest discount of all its rules, and never less than 0.

JavaScript exerciseRuns in your browser with tests
function percentOff(percent, { minCents = 0 } = {}) {
  // return a rule: a function that takes a cart and returns cents off
}

function bestOf(...rules) {
  // return one rule that gives the biggest discount of all the rules
}
Ctrl + Enter runs
Show a solution
JavaScript
function percentOff(percent, { minCents = 0 } = {}) {
  return (cart) =>                                   // percent and minCents live on here
    cart.totalCents >= minCents ? Math.round((cart.totalCents * percent) / 100) : 0;
}

function bestOf(...rules) {
  return (cart) => Math.max(0, ...rules.map((rule) => rule(cart)));
}
Pass the exercise to finish this task.

Fix the counters that share a number

The Store app issues queue tokens per counter. Each call to makeTokenCounter('C1') should return a function that gives C1-001, then C1-002, and so on, independently of every other counter. Right now two counters interfere with each other. Move one line to fix it.

JavaScript exerciseRuns in your browser with tests
let next = 1;

function makeTokenCounter(prefix) {
  return function issue() {
    const token = `${prefix}-${String(next).padStart(3, '0')}`;
    next += 1;
    return token;
  };
}
Ctrl + Enter runs
Show a solution
JavaScript
function makeTokenCounter(prefix) {
  let next = 1;                       // one fresh variable per call: each counter's own drawer
  return function issue() {
    const token = `${prefix}-${String(next).padStart(3, '0')}`;
    next += 1;
    return token;
  };
}
Pass the exercise to finish this task.

Fix the lost this

StockService.list(rows) should turn each row into { articleCode, price }, where price comes from the service's own price formatter. It throws instead. Fix list without changing toDto.

JavaScript exerciseRuns in your browser with tests
class StockService {
  constructor(prices) { this.prices = prices; }
  toDto(row) {
    return { articleCode: row.articleCode, price: this.prices.format(row.unitPrice) };
  }
  list(rows) {
    return rows.map(this.toDto);
  }
}
Ctrl + Enter runs
Show a solution
JavaScript
class StockService {
  constructor(prices) { this.prices = prices; }
  toDto(row) {
    return { articleCode: row.articleCode, price: this.prices.format(row.unitPrice) };
  }
  list(rows) {
    return rows.map((row) => this.toDto(row));   // the arrow keeps list's this
  }
}
Pass the exercise to finish this task.

Check yourself

You call counter1() three times and counter2() once, both made by makeTokenCounter. Why does counter2() return C2-001 rather than C2-004?

This line is at the top of a file, above const formatUsd = (c) => …: console.log(formatUsd(5000)). What happens?

Inside a class method, rows.map(this.toDto) throws "Cannot read properties of undefined". What is the best fix?

Flashcards

Open my review

Key takeaways

  • Declarations, expressions and arrows all make functions; arrows have no this of their own and return their expression when there are no braces.
  • Name lookup goes outward through block, function and module scope, never inward. let and const are block-scoped; var leaks.
  • Everything is hoisted, but let, const and class sit in the temporal dead zone until their line runs.
  • A closure keeps the outer variables themselves alive. Each outer call makes a new private set. Unbounded closure caches leak memory.
  • this is set by the call. Use method syntax for methods and arrows for the callbacks inside them. Keep business rules pure.

Sources

Episode transcriptThe cash drawer that remembers its float · about 12 minutes · tap any line to play from there
  1. NadiaImran, here's a bug report. Festival evening, the Partner app's API is busy, and a stock change from Ravi's shop gets saved under a different partner shop across town. Nobody touched the code for weeks.
  2. ImranThat's the kind of bug that makes me nervous, because it only happens when we're busy.
  3. NadiaExactly. And the cause is one of today's three rules: where a name is visible, when it exists, and what the word this means. Today is functions, scope and closures.
  4. NadiaStart with the store. At the start of a shift, the shift manager opens each counter and puts a float in the cash drawer. Say a hundred dollars in small notes and coins for change. Then the manager goes back to the office, or goes home.
  5. ImranAnd the drawer still has its hundred dollars.
  6. NadiaRight. Counter three's drawer keeps its own float, and every sale adds to that drawer only. Counter four has a separate drawer with a separate float. They never mix.
  7. ImranSo the manager is a function, and the drawer is what it leaves behind.
  8. NadiaThat's a closure. A function that creates another function is the manager. When it finishes, the inner function still holds the variables it was given. And each call makes a fresh drawer. But first, three quick facts about functions themselves.
  9. NadiaThere are three ways to write one. A declaration, which starts with the word function and a name. An expression, where you create a function and store it in a variable. And an arrow function, the short form.
  10. ImranMost of the code I review uses arrows for everything.
  11. NadiaThat's mostly fine. Just know three details. A default value for a parameter only kicks in when the argument is undefined. If an API sends null, the default is skipped.
  12. ImranSo a quantity default of one, with a null coming in, gives me null.
  13. NadiaYes, and the receipt literally prints x null. Second detail: a missing argument isn't an error. It's just undefined, and two times undefined is NaN. Third: an arrow function that returns an object needs the object wrapped in round brackets, or JavaScript reads the curly braces as the function body.
  14. ImranOkay. Now scope.
  15. NadiaScope is the region of code where a name can be used. There are three kinds you meet every day. Block scope, inside the curly braces of an if or a loop. Function scope, for variables inside one call. And module scope, for the top level of a file.
  16. NadiaIf you can see the screen, the first diagram shows them as boxes inside boxes. A dashed outer frame for the module, a box inside it for the function, and a highlighted box inside that for the loop. A thick arrow goes straight up through all three.
  17. ImranAnd that arrow is the lookup.
  18. NadiaYes. When code uses a name, JavaScript looks in its own block first, then the function, then the module. The first match wins. And lookup only goes outward. After the loop ends, the function can't see the variable called line that was declared inside the loop.
  19. ImranAnd var breaks that?
  20. NadiaVar ignores blocks. A var declared inside an if belongs to the whole function, so it leaks out. That's the main reason teams ban it.
  21. ImranThen there's hoisting. I've always found that word confusing.
  22. NadiaHere's the mechanism. Before any line in a scope runs, JavaScript scans the scope and creates every name declared in it. That's hoisting. What differs is the state each name starts in.
  23. NadiaA function declaration is created complete, so you can call it above the line where it's written. A var starts as undefined, so reading it early gives you undefined without any warning. A let or a const exists, but it's locked until its line runs.
  24. ImranAnd that locked period is the temporal dead zone.
  25. NadiaRight. Touch the name during that time and you get a reference error that says cannot access before initialization. If the error says the name is not defined, that's different. It means the name doesn't exist anywhere.
  26. ImranSo one message means my order is wrong, and the other means my spelling is wrong.
  27. NadiaThat's a very useful way to put it. Now closures. Each time a function runs, JavaScript creates a fresh set of its local variables. Normally that set is thrown away when the call returns.
  28. NadiaBut if the call creates an inner function that uses those variables, and the inner function survives, because it's returned or stored or passed as a callback, then the variables survive too.
  29. ImranGive me the example from the lesson.
  30. NadiaA token counter, like the ticket machine at a busy counter. A function called make token counter takes a prefix, like C one for counter one, and keeps a variable called next, starting at one. It returns a small function that builds a token like C one dash zero zero one and adds one to next.
  31. ImranAnd if I make two counters?
  32. NadiaEach gets its own drawer. If you can see the screen, the second diagram shows two rows. Each counter variable points to its own function, and each function points to its own environment box. After three calls, counter one's next is four. Counter two's is two.
  33. ImranI'd have guessed the closure takes a copy of next when it's created.
  34. NadiaThat's the common guess, and it's wrong. The closure keeps the variable itself, which is why next keeps going up. And no code outside can read or reset it. That's how JavaScript did private state for about twenty years.
  35. ImranSo that's also the classic loop bug, where all the callbacks print the same number.
  36. NadiaYes. With var, there's one variable for the whole loop, so three timers that print it later all see four. With let, each turn of the loop gets its own fresh variable, so they print one, two and three.
  37. ImranNow the word this. I see this go wrong in reviews all the time.
  38. NadiaEveryone gets it wrong at first. Inside a normal function or method, this isn't fixed when you write the code. It's decided each time the function is called, by how it's called. Call it on the store object, and this is the store.
  39. NadiaCopy the same function into a plain variable and call it, and this is undefined. In ES modules and classes, which are always strict, that throws an error. In an old non-strict script it's worse: this becomes the global object and your code silently writes to globals.
  40. ImranAnd arrow functions?
  41. NadiaAn arrow has no this of its own. It uses the this of the place it was written, just like any other outer variable. So arrows are the right tool for callbacks inside methods, and the wrong tool for the methods themselves.
  42. ImranSo in a NestJS service, if I pass a method straight into map, it loses its object.
  43. NadiaExactly. The fix is to wrap it in a small arrow that calls the method on this, or to bind it. Two more ideas, quickly. A higher-order function takes or returns a function, like filter or map. A pure function gives the same output for the same input, and changes nothing outside itself.
  44. ImranAnd pricing rules should be pure.
  45. NadiaPricing, discount and tax rules, yes. They're easy to test and safe to cache. Push the impure parts, like reading the database or texting the receipt, to the edges.
  46. ImranSo back to Ravi's stock change. What happened?
  47. NadiaIt's the At Mangrove Market example in the lesson. The service kept the current site ID in a module-level variable. Each request set it at the start, then called a few async helpers that read it. With one phone in testing, it works perfectly.
  48. NadiaOn a busy evening, Ravi's request sets it, pauses on a database call, and another shop's request sets it again in the meantime. Ravi's request resumes and writes under the other shop's site.
  49. ImranBecause the module scope is one variable shared by every request the server handles.
  50. NadiaRight. One Node process serves them all, which we'll see in the lesson on the event loop. Per-request data belongs in function parameters, or a closure made per request, or on the request itself.
  51. NadiaNow the Build it. First, a discount rule factory. One factory makes a percent off rule with a minimum cart size, another makes a flat amount off if the cart has certain articles, and a third takes any number of rules and returns the best discount.
  52. ImranSo the festival offer becomes ten percent off carts of fifty dollars or more, or five dollars off if there's a rice cooker, whichever is bigger.
  53. NadiaYes. A seventy-five dollar cart gets seven fifty off, a cart with the rice cooker gets five dollars, and a small cart gets nothing. And the next offer is one more line, with no change to checkout.
  54. NadiaSecond, a memoize helper. It wraps a slow tax rate lookup and keeps the answers in a map that lives in the closure. A basket of five items makes only two real lookups.
  55. ImranAnd the catch is that the cache never shrinks.
  56. NadiaRight, and it only works for pure functions. If the tax table changes, you serve stale rates. Third, you reproduce the lost this bug in a stock service and fix it with an arrow. Then you rename the scanner file to CommonJS and watch the same bug fail silently.
  57. ImranAnd the practice section turns three of those into exercises in the browser.
  58. NadiaThe discount factory, a pair of token counters that wrongly share one number, and the lost this. The counter one is my favourite. You fix it by moving a single line, and the tests explain why.
  59. ImranFor my next review, I'd ask: is any per-request data in a module-level variable? Are our pricing rules pure and tested? And this cache in a closure, what's its maximum size?
  60. NadiaGood list. I'd add: are we passing any class methods as callbacks without an arrow or bind?
  61. NadiaOne to think about. A line at the top of a file calls format USD, but format USD is a const arrow function defined further down. What happens? Take a second.
  62. ImranIt's a const, so it's in the temporal dead zone. A reference error, cannot access before initialization.
  63. NadiaExactly. If it had been a function declaration, it would have worked.
  64. ImranLet me sum up. One: name lookup goes outward through block, function and module, never inward, and var leaks. Two: a closure keeps the outer variables alive, and each call makes a new private set. Three: this is decided by the call, so arrows go inside methods, not as methods.
  65. NadiaOne nuance on two. Because closures keep variables alive, an unbounded cache in a closure is a memory leak waiting to happen.
  66. ImranNext time?
  67. NadiaObjects and references. Why editing a copy of an order can change the original order, and what two staff sharing one storeroom key has to do with it.
Finished reading?Mark it complete to fill in its stop on your course line.

Use it at work

Utility script

A small script with its options at the top, input checks, a summary line and tests.

Open in the Code Lab
Next stopObjects, arrays and references
Was this lesson helpful?
Questions and discussion

Loading the discussion…