null inside the engine
Trace JavaScript null through V8 oddballs, typeof history, undetectable maps, document.all, null prototypes, and prototype lookups.
- 01Explain how V8 stores nullSay why V8 has exactly one
null, which ready-made answers it keeps, what its undetectable mark does, and how other engines storenull. - 02Read the instructions behind null checksMatch
=== null,== null, andtypeof x === "object"to the small instructions V8 makes for them, and explain whydocument.all == nullis true. - 03Use objects with a null prototype safelyKnow when an object with no prototype helps, why V8 stores it in dictionary mode, and how every property lookup stops at
null.
One null, made once
You know null from everyday JavaScript. It is the value you write on purpose to say "empty" or "nothing here". This lesson shows how a JavaScript engine stores it. An engine is the program that runs your code. We use V8, the engine inside Chrome and Node.js. Every fact here was checked in Node 22, which uses V8 version 12.4.
Inside V8, there is exactly one null. V8 makes it once, when it starts, and never makes another one. V8 calls a few special built-in values oddballs, and null is one of them. A variable that holds null only stores a pointer to it. A pointer is a memory address: it says where a value is.
In V8, null is one oddball object that lives in read-only space: a part of memory that nothing is allowed to change. The object keeps a few ready-made answers:
- As a number, it is
0. - As a string, it is
"null". typeofgives"object".
V8 also gives every object a map: V8's record of a value's type and shape. The map of null has a mark called undetectable. This mark helps V8 answer x == null quickly.
If you want the everyday rules for null first, read null for beginners. This lesson also uses the idea of a tagged pointer: a stored pointer with a small mark that says "this is a pointer, not a small number". The Tagged values, Smis & heap numbers lesson explains it. This lesson comes right after undefined inside the engine.
A shop keeps one printed "empty" label. Any box with nothing in it points to that label. Nobody prints a new label for each box, and nobody can change the words on it.
- In real life: There is one printed "empty" label
- In JavaScript: There is one
nullin V8 - In real life: Many boxes point to that same label
- In JavaScript: Every
nullin your code is a pointer to that one value - In real life: The words on the label are printed and can't change
- In JavaScript:
nullkeeps fixed answers for number, string, andtypeof
Where V8 keeps null and its answers
V8 is written in the C++ language, and its source code is public. So we can read how it builds null. The file oddball.h describes oddballs. Its comment says that four values are oddballs: null, undefined, true, and false.
Each oddball keeps a few ready-made answers, so V8 never has to work them out again. V8 calls these saved answers cached fields:
to_number_rawandto_number: the value as a number.to_string: the value as a string.type_of: whattypeofgives.kind: a small number that says which oddball this is. Fornull, it iskNull = 3.
// V8 12.4.254.21, src/objects/oddball.h// The Oddball describes objects null, undefined, true, and false.// [to_number_raw]: Cached raw to_number computed at startup.// [to_string]: Cached to_string computed at startup.// [to_number]: Cached to_number computed at startup.// [typeof]: Cached type_of computed at startup.static constexpr uint8_t kNull = 3;static constexpr uint8_t kUndefined = 4;Another file, setup-heap-internal.cc, builds null when V8 starts. Here is what the lines below do:
- Line 2 makes a separate map just for
null, callednull_map. - Line 4 puts the undetectable mark on that map.
- Lines 5 and 6 fill in the ready-made answers: the string
"null", the number zero, thetypeofanswer"object", and the kindkNull.Smi::zero()is the number 0 as a Smi, V8's name for a small whole number.
// V8 12.4.254.21, src/heap/setup-heap-internal.ccALLOCATE_AND_SET_ROOT(Map, null_map, Map::kSize);InitializePartialMap(null_map, meta_map, ODDBALL_TYPE, sizeof(Null));roots.null_map()->set_is_undetectable(true);Oddball::Initialize(isolate(), factory->null_value(), "null", handle(Smi::zero(), isolate()), "object", Oddball::kNull);%DebugPrint(null);%DebugPrint is a special V8 helper. It prints what V8 stores for a value. It only works when you start Node with node --allow-natives-syntax, so this block can't run on this page. The lesson's test runs it and checks the parts that are the same on every run:
DebugPrint: ... [Oddball] in ReadOnlySpace: #null... [Map] in ReadOnlySpace - type: ODDBALL_TYPE - stable_map - undetectable - non-extensible - prototype: ... <null>[Oddball] in ReadOnlySpace: #null: it is thenulloddball, kept in read-only space.type: ODDBALL_TYPE: the map says its type is oddball.undetectable: the map has the undetectable mark.prototype: ... <null>: the map's prototype isnullitself.
The test skips memory addresses and object sizes, because they change from build to build. This Node build has pointer compression turned off: a trick that stores pointers in fewer bytes. It also has the sandbox turned off: a fence that limits which memory V8 can touch. Chrome usually turns both on and may use a newer V8.
console.log(Number(null));console.log(String(null));console.log(null + 1);console.log(null >= 0);console.log(null == 0);Here is what each line prints:
- Line 1 prints
0:nullas a number. - Line 2 prints
null:nullas a string. - Line 3 prints
1. The+turnsnullinto0first, and0 + 1is1. - Line 4 prints
true. The>=also turnsnullinto0, and0 >= 0is true. - Line 5 prints
false. Loose equality,==, has its own rule fornull: it is only loosely equal tonullandundefined. Sonull == 0is false.
null when it starts and keeps three answers on it. So Number(null), String(null), and typeof null just read a stored answer.Other engines store null in different ways. Read each row of the table as: the engine, how it stores null, and what its source code says.
| Engine | How it stores null | What its source code says |
|---|---|---|
V8, for null | One read-only object, an oddball, with the kind kNull = 3. | It saves 0 as the number, "null" as the string, and "object" for typeof. |
V8, for undefined | A different read-only oddball, with its own map. | Its typeof answer is "undefined". Its map has the undetectable mark too. |
| JavaScriptCore (Safari) | A fixed value stored right inside the value. No object in memory. | JSCJSValue.h says ValueNull = OtherTag, and uses it for JSNullTag. |
| SpiderMonkey (Firefox) | A value with a type mark that says "null". | Value.h gives the type number JSVAL_TYPE_NULL = 0x04 and the mark JSVAL_TAG_NULL. |
JavaScriptCore JSCJSValue.h: ValueNull = OtherTag; JSValue(JSNullTag) stores ValueNull. SpiderMonkey js/public/Value.h: JSVAL_TYPE_NULL = 0x04; JSVAL_TAG_NULL stores a null payload. V8 12.4 oddball.h: Null is an Oddball subclass; kind kNull = 3.JavaScriptCore is the engine inside Safari. Its file JSCJSValue.h stores null as a fixed value, ValueNull, right inside the value itself. No object in memory is needed. SpiderMonkey is the engine inside Firefox. Its file Value.h gives null the type number JSVAL_TYPE_NULL = 0x04. Every engine gives the same JavaScript results. Only the way it stores null is different.
Why typeof null says "object"
The rules of JavaScript, called the spec, still say that typeof null gives "object". The reason is older than V8. It goes back to the first JavaScript engine, built at Netscape in 1995.
That engine stored every value in one 32-bit word: a number made of 32 bits, where each bit is a 0 or a 1. The last 3 bits were a type tag, a small code that said what kind of value the word held. Axel Rauschmayer's archived 2ality article lists the old tags:
000: an object.1as the last bit: a whole number.010: a decimal number.100: a string.110: a boolean.
null was stored as a word where all 32 bits are 0. The old Netscape source shows this in jsapi.h: JSVAL_OBJECT is 0x0, and JSVAL_NULL is OBJECT_TO_JSVAL(0). In jsapi.c, the old typeof code checks for the object tag, JSVAL_IS_OBJECT, before it checks for numbers, strings, or booleans. All those zeros look like the object tag, so the answer was "object".
const JSVAL_OBJECT = 0x0;const JSVAL_INT = 0x1;const JSVAL_STRING = 0x4;const JSVAL_BOOLEAN = 0x6;const JSVAL_VOID = 0xfffffff0; function tagBits(word) { return (word & 0x7).toString(2).padStart(3, "0");} function typeof1995(word) { const tag = word & 0x7; if (word === JSVAL_VOID) return "undefined"; if (tag === JSVAL_OBJECT) return "object"; if (word & JSVAL_INT) return "number"; if (tag === JSVAL_STRING) return "string"; if (tag === JSVAL_BOOLEAN) return "boolean"; return "number";} const nullWord = 0x00000000;console.log("null", tagBits(nullWord), typeof1995(nullWord));console.log("object", tagBits(0x00001000), typeof1995(0x00001000));console.log("string", tagBits(0x00003004), typeof1995(0x00003004));This code is a teaching model, not the real 1995 engine. It turns each value into a 32-bit word and reads the last 3 bits. In plain words, line by line:
- Line 1 sets the object tag to
0. - Line 7 starts
tagBits. It keeps only the last 3 bits, withword & 0x7, and shows them as 0s and 1s. - Line 11 starts
typeof1995, the oldtypeofdecision. - Line 14 gives
"object"when the tag is000. Nothing checks fornullbefore it. - Line 21 makes the word for
null: all zeros. - Line 22 prints
null 000 object.
Step through a small model of how the 1995 engine decided typeof. It is teaching code, not how engines store values today.
script
const JSVAL_OBJECT = 0x0;const JSVAL_INT = 0x1;const JSVAL_STRING = 0x4;const JSVAL_BOOLEAN = 0x6;const JSVAL_VOID = 0xfffffff0; function tagBits(word) { return (word & 0x7).toString(2).padStart(3, "0");} function typeof1995(word) { const tag = word & 0x7; if (word === JSVAL_VOID) return "undefined"; if (tag === JSVAL_OBJECT) return "object"; if (word & JSVAL_INT) return "number"; if (tag === JSVAL_STRING) return "string"; if (tag === JSVAL_BOOLEAN) return "boolean"; return "number";} console.log("null", tagBits(nullWord), typeof1995(nullWord));console.log("object", tagBits(0x00001000), typeof1995(0x00001000));console.log("string", tagBits(0x00003004), typeof1995(0x00003004));000 meant object, and null was all zeros, so typeof null said "object".Could the answer be fixed? Brendan Eich, who created JavaScript, suggested a change on the es-discuss mailing list. An old proposal said: "With opt-in to Harmony, typeof null === "null"." Harmony was the working name for the next big version of JavaScript. The proposal is archived at harmony:typeof_null. It was dropped, because changing the answer would break websites that already depend on "object".
The small instructions behind null checks
Before V8 runs a function, it turns the function into bytecode: a list of small, simple instructions. V8's interpreter, called Ignition, then runs those instructions one by one. Each instruction has a short name, such as TestNull.
The lesson's test runs the code below with node --print-bytecode, which prints the bytecode of each function. It checks only the instruction names. It skips their positions, because those can change.
function strictNull(x) { return x === null;} function looseNull(x) { return x == null;} function objectType(x) { return typeof x === "object";} console.log(strictNull(null));console.log(looseNull(undefined));console.log(objectType(null));strictNull: Ldar a0; TestNull; ReturnlooseNull: Ldar a0; TestUndetectable; ReturnobjectType: Ldar a0; TestTypeOf #7; ReturnIn each function, Ldar a0 loads the first argument, and Return gives back the result. The instruction in the middle is the check itself:
x === null becomes TestNull. It checks whether the value is the one null by comparing pointers.
x == null becomes TestUndetectable. It checks the value's map for the undetectable mark. V8's interpreter generator file describes this check as "null, undefined or document.all".
typeof x === "object" becomes TestTypeOf #7. The #7 picks the "object" test. In that test, null gives true first, before V8 looks at the map of any other object.
The undetectable mark and document.all
document.all is a very old browser feature. It lists every element on a web page. Browsers keep it working for old websites, but they make it look missing in checks. The undetectable mark is what links == null and document.all.
A comment in V8's map.h file says that an object with the undetectable mark:
- gives
"undefined"fortypeof, - turns into
falseas a boolean, - is useful for
document.all.
V8's public API is the set of tools that programs such as Chrome use to build on V8. In api.cc, it has ObjectTemplate::MarkAsUndetectable. A browser can use it to put the mark on its own objects.
const value = %GetUndetectable();console.log(typeof value);console.log(value == null);console.log(value == undefined);console.log(value === null);console.log(Boolean(value));%DebugPrint(value);%GetUndetectable() is another special V8 helper. It makes a test object that has the undetectable mark. The lesson's test checks that this object:
- gives
"undefined"fortypeof(line 2), - is loosely equal,
==, tonulland toundefined(lines 3 and 4), - is not strictly equal,
===, tonull(line 5), - is falsy, so
Boolean(value)isfalse(line 6).
Its debug print also shows undetectable and callable on the object's map.
// This only works in a browser. Node has no document.all.console.log(typeof document.all);console.log(document.all == null);console.log(document.all == undefined);console.log(document.all === null);console.log(Boolean(document.all));Annex B is the part of the JavaScript spec for old browser features. It defines a hidden flag called the [[IsHTMLDDA]] internal slot. Because document.all has it, document.all is falsy, typeof gives "undefined", and document.all == null is true. But document.all === null is still false.
x === null matches only null. x == null also matches undefined and the old browser value document.all, but never 0.Objects whose prototype is null
Most objects have a prototype: another object they borrow properties from. A plain {} borrows from Object.prototype, which gives it names such as toString.
Object.create(null) and the literal { __proto__: null } make real objects whose prototype is null. They borrow nothing. That makes them safe dictionaries: objects you use to look up values by string keys. A key such as "toString" can't find a borrowed method by accident. But these objects are still objects. They are not the value null.
const dict = Object.create(null);dict.answer = 42;console.log(Object.getPrototypeOf(dict) === null);console.log("toString" in dict);console.log(dict.answer); const literal = { __proto__: null, answer: 42 };console.log(Object.getPrototypeOf(literal) === null);- Line 1 makes
dictwithObject.create(null). - Line 3 prints
true: the prototype ofdictisnull. - Line 4 prints
false:dicthas notoString, because it borrows nothing. - Line 5 prints
42, the value saved on line 2. - Lines 7 and 8 do the same with a literal and print
true.
const created = Object.create(null);const literal = { __proto__: null };const ordinary = {};console.log(%HasFastProperties(created));console.log(%HasFastProperties(literal));console.log(%HasFastProperties(ordinary));%DebugPrint(created);V8 can store the properties of an object in two ways:
- Fast properties sit in fixed places that the object's map describes.
- Dictionary mode, also called slow properties, keeps them in a lookup table, like a small phone book.
%HasFastProperties is a special V8 helper. It gives true when an object uses fast properties. The lesson's test shows:
%HasFastProperties(Object.create(null))isfalse.%HasFastProperties({ __proto__: null })isfalse.%HasFastProperties({})istrue.
V8's bootstrapper is the code that sets up the built-in objects. Its comment says "Set up slow map for Object.create(null) instances without in-object properties". It saves that map as slow_object_with_null_prototype_map.
A null-prototype object is a good choice when you want no borrowed names. It also blocks some prototype pollution bugs, where an attacker adds properties to Object.prototype. A JavaScript Map is often clearer when you add and delete keys often, need keys that aren't strings, or want the keys in the order you added them. See Prototype pollution for the security side.
A property lookup stops at null
When you read a property, JavaScript looks in the object first. If the property isn't there, it goes to the object's prototype and looks there. Then it goes to that object's prototype, and so on. This line of prototypes is the prototype chain. Every chain ends at null.
The spec step OrdinaryGet says it plainly: when the next prototype is null, the read gives undefined.
V8 can also remember the answer to a lookup. Each map has a prototype_validity_cell: a marker that changes when the prototype chain changes. An inline cache is V8's saved answer for a property read at one place in your code. It can check that marker before it reuses its answer.
const base = { shared: "base value" };const middle = Object.create(base);const child = Object.create(middle); console.log(child.shared);console.log(child.missing);console.log(Object.getPrototypeOf(Object.prototype) === null);- Line 1 makes
base, which ownsshared. - Line 2 makes
middle, withbaseas its prototype. - Line 3 makes
child, withmiddleas its prototype. - Line 5 prints
base value.childandmiddledon't ownshared, butbasedoes. - Line 6 prints
undefined. No object ownsmissing, so the walk reachesnull. - Line 7 prints
true: the prototype ofObject.prototypeisnull.
Step through two real property reads: one finds shared on base, and one walks all the way to null.
script
const middle = Object.create(base);const child = Object.create(middle); console.log(child.shared);console.log(child.missing);console.log(Object.getPrototypeOf(Object.prototype) === null);missing, so the walk reaches null and stops. Then child.missing gives undefined.Prototype internals explains maps and prototype pointers in more detail. Inline caches explains why V8 wants to save an answer like "not found, as long as this chain doesn't change".
Try the checks, then use them in real code
Pick a value below. The playground runs five checks on it: typeof, loose null (== null), strict null (=== null), Boolean(), and Number(). It also names the V8 check that decides the answer.
const value = null;console.log(typeof value);console.log(value == null);console.log(value === null);console.log(Boolean(value));objecttruetruefalse0TestNull answers `=== null`. TestUndetectable answers `== null`.
null has typeof object. With == null it gives true, with === null it gives true, and Boolean(...) gives false.
- V8 saves
"object"as thetypeofanswer ofnull. null + 1is1, becausenullas a number is0.x == nullbecomes the instructionTestUndetectable.document.all === nullis false, butdocument.all == nullis true.Object.create(null)has notoString.%HasFastProperties(Object.create(null))is false in V8.Object.getPrototypeOf(Object.prototype) === null.- Reading a property nobody owns gives
undefinedwhen the walk reachesnull.
Put each card under the idea it belongs to.
In real code, you often want to treat null and undefined as "missing" but keep values such as 0 and "". Those two are falsy: they count as false in an if. But they are real values.
function describeResult(value) { if (value == null) { return "missing"; } return "present: " + value;} console.log(describeResult(null));console.log(describeResult(undefined));console.log(describeResult(0));console.log(describeResult(""));- Line 2 uses
value == null, so bothnullandundefinedgive"missing". - Lines 8 and 9 print
missing. - Line 10 prints
present: 0, and line 11 printspresent:with nothing after it.0and""are kept.
Which check to use:
- Use
value === nullwhen onlynullshould match. - Use
value == nullwhen bothnullandundefinedmean "missing". - Don't use
typeof value === "object"alone. It is also true fornull, so checkvalue !== nulltoo. - Use
Object.create(null)for simple dictionaries. UseMapwhen you need more.
Common misconceptions
- "
typeof nullproves thatnullis an object." No. The answer was kept from 1995 so old code keeps working. Reading a property ofnullstill throws an error. - "
== nullchecks for any falsy value." No. It matches onlynull,undefined, and values with the undetectable mark. It does not match0,"", orfalse. - "
document.allis justundefined." No. It is an old browser object with special rules.document.all === nullanddocument.all === undefinedare both false. - "An object with a null prototype is
null." No. It is a real object. Only its prototype isnull. - "Dictionary mode is always bad." No. V8 picks it on purpose for objects used as dictionaries, where keys come and go.
The next table compares four values. Each row is one value. Each column asks one question about it.
| Value | What typeof gives | == null? | === null? | As a number | As a boolean | How V8 handles it |
|---|---|---|---|---|---|---|
null | "object" | true | true | 0 | false | The one null oddball. Its map has the undetectable mark. |
undefined | "undefined" | true | false | NaN | false | The one undefined oddball. Its map has the undetectable mark. |
document.all | "undefined" | true | false | NaN | false | The spec gives it the [[IsHTMLDDA]] flag. V8 uses the undetectable mark. |
Object.create(null) | "object" | false | false | TypeError | true | A real object with a null prototype, in dictionary mode. No undetectable mark. |
This table takes ideas that share the word "null". Each row says what the idea really means, and what it is not.
| Idea | What it really means | What it is not |
|---|---|---|
typeof null | An answer kept from 1995 that gives "object". | A sign that null has properties or methods. |
value == null | A check that matches null, undefined, and values with the undetectable mark. | A check for any falsy value. It does not match 0, "", or false. |
| An object with a null prototype | A real object whose prototype is null. | The value null, or a value that is == null. |
| Dictionary mode | The way V8 stores properties in a lookup table, used for objects with a null prototype. | A JavaScript Map. Use Map for keys that aren't strings, or keys you add and delete often. |
You ask a question and pass it along a queue, from one person to the next. When the last person has no one to pass it to, the search stops.
- In real life: Each person points to the next person
- In JavaScript: Each object points to its prototype
- In real life: The last person has no next person
- In JavaScript: The last prototype in the chain is
null - In real life: Someone standing alone has nobody to point to
- In JavaScript: An object made with
Object.create(null)borrows no names
Practice exercises
6 EXERCISESPredict the three outputs, in order.
console.log(null + 1);
console.log(null >= 0);
console.log(null == 0);The lines print 1, true, and false, in that order. null + 1 is 0 + 1. null >= 0 is 0 >= 0. But null == 0 is false.
What do the loose check and the strict check print?
console.log(null == undefined);
console.log(null === undefined);The loose check prints true. The strict check prints false, because null and undefined are two different values.
Predict both outputs.
const dict = Object.create(null);
console.log("toString" in dict);
console.log(Object.getPrototypeOf(dict) === null);The first line prints false: dict borrows no toString. The second prints true: its prototype is exactly null.
What does the model print for the all-zero word that stands for null?
const JSVAL_OBJECT = 0;
const nullWord = 0x00000000;
console.log((nullWord & 0x7) === JSVAL_OBJECT ? "object" : "other");The word for null is all zeros. Its last 3 bits are 000, the object tag, so the model prints object.
Which check matches null, undefined, and the old document.all?
Use value == null. It becomes TestUndetectable, so it matches null, undefined, and document.all.
What value comes back for child.missing?
const base = { answer: 42 };
const child = Object.create(base);
console.log(child.answer);
console.log(child.missing);child.missing gives undefined. No object in the chain owns missing, so the walk reaches null and stops.
Quiz: check your understanding
8 QUESTIONSQuestion 1 of 8How does V8 store
null?Choose an answer to see the explanation.
Question 2 of 8What do these three lines print?
Read the code, then predictconsole.log(null + 1); console.log(null >= 0); console.log(null == 0);Choose an answer to see the explanation.
Question 3 of 8Why does
typeof nullgive"object"today?Choose an answer to see the explanation.
Question 4 of 8Which instruction did Node 22 make for
x == null?Choose an answer to see the explanation.
Question 5 of 8What does this code print about an object with no prototype?
Read the code, then predictconst dict = Object.create(null); console.log("toString" in dict); console.log(Object.getPrototypeOf(dict) === null);Choose an answer to see the explanation.
Question 6 of 8Which results belong to
document.allin a browser?Choose an answer to see the explanation.
Question 7 of 8What does this lookup code print?
Read the code, then predictconst base = { answer: 42 }; const child = Object.create(base); console.log(child.answer); console.log(child.missing);Choose an answer to see the explanation.
Question 8 of 8When is
Object.create(null)a good tool?Choose an answer to see the explanation.
Key takeaways
- V8 keeps one read-only
null. It holds ready-made answers:0as a number,"null"as a string, and"object"fortypeof. typeof nullgives"object"because, in 1995,nullwas an all-zero word, and000was the object tag.=== nullbecomes the instructionTestNull.== nullbecomesTestUndetectable.document.allis an old browser object. It is falsy and== null, but it is notnull.- An object made with
Object.create(null)is a useful dictionary. Every property lookup stops when it reaches anullprototype.
Remember the one-liner.null is one empty value made once, with special storage and fast checks, and it is also where every prototype chain ends.
Up next: Strings inside the engine. You'll see the different ways V8 stores strings, such as one-byte strings, two-byte strings, ropes, and slices.