Polyfill Factory
โฑ๏ธ ~3-minute bite ยท solve the sandbox to master
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
| Technique | What it does | When applied |
|---|---|---|
| Polyfill | Adds missing runtime API (Promise, Array.from) | Runtime, conditional |
| Transpile | Rewrites syntax (arrow fn โ function) | Build time, always |
| Shim | Patches/normalises existing buggy API | Runtime 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).
Interactive Sandbox
โ Move something, see it react instantly.Pick a polyfill to trace:
Detection guard
| 1 | if (!Array.prototype.map) { |
| 2 | // install polyfill below |
| 3 | } |
Implementation
| 1 | Array.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 | }; |
Test call
| 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)
Challenge
Explore all 5 polyfills. Trace each one step-by-step to understand the spec edge cases.
Why Should I Care?
โ The exact interview question + the bug it kills.๐ค Interview Questions
| 1 | Array.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 | }; |
| 1 | [NaN].indexOf(NaN) // -1 โ uses ===, NaN !== NaN |
| 2 | [NaN].includes(NaN) // true โ uses SameValueZero |
| 3 | ย |
| 4 | // The NaN self-check in the polyfill: |
| 5 | if (el !== el && searchElement !== searchElement) return true; |
| 1 | // WRONG โ enumerable polyfill |
| 2 | Array.prototype.myMethod = fn; |
| 3 | ย |
| 4 | for (var key in [1, 2, 3]) { |
| 5 | console.log(key); // "0", "1", "2", "myMethod" โ BREAKS |
| 6 | } |
| 7 | ย |
| 8 | // CORRECT โ non-enumerable |
| 9 | Object.defineProperty(Array.prototype, "myMethod", { |
| 10 | value: fn, writable: true, configurable: true, enumerable: false |
| 11 | }); |
๐ฅ Production Risks
| 1 | // Malicious JSON payload: |
| 2 | var payload = '{"__proto__":{"isAdmin":true}}'; |
| 3 | Object.assign({}, JSON.parse(payload)); |
| 4 | // Now ALL new objects have isAdmin: true |
| 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 |
| 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 Toolchain
| 1 | // .browserslistrc |
| 2 | last 2 versions |
| 3 | ie 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:
| 1 | Object.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): |
| 9 | Object.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
| 1 | const 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) |
| 5 | sparse.map(x => x * 2); |
| 6 | // โ [2, empty, 6] (length 3, hole at index 1 preserved) |
| 7 | ย |
| 8 | // filter COMPACTS (holes are excluded from result) |
| 9 | sparse.filter(x => x > 0); |
| 10 | // โ [1, 3] (length 2, no holes, sequential indices) |
| 11 | ย |
| 12 | // forEach SKIPS holes (callback never called for holes) |
| 13 | sparse.forEach(x => console.log(x)); |
| 14 | // logs: 1, 3 (index 1 silently skipped) |
| 15 | ย |
| 16 | // Gotcha: hole !== undefined |
| 17 | console.log(sparse[1]); // undefined (reading returns undefined) |
| 18 | console.log(1 in sparse); // false (but it is NOT a property) |
| 19 | console.log(sparse.hasOwnProperty(1)); // false |
โก Differential Serving
| 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)
| 1 | Array.from("abc") // ["a", "b", "c"] |
| 2 | // Uses Symbol.iterator โ NOT .length |
Object.assign skips non-enumerable source properties
| 1 | const src = {}; |
| 2 | Object.defineProperty(src, "hidden", { |
| 3 | value: 42, enumerable: false |
| 4 | }); |
| 5 | Object.assign({}, src); // {} โ "hidden" not copied! |
includes with fromIndex > length (out of range)
| 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
| 1 | [+0].includes(-0); // true (SameValueZero: +0 === -0) |
| 2 | Object.is(+0, -0); // false (SameValue distinguishes signs) |
๐บ๏ธ Polyfill Reference
| Method | Added | Key gotcha | core-js |
|---|---|---|---|
| Array.prototype.map | ES5 | k in O for sparse holes | es.array.map |
| Array.prototype.filter | ES5 | compact result, no holes | es.array.filter |
| Array.prototype.includes | ES2016 | SameValueZero, NaN works | es.array.includes |
| Array.from | ES2015 | iterator > array-like priority | es.array.from |
| Object.assign | ES2015 | shallow, own enumerable only | es.object.assign |
| Promise | ES2015 | microtask queue semantics | es.promise |
| Array.prototype.flat | ES2019 | depth param, default 1 | es.array.flat |
| globalThis | ES2020 | cross-env global reference | es.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.What is `queueMicrotask()` and when would you polyfill it?