JavaScript Functions, this and Closures (Run in jsc)
JavaScript lecture 2, run in macOS jsc: function vs arrow, what this really is, closures, the var-in-a-loop trap, TDZ ReferenceError and a measured memoize.
Series: JavaScript lecture 2/6 (beginner). For people who can write a few lines of JavaScript (see lecture 1) and now meet this, arrow functions and callbacks in real code. After this lecture you will pick the right function form, predict what this is, use closures on purpose, and recognise the var-in-a-loop and TDZ errors on sight.
Functions are where JavaScript stops looking like Java or Python. The same method can see a different this depending on how it is called, an arrow function quietly behaves differently from a function, and a loop of three timers can print 3 3 3. These are the questions new team members ask most in code review, and the bugs they cause show up as TypeError: undefined is not an object far away from the real mistake. Everything below was run with JavaScriptCore's jsc shell (macOS 26.6, Apple Silicon M1 Pro); set up the jsc alias as in lecture 1. Node.js and browsers were not run; where they differ we say so.
Four ways to write a function
print(add(2, 3)); // works: declarations are hoisted
function add(a, b) { return a + b; }
const mul = function (a, b) { return a * b; };
const sq = x => x * x;
const greet = (name = "guest", ...rest) => `hi ${name} (+${rest.length})`;
print(mul(2, 3), sq(4), greet(), greet("Mina", 1, 2));
print(add(1), add(1, 2, 3), add.length, greet.length);
const point = (x, y) => ({ x, y });
const broken = (x, y) => { x, y };
print(JSON.stringify(point(1, 2)), broken(1, 2));
5
6 16 hi guest (+0) hi Mina (+2)
NaN 3 2 0
{"x":1,"y":2} undefined
The vocabulary:
- Function declaration (
function add(…) {…}): a statement that creates a named function. It is hoisted completely, so you can call it above the line where it is written. - Function expression (
const mul = function …): a function stored in a variable. Only the variable exists early, not the function. - Arrow function (
x => x * x): short syntax added in ES2015. One expression after=>is returned automatically. Arrows have nothis, noargumentsand cannot be used withnew. - Default parameters (
name = "guest") and rest parameters (...rest, the leftover arguments as an array).
Three surprises in that output. Calling add(1) does not fail: the missing b is undefined, and 1 + undefined is NaN. Extra arguments are silently ignored (add(1, 2, 3) is 3). And broken returns undefined, because { x, y } after => is read as a block of statements, not an object: wrap an object in parentheses, as point does.
Old code reads arguments through arguments. It does not exist in arrows:
$ jsc arrowargs.js # const f = () => arguments.length; f(1, 2);
Exception: ReferenceError: Can't find variable: arguments
f@arrowargs.js:1:26
global code@arrowargs.js:1:36
Use rest parameters instead.
Hoisting: why only declarations can be called early
Calling a function stored in a var before its line:
print(typeof early); // undefined
early();
var early = function () { print("never"); };
undefined
Exception: TypeError: early is not a function. (In 'early()', 'early' is undefined)
global code@hoistfn.js:2:6
The same with const:
$ jsc hoistfn2.js # later(); const later = () => print("never");
Exception: ReferenceError: Cannot access 'later' before initialization.
global code@hoistfn2.js:1:6
Two different error types for the same mistake: with var the name exists and holds undefined (calling undefined is a TypeError); with const the name is in its temporal dead zone (a ReferenceError). In practice, put helper functions as declarations at the bottom of a file if you like that style, or define everything before use.
| Situation | Use | Why |
|---|---|---|
| a named, reusable function at file level | function name() {} |
hoisted, readable stack traces |
a callback (map, setTimeout, event handler) |
arrow x => … |
short, keeps the outer this |
| a method on an object or class | method shorthand name() {} |
gets this from the call |
| a constructor | class (lecture 4) |
arrows cannot be used with new |
| need access to all arguments | rest ...args |
arguments is missing in arrows |
Scope and the temporal dead zone
Scope is the region of code where a name is visible. let and const are block-scoped (visible only inside the nearest { }), var is function-scoped:
if (true) { var v = 1; let l = 2; const c = 3; }
print(v, typeof l, typeof c); // 1 undefined undefined
function f() { var inside = 1; }
f();
print(typeof inside); // undefined
var v leaked out of the if block; l and c did not. typeof is the one operator that does not throw on an unknown name, which is why it printed undefined instead of failing.
The TDZ from lecture 1 becomes confusing inside functions:
let x = "outer";
function show() {
print(x); // which x?
let x = "inner";
}
show();
Exception: ReferenceError: Cannot access 'x' before initialization.
show@tdz.js:3:10
global code@tdz.js:6:5
Many beginners expect outer. But the inner let x shadows (hides) the outer one for the whole function body, including the lines above it, and before its line it is uninitialised. Even typeof throws here: print(typeof later); let later = 1; gave the same ReferenceError, while typeof notDeclared printed undefined. The fix is to rename the inner variable or move its declaration up.
What this is (four rules, all run)
this is a hidden parameter whose value depends on how a function is called, not where it is written. Arrow functions are the exception: they have no this and use the one from the surrounding code. One file shows all of it (this1.js, strict mode):
"use strict";
const counter = {
count: 0,
inc() { this.count++; return this.count; },
incArrow: () => typeof this,
later() { setTimeout(function () { print("function cb this:", this); }, 0);
setTimeout(() => { this.count++; print("arrow cb count:", this.count); }, 0); },
};
print(counter.inc(), counter.inc());
print("arrow method this:", counter.incArrow());
counter.later();
const inc = counter.inc;
const bound = counter.inc.bind(counter);
print("bound:", bound());
try { inc(); } catch (e) { print(e.name + ": " + e.message); }
1 2
arrow method this: object
bound: 3
TypeError: undefined is not an object (evaluating 'this.count')
function cb this: undefined
arrow cb count: 4
Reading it line by line:
counter.inc()is a method call:thisis the object before the dot.incArrowis an arrow, sothisis whatever it was at the top of the script, here the global object, notcounter. Arrows make poor methods.bindreturns a new function withthisfixed forever (callandapplyset it for one call).inc()is a plain call of the same function: in strict modethisisundefined, sothis.countis aTypeError.- The
functioncallback given tosetTimeoutalso gotundefinedinjsc; the arrow callback keptcounter'sthis. (Browsers call timer callbacks withthisset towindow, and Node passes aTimeoutobject; not run here. Do not rely on either.)
Without "use strict" a plain call does not throw. It sets this to the global object, which is worse:
const counter = { count: 0, inc() { this.count++; return this.count; } };
const inc = counter.inc;
print(inc(), globalThis.count); // NaN NaN
undefined + 1 became NaN, and a global variable count was created as a side effect. No error, just a wrong number and a polluted global.
Common mistake: losing this in a callback
The mistake. A method passes a normal function to forEach, setTimeout or an event listener and uses this inside:
class Timer {
constructor() { this.ticks = 0; }
start() { [1, 2, 3].forEach(function () { this.ticks++; }); }
}
new Timer().start();
What actually happens. The callback is called plainly by forEach, so its this is undefined (class bodies are always strict):
Exception: TypeError: undefined is not an object (evaluating 'this.ticks')
@this3.js:3:49
forEach@[native code]
start@this3.js:3:30
global code@this3.js:5:18
The stack trace helps: the top frame has no name (an anonymous function), called from forEach, called from start. Chrome and Node phrase it as Cannot read properties of undefined (reading 'ticks') (not run here).
The fix. Use an arrow, which takes this from start: [1, 2, 3].forEach(() => { this.ticks++; });. The pre-2015 alternatives, .bind(this) or const self = this;, still work and are common in older code.
Closures: functions that remember
A closure is a function together with the variables it could see when it was created. Those variables stay alive as long as the function does, even after the outer function has returned:
function makeCounter(start = 0) {
let n = start; // lives on after makeCounter returns
return { next: () => ++n, peek: () => n };
}
const a = makeCounter(), b = makeCounter(100);
a.next(); a.next();
print(a.next(), b.next(), a.peek(), b.peek(), a.n); // 3 101 3 101 undefined
function makeAdder(x) { return y => x + y; }
print(makeAdder(10)(5), makeAdder(1)(1)); // 15 2
Each call of makeCounter creates a fresh n, so a and b do not interfere. And a.n is undefined: nothing outside can reach n except through next and peek. That is how JavaScript code hid private state for twenty years before classes got #private fields (lecture 4).
The var-in-a-loop trap
The best-known closure bug, run for real:
for (var i = 0; i < 3; i++) setTimeout(() => print("var i =", i), 0);
for (let j = 0; j < 3; j++) setTimeout(() => print("let j =", j), 0);
const fns = [];
for (var k = 0; k < 3; k++) fns.push(() => k);
print("after loop, k =", k, "fns:", fns.map(f => f()).join(","));
after loop, k = 3 fns: 3,3,3
var i = 3
var i = 3
var i = 3
let j = 0
let j = 1
let j = 2
There is only one var i for the whole loop. The three arrows all close over that same variable, and by the time they run (after the loop, because setTimeout waits until the current script is done) it is 3. let in a for header is special: the loop creates a new binding per iteration, so each arrow captures its own j. Notice also that k is still visible after the loop, another var leak. Before 2015 the fix was an immediately invoked function, ((m) => setTimeout(() => print(m), 0))(m), which printed 0, 1, 2 as well; today, just write let.
A measurement: memoize with a closure
Closures are not only a trap. A classic use is memoization: a wrapper that remembers results in a private Map.
let calls = 0;
function fib(n) { calls++; return n < 2 ? n : fib(n - 1) + fib(n - 2); }
function memoize(fn) {
const cache = new Map(); // private to this closure
return function (n) {
if (!cache.has(n)) cache.set(n, fn(n));
return cache.get(n);
};
}
const mfib = memoize(n => n < 2 ? n : mfib(n - 1) + mfib(n - 2));
Results from memo.js (Apple M1 Pro, jsc from macOS 26.6, timed with performance.now()):
fib(25) = 75025: 242785 calls, 1.70 ms
fib(30) = 832040: 2692537 calls, 7.90 ms
fib(32) = 2178309: 7049155 calls, 17.88 ms
run 1: plain fib(30) 6.52 ms, memoized 0.220 ms
run 2: plain fib(30) 6.36 ms, memoized 0.020 ms
run 3: plain fib(30) 6.00 ms, memoized 0.020 ms
run 4: plain fib(30) 6.12 ms, memoized 0.160 ms
run 5: plain fib(30) 6.22 ms, memoized 0.020 ms
832040 memo body calls: 31
second call body calls: 0
The plain version makes 2.7 million calls for fib(30) and the count grows about 1.6 times (the golden ratio) per extra step of n: 11 times from fib(25) to fib(30). The memoized one computes each value once: 31 calls for 0…30, and zero on a second call because every answer is cached. Each memoized run built a fresh cache, so 0.02 ms is the real cost of 31 computations; runs 1 and 4 at 0.16–0.22 ms include warm-up or garbage-collection noise, which is why we show all five. The price is memory: the cache lives as long as mfib does. A cache that never forgets is a memory leak in a long-running server, so real code bounds it.
Honest limits / version notes
- Engine: JavaScriptCore from macOS 26.6.2 (framework 21624.5.1.11.3) via
jsc. Node.js, Deno and browsers were not run. The language rules here (hoisting, TDZ,this, closures, per-iterationlet) are defined by ECMAScript and behave the same everywhere; error messages differ by engine. - Timers:
jscprovidessetTimeout; it is not part of the language. Browsers and Node implement it with their own rules forthisin the callback and for timing (described, not run here). - Versions: arrow functions,
let/constwith block scope, default and rest parameters, andMapcame with ES2015. Before that,varand IIFEs were the only tools, which is why so much old code and so many old answers use them. - Timings are from one machine; the call counts are exact and engine-independent.
Exercises
1. Write once(fn): it returns a function that calls fn the first time and afterwards returns the first result without calling fn again.
2. Predict the output, then run it. Explain the first value.
const obj = { name: "box", greet: () => "hi " + this.name, greet2() { return "hi " + this.name; } };
print(obj.greet(), "|", obj.greet2());
3. Add a reset() to makeCounter so that c.next(); c.next(); c.reset(); c.next() returns 1.
Answers
1.
function once(fn) {
let done = false, result;
return (...args) => { if (!done) { done = true; result = fn(...args); } return result; };
}
const init = once(() => { print("initialising"); return 42; });
print(init(), init(), init());
It printed initialising once, then 42 42 42. done and result live in the closure.
2. Real output: hi undefined | hi box. greet is an arrow, so its this is the script's top-level this (the global object, which has no name), not obj. greet2 uses method shorthand, so this is obj.
3. function makeCounter() { let n = 0; return { next: () => ++n, reset() { n = 0; } }; } printed 1. Both functions close over the same n.
Next
Lecture 3 moves to objects and arrays: map/filter/reduce, spread and destructuring, the shallow copy trap, and why [10, 9, 1].sort() gives 1,10,9.
- Previous: JavaScript lecture 1/6: types, == vs === and NaN
- Next: JavaScript lecture 3/6: objects, arrays and their methods
- Later in the series: classes, prototypes and modules, async, promises and the event loop, performance, memory and numbers
- Compare: Java streams, lambdas and Optional, Python functions, scope and default arguments
- Where this fits: the programming learning roadmap
FAQ
Why does a loop with var and setTimeout print the same number three times?
var creates one variable for the whole function, not one per iteration. The callbacks run after the loop has finished, so they all read the final value: in our jsc run, for (var i = 0; i < 3; i++) setTimeout(() => print(i)) printed 3, 3, 3. With let, each iteration gets its own binding and it printed 0, 1, 2.
What is the difference between an arrow function and a normal function for this?
A normal function gets this from how it is called: obj.method() sets this to obj, a plain call sets it to undefined in strict mode. An arrow function has no this of its own and uses the this of the code around it. That makes arrows right for callbacks inside methods and wrong as object methods.
What does "Cannot access 'x' before initialization" mean?
You read a let or const variable before the line that declares it ran, inside its temporal dead zone. It often happens when a function has its own let x further down that shadows an outer x: the whole function body then refers to the inner one. Move the declaration up or rename one of them.