A polyfill is code that adds a standard feature to an environment that doesn't have it yet, such as Promise.any or Array.prototype.flat in an older browser. It's written in plain JavaScript that the old environment can already run, and it follows the specification, the document that defines how the feature behaves, as closely as it can. Code written for new browsers then works in old ones too. The name comes from Polyfilla, a paste for filling cracks in walls: a polyfill fills the gaps in a browser.
A real polyfill checks for the feature first, as in if (!Promise.any), and defines it only when it's missing, so a browser that has the built-in keeps its own version:
if (!Array.prototype.flat) {
Array.prototype.flat = function flat(depth) {
// the steps the specification lists, written in plain JavaScript
};
}
Every array gets its methods from one shared object, Array.prototype, and every function from Function.prototype. So a method added there appears on every array or function in the program, even ones created before it was added, and inside it this is the array or function it was called on. Careful polyfills add it with Object.defineProperty rather than = as above, so that, like the built-ins, it isn't enumerable and doesn't show up in a for...in loop.
When an interviewer asks you to write a polyfill, they mean: reproduce a built-in's behaviour yourself, without calling it. It shows you know exactly what the built-in does, beyond its everyday use. The details are the point, because code written against the standard relies on them too:
| Built-in | Everyday use | Details a polyfill must match too |
|---|---|---|
Array.prototype.map | [1, 2].map((n) => n * 2) is [2, 4] | thisArg, holes, and the TypeError for a callback that isn't a function, thrown even when the array is empty |
Function.prototype.bind | A function with a fixed this | Preset arguments, and calling the result with new |
Promise.all | Waits for several promises | Plain values among them, an empty input, and an argument that isn't iterable |
Object.create | An object that inherits from the one you pass | null as the prototype, and a TypeError for one that's neither an object nor null |
JSON.stringify | Turns data into text | undefined and functions, toJSON, and a TypeError on cycles |
These tasks give your version its own name, such as Array.prototype.myMap, Object.myCreate, or JSON.myStringify, or make it a stand-alone function, such as promiseAll(iterable) or objectIs(a, b). Replacing the real method would change it for every other piece of code on the page, and leave you no built-in to compare yours with.
In production you rarely write polyfills by hand: libraries such as core-js provide them, and Babel can add the ones your target browsers need. A polyfill can only add missing objects, functions, and methods. New syntax, such as ?. or class, can't be added that way: a compiler has to rewrite it as older syntax, which is called transpiling.
== and === aren't the only ways JavaScript compares values. The specification, the document that defines the language, names two more, SameValue and SameValueZero, and each built-in uses the one that suits it. Like ===, both compare without converting types, so "1" never equals 1, and two objects are equal only when they're the same object. ===, SameValue, and SameValueZero differ only on two unusual numbers: NaN and -0.
NaN, short for not a number, is what you get from math that has no numeric answer, such as 0 / 0 or Number("abc"). The floating-point standard that JavaScript numbers follow says NaN equals nothing, itself included, so NaN === NaN is false. -0 is a negative zero, which you get from, for example, -1 * 0 or Math.round(-0.4). -0 === 0 is true and String(-0) is "0", but it isn't quite the same number: 1 / -0 is -Infinity, while 1 / 0 is Infinity.
| Values | == | === | SameValueZero | SameValue |
|---|---|---|---|---|
NaN and NaN | false | false | true | true |
0 and -0 | true | true | true | false |
"1" and 1 | true | false | false | false |
null and undefined | true | false | false | false |
| Two objects with the same contents | false | false | false | false |
So SameValueZero is === with one change: NaN equals NaN. SameValue makes the same change and one more: it tells 0 and -0 apart, which makes it the strictest of the three. You can call SameValue yourself, as Object.is(a, b). SameValueZero has no function of its own; you meet it inside the built-ins that use it:
| Comparison | Used by |
|---|---|
=== | === and !==, indexOf and lastIndexOf on arrays, and the cases of a switch |
| SameValueZero | includes on arrays, the keys of a Map and the values of a Set, which store -0 as 0, and Map.groupBy, which groups keys as a Map does |
| SameValue | Object.is, React when it compares state and hook dependencies, and toBe in Jest and Vitest |
[NaN].indexOf(NaN); // -1: indexOf uses ===, so it never finds NaN
[NaN].includes(NaN); // true: includes uses SameValueZero
[-0].includes(0); // true
Object.is(NaN, NaN); // true
Object.is(0, -0); // false
const set = new Set([NaN, NaN, 0, -0]);
set.size; // 2: one NaN and one zero
new Map([[-0, "zero"]]).get(0); // "zero": the key -0 was stored as 0
Why make NaN equal itself? For arithmetic, NaN === NaN being false makes sense: two failed calculations needn't be the same failure. For finding things, it's a trap: an array can hold NaN and indexOf still won't find it, and a Map that compared keys with === could store a NaN key but never read it back. So Map and Set (ES2015) and includes (ES2016) were given SameValueZero. It keeps 0 and -0 together, as === does, since nearly all code treats them as the same number.
Write Object.myGroupBy(items, callback) and Map.myGroupBy(items, callback), which work like the built-in Object.groupBy and Map.groupBy without calling them. Both put items into groups and return the groups.
items can be any iterable: an array, a string, a Set, a generator. For each item, in order, call callback(item, index), with exactly those two arguments. What it returns is the item's key. Items with the same key go into one array, in the order they came.
Object.myGroupBy returns an object with no prototype, with one property per key holding that key's array. Properties are created in the order their keys first appear. Because there's no prototype, keys like "constructor" and "__proto__" make ordinary groups.
Object.myGroupBy uses keys as property names, so they're converted to strings (symbols stay symbols): 1 and "1" share a group, and true becomes "true".
Map.myGroupBy returns a Map from each key to its array, with keys in the order they first appear. Keys aren't converted: 1 and "1" are different groups, and an object key groups items by identity. As in any Map, NaN matches NaN.
Throw a TypeError if items isn't iterable (for example null or a number), or if callback isn't a function, even when items is empty.
const words = ["apple", "banana", "avocado"];
const byLetter = Object.myGroupBy(words, (word) => word[0]);
// { a: ["apple", "avocado"], b: ["banana"] }
Object.getPrototypeOf(byLetter); // null
Object.myGroupBy([1, 2, 3, 4], (n, index) => (index < 2 ? "first" : "rest"));
// { first: [1, 2], rest: [3, 4] }
const byParity = Map.myGroupBy([1, 2, 3, 4], (n) => n % 2 === 0);
// Map { false => [1, 3], true => [2, 4] }
byParity.get(true); // [2, 4]
const words = ["banana", "apple", "blueberry", "avocado", "cherry"];
const groups = Object.myGroupBy(words, (word) => word[0]);
expect(groups).toEqual({ b: ["banana", "blueberry"], a: ["apple", "avocado"], c: ["cherry"] });
expect(Object.keys(groups)).toEqual(["b", "a", "c"]);
const words = ["constructor", "__proto__", "toString", "toString"];
const groups = Object.myGroupBy(words, (word) => word);
expect(Object.getPrototypeOf(groups)).toBeNull();
expect(Object.keys(groups)).toEqual(["constructor", "__proto__", "toString"]);
expect(groups.constructor).toEqual(["constructor"]);
expect(groups["__proto__"]).toEqual(["__proto__"]);
expect(groups.toString).toEqual(["toString", "toString"]);
const groups = Object.myGroupBy([1, "1", 2], (x) => x);
expect(Object.keys(groups)).toEqual(["1", "2"]);
expect(groups["1"]).toEqual([1, "1"]);
const byParity = Object.myGroupBy([1, 2, 3], (n) => n % 2 === 0);
expect(Object.keys(byParity)).toEqual(["false", "true"]);
expect(byParity.false).toEqual([1, 3]);
const callback = spy(() => "key");
Object.myGroupBy(["a", "b"], callback);
expect(callback.calls.map((call) => call.args)).toEqual([["a", 0], ["b", 1]]);
const mapCallback = spy(() => "key");
Map.myGroupBy(new Set(["x", "y"]), mapCallback);
expect(mapCallback.calls.map((call) => call.args)).toEqual([["x", 0], ["y", 1]]);
const letters = Object.myGroupBy("hello", (char) => ("aeiou".includes(char) ? "vowels" : "consonants"));
expect(letters).toEqual({ consonants: ["h", "l", "l"], vowels: ["e", "o"] });
expect(Object.myGroupBy(new Set([1, 2, 3]), (n) => n % 2)).toEqual({ 0: [2], 1: [1, 3] });
function* numbers() {
yield 1;
yield 2;
yield 3;
}
const sizes = Map.myGroupBy(numbers(), (n) => (n > 1 ? "big" : "small"));
expect([...sizes]).toEqual([["small", [1]], ["big", [2, 3]]]);
const groups = Map.myGroupBy([1, 2, 3, 4, 5], (n) => (n % 2 === 0 ? "even" : "odd"));
expect(groups).toBeInstanceOf(Map);
expect([...groups.keys()]).toEqual(["odd", "even"]);
expect(groups.get("odd")).toEqual([1, 3, 5]);
expect(groups.get("even")).toEqual([2, 4]);
const red = { name: "red" };
const blue = { name: "blue" };
const players = [
{ name: "Ada", team: red },
{ name: "Bo", team: blue },
{ name: "Cy", team: red },
];
const byTeam = Map.myGroupBy(players, (player) => player.team);
expect(byTeam.size).toBe(2);
expect(byTeam.get(red).map((player) => player.name)).toEqual(["Ada", "Cy"]);
const mixed = Map.myGroupBy([1, "1", true, NaN, NaN], (x) => x);
expect([...mixed.keys()]).toEqual([1, "1", true, NaN]);
expect(mixed.get(NaN)).toEqual([NaN, NaN]);
expect(() => Object.myGroupBy(null, (x) => x)).toThrow(TypeError);
expect(() => Object.myGroupBy(42, (x) => x)).toThrow(TypeError);
expect(() => Map.myGroupBy(undefined, (x) => x)).toThrow(TypeError);
expect(() => Object.myGroupBy([], "key")).toThrow(TypeError);
expect(() => Map.myGroupBy([], undefined)).toThrow(TypeError);
const builtIn = { object: Object.groupBy, map: Map.groupBy };
Object.groupBy = function () {
throw new Error("myGroupBy called the built-in Object.groupBy");
};
Map.groupBy = function () {
throw new Error("myGroupBy called the built-in Map.groupBy");
};
let byObject;
let byMap;
try {
byObject = Object.myGroupBy([1, 2, 3], (n) => (n > 1 ? "big" : "small"));
byMap = Map.myGroupBy([1, 2, 3], (n) => n > 1);
} finally {
Object.groupBy = builtIn.object;
Map.groupBy = builtIn.map;
}
expect(byObject).toEqual({ small: [1], big: [2, 3] });
expect(byMap.get(true)).toEqual([2, 3]);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Object.groupBy and Map.groupBy. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.