this;function without agonizing over it.Learn JS Series):Before we start, here are the three worked solutions to last episode's exercises. Type them out, run them, compare them to your own attempts -- that comparison is where the learning actually happens.
Exercise 1 - operations by name:
const mathOps = {
add: (a, b) => a + b,
sub: (a, b) => a - b,
mul: (a, b) => a * b,
};
function compute(a, b, opName) {
const op = mathOps[opName];
if (typeof op !== "function") {
throw new Error(`unknown operation: "${opName}"`);
}
return op(a, b);
}
console.log(compute(6, 4, "mul")); // 24
The insight: bracket access picks the function by its string name, then (a, b) calls it -- a lookup table replacing a switch, exactly the dispatch-table idea from episode 15. The typeof guard keeps an unknown name from blowing up with an ugly TypeError.
Exercise 2 - applying a function twice:
function applyTwice(fn, value) {
return fn(fn(value));
}
console.log(applyTwice((n) => n * 2, 3)); // 12
console.log(applyTwice((s) => s.toUpperCase(), "hi")); // "HI"
The insight: we pass a function in and call it on its own result, composing it with itself. Notice it works for numbers and strings alike -- applyTwice neither knows nor cares what fn does.
Exercise 3 - a greeter factory:
function makeGreeter(greeting) {
return (name) => `${greeting}, ${name}!`;
}
const hi = makeGreeter("Hi");
const bye = makeGreeter("Bye");
console.log(hi("scipio"), bye("scipio")); // "Hi, scipio!" "Bye, scipio!"
The insight: makeGreeter("Hi") returns a function you keep around and reuse. If you tacked on extra parentheses -- makeGreeter("Hi")() -- you would call the returned greeter immediately (with no name), get one throwaway string, and be left with no reusable greeter to hand out later.
Right -- with that behind us, let's compare the two ways to write a function in JavaScript, because you will read and write both of them constantly, in every codebase you ever touch.
So far in this series we have written functions the classic way, with the function keyword. In ES2015 (the big 2015 language update, also called ES6) JavaScript gained a second, more compact way to write them: the arrow function. You have already seen me sneak arrows into callbacks like nums.map((n) => n * 2), because they are lighter to read there. But arrows are not just "shorter functions". They carry one deep behavioural difference that quietly decides, in a lot of real code, which of the two you must reach for. This episode takes both apart, side by side, so you know exactly what you are choosing between.
Arrow functions have a syntax that collapses down in stages, and it is worth walking through those stages deliberately so that the shortest form does not look like line noise later. Here is a regular function value and its direct arrow equivalent, written as verbosely as an arrow can be written:
const addLong = function (a, b) {
return a + b;
};
const addArrow = (a, b) => {
return a + b;
};
console.log(addLong(2, 3), addArrow(2, 3)); // 5 5
At this fullest form the two are nearly identical -- parameters in parentheses, a body in braces, an explicit return. The only visible change is that function before the parentheses becomes => after them. So far, arrows have bought us almost nothing. The savings start on the next step.
When the whole body is a single expression, an arrow lets you drop the braces and the return keyword. The value of that one expression becomes the return value automatically. This is called the implicit return, and it is the arrow's signature feature:
const add = (a, b) => a + b; // implicit return, no braces, no 'return'
const square = (n) => n * n;
const greet = (name) => `Hi, ${name}`;
console.log(add(2, 3), square(4), greet("scipio")); // 5 16 "Hi, scipio"
Read (a, b) => a + b as "given a and b, give back a + b". No braces means "this is an expression, and its result is the return value". The moment you add braces, you are back to a normal block that needs an explicit return -- a very common beginner slip is writing (n) => { n * n } and wondering why it returns undefined (the braces make it a body, and a body with no return gives back undefined). Braces = block = you must return. No braces = expression = returned for you. Keep those two apart in your head.
And when there is exactly one parameter, the parentheses around it become optional (though quite some style guides, and I among them, keep them for consistency):
const double = (n) => n * 2; // parens around one param (my preference)
const triple = n => n * 3; // legal too: one param, no parens
const noArgs = () => "constant"; // ZERO params still need the empty ()
console.log(double(21), triple(7), noArgs()); // 42 21 "constant"
Note the asymmetry: one parameter may drop its parentheses, but zero parameters still need an empty () to mark "this is a function of no arguments". Two or more parameters always need parentheses. This stacking of shortcuts -- implicit return plus optional parens -- is exactly why arrows dominate in callbacks. Compare nums.map(function (n) { return n * 2; }) with nums.map((n) => n * 2). Same behaviour, a third of the visual weight, and your eye can take in the intent (double each number) without wading through ceremony.
There is one syntax trap that is worth learning now, up front, because it bites absolutely everyone exactly once and then never again. Suppose you want an arrow to implicitly return an object literal. Your instinct is to write (name) => { name: name }. But those braces are ambiguous -- JavaScript reads a { right after the arrow as the start of a function body, not an object literal. So name: gets parsed as a label (a rarely-used, ancient JS feature), name as a bare expression statement, and the whole thing returns undefined:
// const makeUser = (name) => { name: name }; // BROKEN: {} read as a body -> returns undefined
const makeUser = (name) => ({ name: name }); // wrap the object in () -> returns the object
console.log(makeUser("scipio")); // { name: 'scipio' }
The fix is one character on each side: wrap the object literal in parentheses, => ({ ... }). Now the ( tells the parser "what follows is an expression", the braces are unambiguously an object, and the implicit return does its job. Burn that => ({ ... }) shape into memory, because "my arrow keeps returning undefined when I try to return an object" is one of the most common head-scratchers in the language, and it has a one-character solution.
Syntax aside, arrows and regular functions differ in one deep, behavioural way -- and this, not the brevity, is the whole reason you actually choose one over the other: arrow functions do not have their own this.
Let me unpack that, because it is the heart of the episode. A regular function gets a this that is decided by how it is called (the full five rules are literally the next episode, so I will keep it light here). An arrow function is different: it has no this of its own at all. When you write this inside an arrow, JavaScript looks outward to the surrounding scope where the arrow was written, and uses that this. This is called lexical this, and it is the exact same "look outward to where the code was written" behaviour as the lexical scope we met back in episode 10 -- just applied to this instead of to variable names.
That sounds abstract, so here is the classic situation where it bites. Imagine an object with a method that hands back an inner function to be called later. With a regular inner function, this inside it is decided by how that inner function eventually gets called -- and that is almost never the object you wanted:
const timer = {
seconds: 0,
// a REGULAR inner function loses the connection to 'timer':
brokenTick: function () {
return function () {
// when this inner function is later called on its own,
// 'this' is NOT 'timer' -- so 'this.seconds' is wrong (or throws)
this.seconds++;
return this.seconds;
};
},
};
The inner regular function's this gets rebound by whoever calls it later, so this.seconds does not point at timer and the counter breaks. Now watch an arrow fix it cleanly, precisely because the arrow refuses to have its own this and instead borrows the one from the method it was written inside:
const counter = {
count: 0,
makeIncrementer() {
// 'this' here is 'counter' (regular method, called as counter.makeIncrementer())
return () => {
this.count++; // the ARROW borrows makeIncrementer's 'this' = counter
return this.count;
};
},
};
const inc = counter.makeIncrementer();
console.log(inc()); // 1
console.log(inc()); // 2 -- 'this.count' correctly refers to counter
console.log(inc()); // 3
Because the arrow inherited this from makeIncrementer (where this is counter), the increment works even though we call inc() completely detached from the object. This lexical-this behaviour is precisely why arrows became the default for callbacks nested inside methods: they quietly solve the single most infamous this-loss bug in JavaScript's history. Before arrows existed, people worked around it with const self = this; at the top of the method, or with .bind(this) on the inner function (we cover bind in two episodes). Arrows made all of that boilerplate disappear.
Here is the flip side, and it is just as important: the very feature that makes arrows great for callbacks makes them wrong as top-level object methods. If you write an object's method as an arrow, its this is not the object -- it is whatever this was in the scope surrounding the object (at the top level of a module that is undefined or the module scope, in a browser script it is often the global object). So the arrow method cannot reach the object's own properties:
const user = {
name: "scipio",
greetBad: () => `Hi, I am ${this.name}`, // ARROW: 'this' is NOT user
greetGood() {
return `Hi, I am ${this.name}`; // regular method: 'this' IS user
},
};
console.log(user.greetGood()); // "Hi, I am scipio"
console.log(user.greetBad()); // "Hi, I am undefined" -- arrow has no object 'this'
greetGood is a normal method, so when you call user.greetGood() its this is user, and this.name is "scipio". greetBad is an arrow, so its this was fixed at write-time to the surrounding scope -- which has no name -- and you get undefined. So the guidance splits cleanly and memorably: use a regular function (method shorthand) for object methods that need this, and use an arrow for callbacks nested inside those methods, or anywhere you want to hold on to the surrounding this.
While we are cataloguing what arrows lack, two more absences are worth banking, because they occasionally trip people who reach for an arrow out of habit.
First, arrows have no arguments object. In a regular function, arguments is a special array-like value holding every argument that was passed, even ones you did not name. An arrow does not get its own -- like this, a bare arguments inside an arrow refers to the enclosing function's arguments (or is simply not defined at the top level). In modern code this is a non-issue: rest parameters (...args, which we cover next phase) are clearer than arguments anyway, and they work perfectly in arrows:
const sumRegular = function () {
// 'arguments' exists here: array-like of everything passed
return Array.from(arguments).reduce((a, b) => a + b, 0);
};
const sumArrow = (...nums) => nums.reduce((a, b) => a + b, 0); // rest params, arrow-friendly
console.log(sumRegular(1, 2, 3), sumArrow(1, 2, 3)); // 6 6
Second, arrows cannot be used with new -- they are not constructors (constructors and the new keyword are a Phase 3 topic). Trying new (() => {})() throws a TypeError. Again, rarely a problem in day-to-day code, but part of the complete picture, and a reason you will never see an arrow used as a class-like factory built with new.
You do not need to agonize over this decision on every single function you write. Here is a practical rule that will serve you well for years:
map, filter, reduce), and any helper nested inside a method that needs to keep the outer this.function or method shorthand) for object methods, for the rare cases that genuinely need their own this or the arguments object, for constructors, and when you specifically want the hoisting behaviour we saw in episode 9 (function declarations are hoisted; arrows assigned to a const are not).Here are both halves of that rule in action, in the shapes you will actually type:
// arrows shine here -- short transforms, the outer 'this' is irrelevant:
const nums = [1, 2, 3, 4];
console.log(nums.filter((n) => n % 2 === 0).map((n) => n * 10)); // [20, 40]
// a regular method shines here -- it needs 'this' to be the object:
const account = {
balance: 100,
deposit(amount) {
this.balance += amount; // 'this' must be the account
return this.balance;
},
// ...and an arrow is correct for a callback INSIDE the method:
applyAll(amounts) {
amounts.forEach((amt) => {
this.balance += amt; // arrow keeps 'this' = account
});
return this.balance;
},
};
console.log(account.deposit(50)); // 150
console.log(account.applyAll([10, 20])); // 180
Look at applyAll: it is a regular method (so this is the account), and inside it the forEach callback is an arrow (so it borrows that same this). That pairing -- regular method on the outside, arrow callback on the inside -- is the single most common and most correct arrangement you will write. Having said that, if you had made the forEach callback a regular function, this.balance inside it would have broken, and you would be back to the bug from earlier. The rule is not arbitrary; it falls straight out of the this behaviour.
Many of you came over from the Learn Python Series (and a few from Rust and Go), so a quick look sideways helps place JavaScript's arrows in context -- and shows that the underlying ideas are not JS quirks but broadly shared tools.
Python has a compact function expression too, the lambda, but it is deliberately limited to a single expression -- there is no multi-statement lambda. Python's arrow-equivalent for a one-liner reads very close to JS:
double = lambda n: n * 2
add = lambda a, b: a + b
print(double(21), add(2, 3)) # 42 5
# functions in a dict -- the same dispatch-table idea from episode 15
ops = {"mul": lambda a, b: a * b, "sub": lambda a, b: a - b}
print(ops["mul"](6, 7)) # 42
But note the crucial difference: Python's lambda does not change how self (Python's this) works, because Python passes the receiver explicitly as the first parameter. Python simply never had JavaScript's "this gets rebound by the caller" problem, so it never needed a lexical-this fix. JavaScript's arrow solves a problem that is specific to how JS binds this -- which is exactly why the arrow bundles a this rule in with its syntax, and lambda does not.
Rust has closures with a |params| body syntax that is its arrow analogue, and, like arrows, a closure captures variables from the scope where it was written:
fn main() {
let factor = 3;
let triple = |n: i32| n * factor; // closure captures 'factor' lexically
println!("{}", triple(5)); // 15
}
The lexical capture matches the spirit of the arrow's lexical this -- both look outward to where the code was written. Rust just makes the capture rules explicit and checked by the compiler, where JavaScript keeps it loose and implicit. Same idea, different amount of paperwork -- the recurring theme every time we hold these languages up next to each other. ;-)
Go uses func(params) returnType { ... } for its function literals, which also close over surrounding variables:
package main
import "fmt"
func main() {
factor := 3
triple := func(n int) int { return n * factor } // closes over 'factor'
fmt.Println(triple(5)) // 15
}
Go has no separate short-arrow form and no this-rebinding problem (methods take an explicit receiver, much like Python), so a Go function literal is closest in feel to a JavaScript arrow that is being used purely for its brevity and its lexical capture -- minus the this machinery. The throughline across all four: a compact function expression that captures its surrounding scope. JavaScript is the odd one that also folds a this rule into that same syntax, because JS's this needed the fix and the others' did not.
Three exercises, increasing in difficulty. Type them out and run them -- reading is genuinely not the same as knowing. Full solutions open the next episode.
function (x) { return x + 1; }, function (a, b) { return a * b; }, and function () { return "hello"; }. (Watch the zero-parameter one -- it still needs ().)makePoint(x, y) that returns the object { x, y } using the implicit-return syntax. Then, in a comment, show the BROKEN version without the wrapping parentheses and explain in one sentence why it returns undefined.stopwatch with a laps array and a record() method written as a regular function. Inside record(), use [10, 20, 30].forEach(...) with an arrow callback that pushes each value onto this.laps. Confirm the arrow can see the object's this, and then, in a comment, describe what would break if you rewrote that callback as a regular function.return, then implicit return for a single expression, then optional parentheses for a single parameter (but zero params always need ()).=> ({ ... }), or the braces get read as a function body.this of their own -- they use the surrounding (lexical) this, while regular functions get a this decided by how they are called (full rules next episode).this) but wrong as object methods themselves (their this is not the object).arguments object and cannot be used with new -- reach for rest parameters and regular functions respectively when you need those.this, arguments, new, or hoisting.this rule into the syntax, because JS's this is the one that needed fixing.Next episode is the big one -- the single most important idea in the whole language, the mechanism that has been quietly powering every factory and wrapper we have written since episode 15. We take it fully apart, with real, practical examples of the private state it makes possible.