Skip to content

Commit 809cbf6

Browse files
committed
fix: improve technical accuracy in value vs reference types concept page
1 parent 8f7047e commit 809cbf6

2 files changed

Lines changed: 61 additions & 9 deletions

File tree

docs/concepts/value-reference-types.mdx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,24 @@ JavaScript has two categories of data types that behave very differently:
4242

4343
| Type | Example | Stored As |
4444
|------|---------|-----------|
45-
| `string` | `"hello"` | The actual characters |
46-
| `number` | `42` | The actual number |
47-
| `bigint` | `9007199254740993n` | The actual large integer |
48-
| `boolean` | `true` | The actual boolean |
45+
| `string` | `"hello"` | The string value |
46+
| `number` | `42` | The numeric value |
47+
| `bigint` | `9007199254740993n` | The large integer value |
48+
| `boolean` | `true` | The boolean value |
4949
| `undefined` | `undefined` | The undefined value |
5050
| `null` | `null` | The null value |
5151
| [`symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) | `Symbol("id")` | The unique symbol |
5252

5353
**Key characteristics:**
54-
- Stored directly in the variable
54+
- Behave as if stored directly in the variable
5555
- Immutable — you can't change them, only replace them
5656
- Copied by value — copies are independent
5757
- Compared by value — same value = equal
5858

59+
<Info>
60+
**Under the hood:** Modern JavaScript engines optimize string storage through a technique called "string interning" — identical strings may share the same memory location internally. However, this is an optimization detail; strings still *behave* as independent values when you work with them.
61+
</Info>
62+
5963
### Reference Types
6064

6165
**Everything else** is a reference type:
@@ -119,6 +123,10 @@ This difference between "storing the value itself" vs "storing a map to the valu
119123

120124
To truly understand the difference, you need to know where JavaScript stores data.
121125

126+
<Info>
127+
**Important note:** The stack/heap model described below is a **conceptual simplification** to help you understand how value types and reference types *behave*. The JavaScript specification doesn't define where values are stored in memory—actual engines (V8, SpiderMonkey, etc.) may optimize storage differently. What matters is understanding the *behavior* difference, not the physical memory location.
128+
</Info>
129+
122130
### The Stack: Home of Primitives
123131

124132
The **stack** is a fast, organized region of memory. It stores:
@@ -380,6 +388,8 @@ arr1.length === arr2.length && arr1.every((v, i) => v === arr2[i])
380388
// For complex cases, use a library like Lodash
381389
_.isEqual(obj1, obj2)
382390
```
391+
392+
**Caution with JSON.stringify:** Property order matters! `{a:1, b:2}` and `{b:2, a:1}` will produce different strings even though they're logically equal. It also fails with `undefined`, functions, Symbols, circular references, `NaN`, and `Infinity`. For reliable deep equality, use a library like Lodash's `_.isEqual()`.
383393
</Tip>
384394

385395
---
@@ -608,15 +618,19 @@ console.log(user.address.city); // "LA"
608618
To freeze nested objects too, you need a recursive "deep freeze" function:
609619

610620
```javascript
611-
function deepFreeze(obj) {
621+
function deepFreeze(obj, seen = new WeakSet()) {
622+
// Prevent infinite loops from circular references
623+
if (seen.has(obj)) return obj;
624+
seen.add(obj);
625+
612626
// Get all property names (including symbols)
613627
const propNames = Reflect.ownKeys(obj);
614628

615629
// Freeze nested objects first
616630
for (const name of propNames) {
617631
const value = obj[name];
618632
if (value && typeof value === "object") {
619-
deepFreeze(value);
633+
deepFreeze(value, seen);
620634
}
621635
}
622636

@@ -633,6 +647,10 @@ user.address.city = "LA"; // Now this is blocked too!
633647
console.log(user.address.city); // "NYC"
634648
```
635649

650+
<Info>
651+
**Why the `seen` WeakSet?** Objects can have circular references (e.g., `obj.self = obj`). Without tracking visited objects, the function would recurse infinitely. The WeakSet ensures each object is only processed once.
652+
</Info>
653+
636654
### Related Methods: freeze vs seal vs preventExtensions
637655

638656
| Method | Add Properties | Delete Properties | Change Values |
@@ -776,6 +794,7 @@ const deep = JSON.parse(JSON.stringify(original));
776794
- Cannot clone property descriptors, getters/setters
777795
- Cannot clone the prototype chain
778796
- Symbol-keyed properties are ignored
797+
- Error objects lose their stack trace (only `message` is preserved)
779798
- Not available in older browsers (pre-2022)
780799

781800
```javascript

tests/fundamentals/value-reference-types/value-reference-types.test.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,17 @@ describe('Value Types and Reference Types', () => {
253253

254254
describe('Deep Freeze', () => {
255255
it('should freeze nested objects with deep freeze function', () => {
256-
function deepFreeze(obj) {
256+
function deepFreeze(obj, seen = new WeakSet()) {
257+
// Prevent infinite loops from circular references
258+
if (seen.has(obj)) return obj
259+
seen.add(obj)
260+
257261
const propNames = Reflect.ownKeys(obj)
258262

259263
for (const name of propNames) {
260264
const value = obj[name]
261265
if (value && typeof value === "object") {
262-
deepFreeze(value)
266+
deepFreeze(value, seen)
263267
}
264268
}
265269

@@ -275,6 +279,35 @@ describe('Value Types and Reference Types', () => {
275279
expect(() => { user.address.city = "LA" }).toThrow(TypeError)
276280
expect(user.address.city).toBe("NYC") // Now blocked!
277281
})
282+
283+
it('should handle circular references without infinite loop', () => {
284+
function deepFreeze(obj, seen = new WeakSet()) {
285+
if (seen.has(obj)) return obj
286+
seen.add(obj)
287+
288+
const propNames = Reflect.ownKeys(obj)
289+
290+
for (const name of propNames) {
291+
const value = obj[name]
292+
if (value && typeof value === "object") {
293+
deepFreeze(value, seen)
294+
}
295+
}
296+
297+
return Object.freeze(obj)
298+
}
299+
300+
// Create object with circular reference
301+
const obj = { name: "test" }
302+
obj.self = obj // Circular reference
303+
304+
// Should not throw or hang - handles circular reference
305+
const frozen = deepFreeze(obj)
306+
307+
expect(Object.isFrozen(frozen)).toBe(true)
308+
expect(frozen.self).toBe(frozen) // Circular reference preserved
309+
expect(() => { frozen.name = "changed" }).toThrow(TypeError)
310+
})
278311
})
279312

280313
describe('Object.seal() and Object.preventExtensions()', () => {

0 commit comments

Comments
 (0)