How to Write Custom Polyfills
1. Polyfill Structure and Template Patterns
| Component | Pattern | Purpose | Example |
|---|---|---|---|
| Feature Detection | if (!Type.prototype.method) |
Check if polyfill needed | Prevent overwriting native implementation |
| IIFE Wrapper | (function() { ... })() |
Isolate scope and prevent pollution | Avoid variable leakage to global scope |
| Strict Mode | 'use strict'; |
Enforce better coding practices | Catch common errors early |
| Type Checking | if (this == null) throw TypeError |
Validate context and arguments | Match native behavior for errors |
| ToObject Coercion | var O = Object(this) |
Convert this to object safely | Handle primitives correctly |
| Length Coercion | len = O.length >>> 0 |
Convert length to uint32 | Match array length behavior |
Example: Complete polyfill template structure
// Standard Polyfill Template
(function() {
'use strict';
// Feature detection - exit if already exists
if (Array.prototype.includes) {
return;
}
// Polyfill implementation
Array.prototype.includes = function(searchElement, fromIndex) {
// Step 1: Type check - throw if this is null/undefined
if (this == null) {
throw new TypeError('Array.prototype.includes called on null or undefined');
}
// Step 2: Convert this to object
var O = Object(this);
// Step 3: Get and convert length to unsigned 32-bit integer
var len = parseInt(O.length) || 0;
// Step 4: Handle edge cases
if (len === 0) {
return false;
}
// Step 5: Process fromIndex parameter
var n = parseInt(fromIndex) || 0;
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
// Step 6: Main algorithm
while (k < len) {
var currentElement = O[k];
// Handle NaN comparison (NaN === NaN should be true)
if (searchElement === currentElement ||
(searchElement !== searchElement && currentElement !== currentElement)) {
return true;
}
k++;
}
// Step 7: Return result
return false;
};
})();
Note: Always wrap polyfills in an IIFE to avoid polluting global scope. Use strict mode to catch errors and ensure feature
detection runs first to avoid overwriting native implementations.
2. Prototype Extension (Array.prototype, String.prototype)
| Practice | Do This | Don't Do This | Reason |
|---|---|---|---|
| Check Existence | if (!proto.method) |
Unconditional assignment | Preserve native implementation |
| Use defineProperty | Object.defineProperty() |
proto.method = fn |
Control enumerability and writability |
| Non-enumerable | enumerable: false |
enumerable: true |
Don't break for...in loops |
| Configurable | configurable: true |
configurable: false |
Allow future modifications |
| Test Context | if (this == null) throw |
Assume this is valid | Match native error behavior |
| Handle Edge Cases | Check length, NaN, undefined | Assume happy path | Spec compliance and robustness |
Example: Proper prototype extension with defineProperty
// Good: Using Object.defineProperty
if (!Array.prototype.find) {
Object.defineProperty(Array.prototype, 'find', {
value: function(predicate) {
if (this == null) {
throw new TypeError('this is null or not defined');
}
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
var O = Object(this);
var len = O.length >>> 0;
var thisArg = arguments[1];
for (var k = 0; k < len; k++) {
if (k in O) {
var kValue = O[k];
if (predicate.call(thisArg, kValue, k, O)) {
return kValue;
}
}
}
return undefined;
},
configurable: true,
writable: true,
enumerable: false // Critical: prevents for-in enumeration
});
}
// Bad: Direct assignment (enumerable by default)
// Array.prototype.find = function() { ... }; // DON'T DO THIS
Warning: Never extend prototypes of native objects without feature
detection. Always use Object.defineProperty with enumerable: false to avoid breaking existing code.
3. Static Method Polyfills (Object.assign, Array.from)
| Type | Pattern | Check Method | Example |
|---|---|---|---|
| Object Static | Object.assign = function() {} |
if (!Object.assign) |
Object.keys, Object.values, Object.entries |
| Array Static | Array.from = function() {} |
if (!Array.from) |
Array.from, Array.of, Array.isArray |
| Number Static | Number.isNaN = function() {} |
if (!Number.isNaN) |
Number.isFinite, Number.parseInt |
| String Static | String.fromCodePoint = function() {} |
if (!String.fromCodePoint) |
String.raw, String.fromCodePoint |
| Math Static | Math.trunc = function() {} |
if (!Math.trunc) |
Math.sign, Math.trunc, Math.cbrt |
Example: Static method polyfills
// Object.assign polyfill
if (!Object.assign) {
Object.assign = function(target) {
'use strict';
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) {
for (var nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
};
}
// Array.from polyfill
if (!Array.from) {
Array.from = function(arrayLike, mapFn, thisArg) {
var C = this;
var items = Object(arrayLike);
var len = items.length >>> 0;
var A = typeof C === 'function' ?
Object(new C(len)) : new Array(len);
var k = 0;
while (k < len) {
var kValue = items[k];
if (mapFn) {
A[k] = typeof thisArg === 'undefined' ?
mapFn(kValue, k) :
mapFn.call(thisArg, kValue, k);
} else {
A[k] = kValue;
}
k++;
}
A.length = len;
return A;
};
}
// Number.isNaN polyfill (more accurate than global isNaN)
if (!Number.isNaN) {
Number.isNaN = function(value) {
// NaN is the only value that is not equal to itself
return value !== value;
};
}
4. Object.defineProperty for Non-enumerable Methods
| Descriptor | Value | Effect | Use Case |
|---|---|---|---|
| value | Any value | The actual function or value | Set the method implementation |
| writable | true/false |
Can property be changed with assignment | Usually true for polyfills |
| enumerable | false |
Appears in for...in loops | Must be false to match native behavior |
| configurable | true |
Can descriptor be changed/deleted | Allow future updates |
| get | Function | Getter function for property | Computed properties |
| set | Function | Setter function for property | Validation on assignment |
Example: Advanced defineProperty usage patterns
// Data descriptor (value + writable)
Object.defineProperty(Array.prototype, 'last', {
get: function() {
return this[this.length - 1];
},
set: function(value) {
this[this.length - 1] = value;
},
enumerable: false,
configurable: true
});
// Usage: arr.last returns last element
var arr = [1, 2, 3];
console.log(arr.last); // 3
arr.last = 5;
console.log(arr.last); // 5
// Batch property definition
if (!Object.defineProperties) {
Object.defineProperties = function(obj, properties) {
for (var prop in properties) {
if (properties.hasOwnProperty(prop)) {
Object.defineProperty(obj, prop, properties[prop]);
}
}
return obj;
};
}
// Define multiple properties at once
Object.defineProperties(String.prototype, {
"trim": {
value: function() {
return this.replace(/^\s+|\s+$/g, "");
},
writable: true,
enumerable: false,
configurable: true
},
"trimStart": {
value: function() {
return this.replace(/^\s+/, "");
},
writable: true,
enumerable: false,
configurable: true
}
});
Note: Use accessor descriptors (get/set) for computed
properties. Use data descriptors (value/writable) for methods. Never mix both in
the same descriptor.
5. UMD and ES Module Polyfill Patterns
| Format | Detection | Export Pattern | Use Case |
|---|---|---|---|
| CommonJS | typeof module !== 'undefined' |
module.exports = polyfill |
Node.js environments |
| AMD | typeof define === 'function' && define.amd |
define([], factory) |
RequireJS, legacy browsers |
| ES Module | Separate .mjs file | export default polyfill |
Modern bundlers, native ESM |
| Global | Fallback for all | window.polyfill = ... |
Browser script tags |
| UMD | Combines all patterns | Check order: AMD, CommonJS, Global | Universal compatibility |
Example: UMD wrapper pattern
// Universal Module Definition (UMD) Pattern
(function(root, factory) {
'use strict';
// AMD (RequireJS)
if (typeof define === 'function' && define.amd) {
define([], factory);
}
// CommonJS (Node.js)
else if (typeof module === 'object' && module.exports) {
module.exports = factory();
}
// Browser globals
else {
root.myPolyfill = factory();
}
}(typeof self !== 'undefined' ? self : this, function() {
'use strict';
// Feature detection
if ('myFeature' in window) {
return window.myFeature;
}
// Polyfill implementation
function MyPolyfill() {
// Implementation here
}
MyPolyfill.prototype.method = function() {
// Method implementation
};
// Install globally if needed
if (typeof window !== 'undefined') {
window.myFeature = MyPolyfill;
}
// Return for module systems
return MyPolyfill;
}));
// ES Module format (separate file)
// polyfill.mjs
export default function polyfill() {
if (!Array.prototype.at) {
Array.prototype.at = function(index) {
var len = this.length;
var k = index >= 0 ? index : len + index;
return (k >= 0 && k < len) ? this[k] : undefined;
};
}
}
// Usage in ES modules
// import polyfill from './polyfill.mjs';
// polyfill();
Note: For maximum compatibility, use UMD pattern in polyfills.
Modern projects can use ES modules (.mjs) with build tool transpilation for
legacy support.
6. Idempotency and Safe Polyfill Execution
| Pattern | Implementation | Purpose | Benefit |
|---|---|---|---|
| Existence Check | if (!Type.prototype.method) |
Only add if missing | Prevent double-loading issues |
| Early Return | if (hasFeature) return; |
Exit immediately if native exists | Zero overhead for modern browsers |
| Version Check | if (polyfill.version) return; |
Track polyfill installation | Avoid conflicts with other polyfills |
| Strict Equality | feature === undefined |
Precise undefined checking | Distinguish from null or falsy values |
| IIFE Isolation | (function(){ ... })() |
Create private scope | Prevent variable name collisions |
| Namespace Guard | window.POLYFILLS = {} |
Track loaded polyfills | Coordinate multiple polyfill scripts |
Example: Idempotent polyfill patterns
// Pattern 1: Simple existence check
(function() {
'use strict';
// Early return if already exists
if (Array.prototype.includes) {
return;
}
// Polyfill implementation
Array.prototype.includes = function(search) {
// Implementation...
};
})();
// Pattern 2: Version tracking
(function() {
'use strict';
// Check if already loaded
if (window.__POLYFILLS__ && window.__POLYFILLS__.arrayIncludes) {
return;
}
// Initialize polyfill registry
window.__POLYFILLS__ = window.__POLYFILLS__ || {};
if (!Array.prototype.includes) {
Array.prototype.includes = function(search) {
// Implementation...
};
}
// Mark as loaded
window.__POLYFILLS__.arrayIncludes = '1.0.0';
})();
// Pattern 3: Safe multiple execution
(function(global) {
'use strict';
// Create polyfill only once
function createPolyfill() {
// Check each feature individually
if (!Array.prototype.find) {
Object.defineProperty(Array.prototype, 'find', {
value: function(predicate) { /* ... */ },
configurable: true,
writable: true,
enumerable: false
});
}
if (!Array.prototype.findIndex) {
Object.defineProperty(Array.prototype, 'findIndex', {
value: function(predicate) { /* ... */ },
configurable: true,
writable: true,
enumerable: false
});
}
}
// Safe to call multiple times - only executes missing polyfills
createPolyfill();
})(typeof window !== 'undefined' ? window : global);
Key Takeaways - Polyfill Writing
- Always wrap polyfills in IIFE with strict mode
- Use feature detection before adding any polyfill
- Set enumerable: false when extending prototypes
- Match native error behavior with proper type checking
- Use UMD pattern for maximum compatibility
- Make polyfills idempotent - safe to load multiple times