๐Ÿณ IntuitiveFE
Login
โ† All concepts

Polyfill Factory

โฑ๏ธ ~3-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿชฃ Polyfilla โ€” the wall filler that gave JavaScript its compatibility glue.

Etymology

In 2009, Remy Sharp needed a word for "code that fills browser gaps." He reached for Polyfilla โ€” the British spackling compound used to fill holes in walls before painting. The metaphor is exact: a polyfill fills in missing functionality so the rest of your paint (your app) can go on smoothly.

polyfill โ‰  transpile. Polyfills add missing runtime APIs. Transpilers rewrite syntax at build time.

๐Ÿญ The Factory Line Analogy

A polyfill factory has three stations:

๐Ÿ”

Quality Control (Feature Detection)

Check if the machine already exists: `if (!Array.prototype.map)`. Never ship a replacement part if the factory already has the real one. Feature detection checks actual capability โ€” UA sniffing checks the name plate and gets it wrong.

๐Ÿ”ง

Assembly (Spec-Compliant Implementation)

Build a replacement that matches the spec exactly: handle sparse arrays, coerce length with `>>> 0`, bind `thisArg`. The replacement must behave identically to the native โ€” even on edge cases.

๐Ÿ“ฆ

Silent Installation (Object.defineProperty)

Mount it on the prototype without making it visible to `for...in`. Use `Object.defineProperty(..., { enumerable: false })` so legacy code that iterates arrays doesn't accidentally see your polyfill as a key.

๐Ÿ“ Three Compatibility Techniques

TechniqueWhat it doesWhen applied
PolyfillAdds missing runtime API (Promise, Array.from)Runtime, conditional
TranspileRewrites syntax (arrow fn โ†’ function)Build time, always
ShimPatches/normalises existing buggy APIRuntime or build time

You cannot polyfill syntax. You can only polyfill APIs. That is the line between polyfill and transpile.

๐Ÿ”ข The `>>> 0` Trick

Every array polyfill starts with var len = O.length >>> 0. Unsigned right shift converts any value to a 32-bit unsigned integer โ€” the exact type array lengths must be per the ES spec (ToUint32).

-1 >>> 0โ†’4294967295wraps negative
3.9 >>> 0โ†’3truncates float
undefined >>> 0โ†’0normalises undefined
null >>> 0โ†’0normalises null
'5' >>> 0โ†’5coerces string
Infinity >>> 0โ†’0normalises Infinity
๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Pick a polyfill to trace:

Detection guard

js
1if (!Array.prototype.map) {
2 // install polyfill below
3}

Implementation

js
1Array.prototype.map = function(callback, thisArg) {
2 if (this == null) throw new TypeError("null/undefined");
3 if (typeof callback !== "function") throw new TypeError();
4ย 
5 var O = Object(this); // wrap primitives
6 var len = O.length >>> 0; // coerce to uint32
7 var A = new Array(len); // pre-size result
8ย 
9 for (var k = 0; k < len; k++) {
10 if (k in O) { // skip holes (sparse)
11 A[k] = callback.call(thisArg, O[k], k, O);
12 }
13 }
14 return A;
15};
Spec: ES5 ยง15.4.4.19 โ€” ToObject(this), ToUint32(len), then iterate with `k in O`.

Test call

js
1[1, 2, 3].map(x => x * 2)

Expected: [2, 4, 6]

Execution trace โ€” step 1/5

O = Object([1,2,3]), len = 3 >>> 0 = 3. A = new Array(3)

A = [___, ___, ___]
Key gotcha: The `k in O` check is essential โ€” it skips holes in sparse arrays. Without it, [1,,3].map() would call callback on the hole.
๐ŸŽฏ

Challenge

Explore all 5 polyfills. Trace each one step-by-step to understand the spec edge cases.

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

๐ŸŽค Interview Questions

