Write binary by hand
Finish toBinary(n) so it turns any number from 0 to 255 into exactly eight binary digits, without using toString(2). toBinary(65) should give "01000001".
LEARN TO PROGRAM · CHAPTER 02 · BASICS
Programs are mostly about values: numbers, text, true or false, lists of things. Inside the machine they are all the same stuff, rows of bits in memory. What makes 65 a number in one place and the letter A in another is only how the program reads it.
This chapter opens memory up, one cell at a time: how numbers, decimals, text and "nothing" become bits, how the names in your code find their values, and who cleans up afterwards. Scroll, and the 3D follows.
01 · BITS AND BYTES
A bit is the smallest piece of information there is: one switch, off or on, written 0 or 1. In the memory chips of your computer each bit is a tiny capacitor that either holds a charge or doesn't, guarded by one transistor. Robert Dennard patented that one-transistor cell at IBM in 1968, and DRAM still works that way. The transistor and Memory show the hardware.
Memory hands bits out eight at a time. Eight bits make a byte, and every byte in memory has its own number, its address, like houses on a very long street. With 8 switches there are 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 = 256 patterns.
To read a pattern as a number, give each bit a place value, doubling from right to left: 1, 2, 4, 8, 16, 32, 64, 128. Add up the places that are on. 01000001 is 64 + 1 = 65. That is all binary is: counting where each column is worth twice the one to its right, instead of ten times. In the 3D the enlarged byte counts through some values; watch the place values light up.
Press Step through. Notice how 0111 rolls over to 1000, just as 0999 rolls over to 1000 in decimal.
Binary is long to write, so programmers use hexadecimal (base 16, digits 0–9 then a–f). One hex digit is exactly four bits, so a byte is always exactly two hex digits: 01000001 is 41. That is why memory dumps, addresses and colours like #ff9d4d are written in hex.
Change 65 to 97, then to 200. The last line still tries to read it as a letter.
02 · INTEGERS
Whole numbers are stored in a fixed number of bits: 8, 16, 32 or 64. Fixed width means a fixed range. An unsigned byte holds 0 to 255; add 1 to 255 and the carry falls off the left end, leaving 0. That is called overflow, and the number wraps round like a car's odometer.
Negative numbers use two's complement. The top bit keeps its place but counts as negative: in a byte it is worth −128, so 10000000 is −128 and 11111111 is −128 + 127 = −1. The wheel in the 3D does the same with 4 bits, where the top bit is worth −8: count up from 0 and after +7 comes −8.
Why this odd scheme? Because the same adding circuit then works for positive and negative numbers alike, there is only one zero, and negating is simple: flip every bit, then add 1.
Typed arrays give JavaScript real fixed-width integers. Try Uint16Array and 65535.
Overflow is not a quirk of toy examples. Many systems store time as seconds since 1 January 1970 in a signed 32-bit integer, and that counter runs out in 2038:
The last line is the date a wrapped 32-bit clock jumps back to.
JavaScript is unusual here: its Number is not an integer type at all but a 64-bit float (the next act). Whole numbers are exact only up to 2⁵³ − 1, called Number.MAX_SAFE_INTEGER. For bigger integers there is BigInt, which grows as needed and never overflows.
03 · FLOATING POINT
For fractions, computers use floating point: scientific notation, in base 2. A double (IEEE 754 binary64) spends its 64 bits on three fields, the three colours in the 3D: 1 sign bit, 11 exponent bits that say how far to slide the point, and 52 fraction bits for the digits. The value is ± 1.fraction × 2exponent. Every number in JavaScript is one of these.
The catch is that most decimal fractions have no exact binary form. You can't write 1/3 in decimal with a finite number of digits (0.3333…); in binary you can't write 1/10 either. It comes out as 0.000110011001100… repeating forever. The 52 fraction bits hold the nearest value they can. You can see the repeating 1001 1001 in the 3D.
The last two lines differ only at the very end. Try bits(0.5) and bits(-2): numbers made of halves and quarters are exact.
0.1 and 0.2 are each stored a hair too big. Their sum rounds to the double just above the one nearest 0.3. Near 0.3, neighbouring doubles are 2⁻⁵⁴ apart, about 0.000000000000000056, and the number line in the 3D shows four of them: 0.1 + 0.2 lands exactly one step too high.
The first line is the exact value the computer holds when you write 0.1. Adding the same three numbers in a different order gives a different answer.
This is not a JavaScript bug. Python, Java, C and nearly every language use the same IEEE 754 doubles and print the same 0.30000000000000004. The rules that follow:
===. Check that they are close, with a tolerance that suits the size of your numbers.Infinity, NaN ("not a number", which isn't even equal to itself), and a separate -0.Graphics cards and AI models often go the other way and use smaller floats, 32 or even 16 bits, trading precision for speed: see The GPU.
04 · TEXT AND UTF-8
Text is numbers too. Unicode gives every character in every writing system a number called its code point, written U+ and hex: A is U+0041 (65), é is U+00E9, 🙂 is U+1F642.
An encoding turns code points into bytes. The one that won is UTF-8: plain English letters take 1 byte (so old ASCII text is already valid UTF-8), accented letters 2, most other scripts 3, and emoji 4. The first bits of every byte say what it is, yellow in the 3D:
0xxxxxxx: a whole character in one byte.110xxxxx, 1110xxxx, 11110xxx: the first byte of a 2-, 3- or 4-byte character.10xxxxxx: a continuation byte. Never the start of a character, so a program dropped into the middle of text can always find the next one.Try your own name, Japanese, or a flag emoji. Look closely at the last two lines.
That last surprise: JavaScript strings are UTF-16 inside. Each element is a 16-bit unit, and anything beyond U+FFFF, like most emoji, takes two of them. So "🙂".length is 2. Use for…of or [...str] to walk real characters. Even those aren't always what a person calls one character: 👍🏽 is two code points, a thumb and a skin tone.
Bytes on their own don't say which encoding made them. Read UTF-8 with the wrong one and you get mojibake:
05 · TRUE, FALSE, NOTHING
A boolean is true or false: the answer to every comparison, and what every if decides on. It is one bit of information, though it is usually stored in a whole byte or more, because a byte is the smallest piece of memory with an address.
JavaScript has two ways to say "no value", the middle and right boxes in the 3D:
undefined: nothing was ever put here. A variable declared without a value, a missing property, a function that returns nothing.null: deliberately empty. A programmer wrote it to say "there is no value, on purpose".Most languages manage with one: None in Python, NULL in C (a pointer to address 0), null in Java.
Look at typeof null.
typeof null says "object", and that is a bug. In the first JavaScript engine every value carried a small type tag; objects had tag 0, and null was the null pointer, all zero bits, so it read as an object. Fixing it would break existing websites, so it stays.
if accepts any value, not just booleans. It treats exactly these as false, the falsy values: false, 0, -0, 0n, "", null, undefined and NaN. Everything else is truthy, including "0", "false", [] and {}. The gate in the 3D lets them through one at a time.
The last line shows why ?? exists: a volume of 0 is a real setting, not a missing one.
06 · VARIABLES
A variable is a name for a value. let score = 10; puts 10 in memory and ties the name score to it. In the 3D, each name gets a slot in the program's global frame.
The = sign is not "equals" as in maths. It is an instruction: work out the right-hand side, then make the name on the left hold the result. That is why score = score + 5 makes sense: read the old value, add 5, store the new one.
Press Step through and watch the variables panel. best got a copy of 10, so changing score later doesn't touch it.
Three keywords make names in JavaScript. const: the name can never be pointed at another value. let: it can. var: the old way, with looser rules you don't need; avoid it. Use const by default and let when you must reassign.
A TypeError, at run time. Also try using a let variable on a line before it is declared: that is a ReferenceError.
Names are for people. A C compiler turns every local name into a fixed offset in the function's frame, and a JavaScript engine turns them into register or slot numbers; the names themselves are gone by the time the machine runs.
Look at the last line in the 3D: player doesn't hold the text "Ada". It holds an address, @1f40, and an arrow to where the string really lives. Where that is, and why, is the next act.
07 · STACK AND HEAP
A running program splits its memory by purpose. Two regions matter most.
The stack holds function calls. Each call gets a frame, a block holding its parameters and local variables. Calling pushes a frame on top; returning pops it off. Freeing a whole frame is just moving one pointer, so the stack is very fast, but everything in it dies when its function returns, and it is small. In the 3D, area(4, 3) pushes a frame, computes 12, and the frame vanishes when it returns.
The heap is for everything that is big, that grows, or that must outlive the function that made it: objects, arrays, strings, closures. It can be allocated anywhere, so it is found by address, and someone has to give it back when it is no longer needed: you, in C, or a garbage collector. The user object in the 3D lives on the heap, and so do the string and the array inside it.
Step through and watch the call stack line: area appears while it runs and is gone after it returns.
The stack has a fixed size: on Windows, 1 MB per thread by default. A function that calls itself forever keeps pushing frames until there is no room left: a stack overflow.
The exact count depends on your browser and how big each frame is.
JavaScript hides where things go, and engines are free to choose. A good mental model: primitives (numbers, booleans, and so on) behave as if they sit in the slot; objects always live on the heap, and the slot holds a reference. On real x86 and ARM processors the stack grows downwards, towards lower addresses; chapter 4 follows it call by call.
08 · REFERENCES
This is the idea in this chapter that causes the most bugs. Copying a primitive copies the value. Copying an object variable copies the reference, the arrow, so both names now point at the same object. Change it through one name and the other sees the change. That is called aliasing.
Step through it alongside the 3D: two arrows into one { score } object, then a third arrow into a new one.
So === on objects asks "is this the same object?", not "do they look the same?". {} === {} is false: two objects, two addresses.
Spread ({ ...p }) makes a shallow copy: a new outer object, but anything nested inside is still shared. structuredClone makes a deep copy, all the way down.
Passing an object to a function passes a copy of the reference. The function can change the object, and the caller will see it; but re-pointing its parameter at a new object changes nothing outside. This is sometimes called call by sharing.
Predict the output before you run it.
09 · MUTABILITY
A mutable value can be changed in place. An immutable one can't: to "change" it you make a new value. In JavaScript, all primitives are immutable, strings included, while objects and arrays are mutable.
In the 3D: the array grows in place, toUpperCase() makes a second string, and the frozen object sits in a cage that turns writes away.
Notice that const doesn't make anything immutable. It only stops the name being pointed elsewhere. Object.freeze makes an object immutable, but only one level deep.
Why care? Shared mutable state is one of the classic sources of bugs: two parts of a program hold the same object, one changes it, and the other is surprised. Much modern code prefers to make new values instead of changing old ones. In 2023 JavaScript added copying versions of the array methods that change things in place: toSorted, toReversed, toSpliced and with.
10 · GARBAGE COLLECTION
Heap memory has to be given back, or a long-running program eventually runs out. In C you do it yourself, with free(). Forget, and memory leaks. Free too early and later code reads memory that now belongs to something else (use after free). Free twice and the allocator's own records get corrupted. These memory-safety bugs are not rare.
JavaScript, Python, Java, Go and C# use a garbage collector (GC) instead. Its rule: an object is alive if it can be reached from a root (a global variable, or a variable in a frame on the stack) by following references. Everything else is garbage, because no code can ever get to it again.
The classic algorithm is mark and sweep, invented by John McCarthy for Lisp in 1960. Mark: start at the roots and follow every arrow, marking what you reach. Sweep: free everything unmarked. In the 3D, d = null cuts the only path to D and E. They still point at each other, but nothing can reach them, so the sweep takes both.
Step through and watch marked fill up as mark calls itself along the arrows.
Real collectors are cleverer. V8, the engine in Chrome and Node.js, relies on the observation that most objects die young: new objects go in a small young generation that is collected often and cheaply, and the survivors move to an old generation that is collected rarely, much of the work running on helper threads. CPython counts references instead, freeing an object the moment its count reaches zero, and runs a separate cycle collector for loops like D and E.
A garbage collector frees what you can't reach, not what you don't need. Keep a reference by accident, in a cache or a list of event listeners, and the memory stays alive forever:
11 · JS, PYTHON, C
Every language makes the same choices this chapter has covered, but picks different defaults. The 3D shows x = 42 in all three.
In C a variable is a box: a fixed number of bytes at a fixed place. An int is 4 bytes on today's mainstream platforms, and x86 and ARM store the lowest byte first (little-endian), so 42 sits in memory as 2a 00 00 00. Unsigned integers wrap; signed overflow is undefined behaviour, meaning the compiler may assume it never happens. Heap memory is yours to malloc and free.
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main(void) {
int x = 42; /* 4 bytes, in main's stack frame */
unsigned char *b = (unsigned char *)&x;
printf("%zu bytes: %02x %02x %02x %02x\n", sizeof x, b[0], b[1], b[2], b[3]);
uint8_t small = 255;
small = small + 1; /* unsigned types wrap: 0 */
printf("%u\n", small);
int *p = malloc(sizeof *p); /* 4 bytes on the heap */
*p = 7;
printf("%d\n", *p);
free(p); /* you give it back, exactly once */
return 0;
}
4 bytes: 2a 00 00 00
0
7
In Python, even 42 is an object on the heap, with a reference count and a type, which is why it takes 28 bytes rather than 4. Names point at objects exactly as in JavaScript. Integers grow as big as they need and never overflow, floats are the same IEEE 754 doubles, and len counts code points, so an emoji is 1.
import sys
x = 42
print(sys.getsizeof(x)) # 28: a whole object, not 4 bytes
print(2 ** 100) # ints grow; they never overflow
print(0.1 + 0.2) # the same IEEE 754 doubles as JavaScript
print(len("🙂"), len("🙂".encode("utf-8")))
a = [1, 2]
b = a # names point at objects here too
b.append(3)
print(a, a is b)
print(sys.getrefcount(a)) # a, b, and the call's own argument
28
1267650600228229401496703205376
0.30000000000000004
1 4
[1, 2, 3] True
3
JavaScript gives you one Number type (a double) plus BigInt, UTF-16 strings, objects by reference, and a garbage collector. Underneath, V8 cheats for speed: small integers, called Smis, are stored inside the slot itself, shifted one bit left with a 0 tag bit at the end (the pink bit in the 3D), while a number like 4.2 is boxed as a HeapNumber on the heap. You never see the difference: the language only promises the behaviour.
PRACTICE
Type the code yourself rather than pasting it: producing an answer builds memory that reading does not. Use the hints one at a time.
Finish toBinary(n) so it turns any number from 0 to 255 into exactly eight binary digits, without using toString(2). toBinary(65) should give "01000001".
In two's complement the leftmost bit of a byte is worth −128 instead of +128; the other seven keep their usual values. Finish fromByte(bits) so it reads an 8-character string of bits as a signed number.
This shopping cart adds up to 0.9999999999999999, so the check says it didn't charge exactly $1. Rewrite it to add whole cents in a variable called totalCents, so totalCents is exactly 100 and ok is true.
Finish utf8Length(str) so it returns how many bytes the string takes in UTF-8, using only the code point ranges in the comment (no TextEncoder).
Every new player should start at volume 5. But after player one turns it up to 11, player two starts at 11 too. Fix newPlayer so each player gets its own settings object.
Write deepCopy(value) without structuredClone. It must copy objects and arrays all the way down, so changing the copy never changes the original. Numbers, strings, booleans and null can be returned as they are.
RECALL
Answer each one before you open it. Then come back tomorrow, and again next week, and answer them cold: retrieval spaced over days is the most reliable study method we know of. Why that works.
How many different patterns can one byte hold, and why that many?
In an 8-bit signed integer, what is 11111111, and what is 127 + 1?
Why is 0.1 + 0.2 !== 0.3 in JavaScript?
Why is "🙂".length 2 in JavaScript, and how many UTF-8 bytes is it?
const list = [1, 2]; list.push(3); Is that allowed? What about list = [];?
What does a tracing garbage collector free, and why can't reference counting alone free a cycle?
GO FURTHER
ON THIS SITE
SOURCES