Q: Implement Array.prototype.map from scratch.
js
1Array.prototype.map = function(callback, thisArg) {
2 var O = Object(this);
3 var len = O.length >>> 0;
4 var A = new Array(len);
5 for (var k = 0; k < len; k++) {
6 if (k in O) {
7 A[k] = callback.call(thisArg, O[k], k, O);
8 }
9 }
10 return A;
11};
Three gotchas interviewers check: (1) `>>> 0` for length coercion, (2) `k in O` to preserve sparse holes, (3) `callback.call(thisArg, ...)` โ€” not just `callback(O[k])`.
Q: Why does [NaN].indexOf(NaN) return -1 but [NaN].includes(NaN) return true?
js
1[NaN].indexOf(NaN) // -1 โ€” uses ===, NaN !== NaN
2[NaN].includes(NaN) // true โ€” uses SameValueZero
3ย 
4// The NaN self-check in the polyfill:
5if (el !== el && searchElement !== searchElement) return true;
SameValueZero is like === but treats NaN as equal to itself. The self-inequality trick (`x !== x` is only true for NaN) is the idiomatic implementation without Number.isNaN.
Q: What breaks if you assign a polyfill directly instead of using Object.defineProperty?
js
1// WRONG โ€” enumerable polyfill
2Array.prototype.myMethod = fn;
3ย 
4for (var key in [1, 2, 3]) {
5 console.log(key); // "0", "1", "2", "myMethod" โ† BREAKS
6}
7ย 
8// CORRECT โ€” non-enumerable
9Object.defineProperty(Array.prototype, "myMethod", {
10 value: fn, writable: true, configurable: true, enumerable: false
11});
Direct assignment is enumerable. `for...in` on arrays (bad practice but common in legacy code) then iterates polyfill names as keys, breaking iteration logic.

๐Ÿ”ฅ Production Risks

โš  Prototype Pollution via Object.assign
js
1// Malicious JSON payload:
2var payload = '{"__proto__":{"isAdmin":true}}';
3Object.assign({}, JSON.parse(payload));
4// Now ALL new objects have isAdmin: true
Sanitise untrusted input before passing to Object.assign. Use Object.create(null) for pure data bags.
โš  Spec Divergence in Edge Cases
js
1// Polyfill bug: checking length before Symbol.iterator
2// Spec says: prefer Symbol.iterator if present
3// Buggy polyfill: Array.from(set) uses length, skips iterator
4// Set has no numeric length โ†’ result is [] โ† silent bug
Polyfill libraries like core-js have comprehensive test suites. Rolling your own for complex APIs is risky.
โš  Bundle Bloat โ€” Everyone Pays
js
1// Without differential serving, ALL users download polyfills
2// Even Chrome 120 downloads the IE11 polyfill bundle
3ย 
4// Fix: differential serving
5// <script type="module" src="modern.js"></script>
6// <script nomodule src="legacy.js"></script>
Modern bundle can be 20โ€“40% smaller with no polyfills. Old browsers get the full legacy bundle.

๐Ÿงฐ Modern Toolchain

js
1// .browserslistrc
2last 2 versions
3ie 11
4ย 
5// babel.config.json
6{
7 "presets": [
8 ["@babel/preset-env", {
9 "useBuiltIns": "usage",
10 "corejs": 3
11 }]
12 ]
13}
14ย 
15// Result: Babel reads browserslist, automatically injects ONLY
16// the core-js polyfills your code uses AND your targets need.
17// For modern Chrome: injects nothing.
18// For IE 11: injects Promise, Array.from, Object.assign, ...
"entry"

Import all core-js polyfills for all targets at entry. Safe but larger.

"usage"

Import only polyfills for features actually used. Smaller but needs analysis.

๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

๐Ÿ“š Object.defineProperty Deep Dive

Property descriptors control visibility and mutability. Every polyfill should use the full descriptor form:

js
1Object.defineProperty(Array.prototype, "map", {
2 value: function(cb, ta) { /* ... */ },
3 writable: true, // allows Array.prototype.map = newFn
4 configurable: true, // allows delete / redefine
5 enumerable: false, // HIDDEN from for...in โ† the important one
6});
7ย 
8// Read-only constant (e.g. polyfilling Math.TAU):
9Object.defineProperty(Math, "TAU", {
10 value: Math.PI * 2,
11 writable: false,
12 configurable: false,
13 enumerable: false,
14});

Accessor descriptors (get/set) are a third form โ€” used in Vue 2 reactivity and legacy observability patterns to intercept property reads/writes.

๐Ÿ”ฌ Sparse Arrays โ€” map vs filter vs forEach

js
1const sparse = [1, , 3]; // length=3, index 1 is a HOLE
2// "1 in sparse" === false โ† key distinction
3ย 
4// map PRESERVES holes (hole position โ†’ hole position)
5sparse.map(x => x * 2);
6// โ†’ [2, empty, 6] (length 3, hole at index 1 preserved)
7ย 
8// filter COMPACTS (holes are excluded from result)
9sparse.filter(x => x > 0);
10// โ†’ [1, 3] (length 2, no holes, sequential indices)
11ย 
12// forEach SKIPS holes (callback never called for holes)
13sparse.forEach(x => console.log(x));
14// logs: 1, 3 (index 1 silently skipped)
15ย 
16// Gotcha: hole !== undefined
17console.log(sparse[1]); // undefined (reading returns undefined)
18console.log(1 in sparse); // false (but it is NOT a property)
19console.log(sparse.hasOwnProperty(1)); // false

โšก Differential Serving

html
1<!-- Modern browsers: support ES modules, ignore nomodule -->
2<script type="module" src="/dist/app.modern.js"></script>
3ย 
4<!-- Legacy browsers: ignore type="module", run nomodule -->
5<script nomodule src="/dist/app.legacy.js"></script>
6ย 
7<!-- Modern bundle: ~80KB (no polyfills, ES2020+ syntax) -->
8<!-- Legacy bundle: ~130KB (core-js polyfills + transpiled) -->
9ย 
10<!-- โš ๏ธ Edge 18-79 double-execution bug:
11 It runs BOTH scripts. Guard legacy with:
12 if (!window.__legacyRan) {
13 window.__legacyRan = true;
14 // actual app code
15 } -->

๐Ÿง  Common Edge Cases

Array.from on a string (iterable, not array-like first)

js
1Array.from("abc") // ["a", "b", "c"]
2// Uses Symbol.iterator โ€” NOT .length

Object.assign skips non-enumerable source properties

js
1const src = {};
2Object.defineProperty(src, "hidden", {
3 value: 42, enumerable: false
4});
5Object.assign({}, src); // {} โ† "hidden" not copied!

includes with fromIndex > length (out of range)

js
1[1,2,3].includes(1, 10); // false (search starts past end)
2[1,2,3].includes(1, -100); // true (max(3-100,0)=0 โ†’ search from 0)

+0 and -0: includes vs Object.is

js
1[+0].includes(-0); // true (SameValueZero: +0 === -0)
2Object.is(+0, -0); // false (SameValue distinguishes signs)

๐Ÿ—บ๏ธ Polyfill Reference

MethodAddedKey gotchacore-js
Array.prototype.mapES5k in O for sparse holeses.array.map
Array.prototype.filterES5compact result, no holeses.array.filter
Array.prototype.includesES2016SameValueZero, NaN workses.array.includes
Array.fromES2015iterator > array-like priorityes.array.from
Object.assignES2015shallow, own enumerable onlyes.object.assign
PromiseES2015microtask queue semanticses.promise
Array.prototype.flatES2019depth param, default 1es.array.flat
globalThisES2020cross-env global referencees.global-this
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

Key concerns: `>>> 0` for length, `k in O` for sparse holes, `callback.call(thisArg, ...)` for context.

`indexOf` uses `===` (NaN โ‰  NaN); `includes` uses SameValueZero (NaN = NaN).

Polyfills add missing runtime APIs; transpilers rewrite syntax at build time. You cannot polyfill syntax.

Direct assignment creates an enumerable property; `Object.defineProperty` with `enumerable: false` hides it from `for...in`.

Babel reads your browserslist, determines which APIs need polyfilling for those targets, and injects only the relevant core-js imports.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What is `queueMicrotask()` and when would you polyfill it?