Don't break the web
Learn why JavaScript can add features but almost never remove them, through SmooshGate, includes, globalThis, Symbol.unscopables and use counters.
- 01Explain the ruleSay why browsers can't ship a change that breaks old websites, and why the spec follows the browsers.
- 02Trace a name clashShow, with real code, how a new built-in method like
flattenbroke a library's copy loop, and whyflatdidn't. - 03Write code that ages wellAvoid patching built-in objects, avoid for-in on arrays, and check for a feature before you add it.
Don't break the web: new features must fit old pages
Every year, JavaScript gets new features. But the web is full of old pages that nobody will ever update, and browsers must keep running them. So every new feature has to fit next to all the old code already out there. This rule is called "don't break the web", and it explains some odd names in JavaScript.
var prices = [3, [4, [5]]];
console.log(prices.flat().length);
console.log(typeof prices.flatten);
console.log(typeof prices.includes, typeof prices.contains);Line 1 makes an array with arrays inside it. Line 2 calls flat, which pulls the inner items up one level, so the array becomes [3, 4, [5]] and line 2 prints 3.
Line 3 prints undefined: there is no flatten method, even though that is the name most people expect. Line 4 prints function undefined: arrays have includes, but not contains.
flatten and contains, broke real websites when browsers tried them. So line 2 calls flat, and lines 3 and 4 find no flatten or contains.The obvious names were tried first. In each case, a browser shipped the method for testing, and real websites broke. So the spec picked other names. This lesson shows exactly how that happens, and what it means for the code you write.
Picture a city opening a new street. It wants to call it Main Street. But thousands of old address books already list a Main Street, and letters with those addresses are still in the mail. If the city reuses the name, those letters go to the wrong house. Nobody can rewrite every old letter, so the city picks another name, Market Street, and every old letter still arrives.
- In real life: Old address books and letters still in the mail
- In JavaScript: Old websites that nobody updates
- In real life: A new street the city wants to name
- In JavaScript: A new method or global the spec wants to add
- In real life: An old street already called Main Street in those books
- In JavaScript: An old library that already uses the name
flatten - In real life: Calling the new street Market Street instead
- In JavaScript: Shipping the method as
flat
Browsers must keep every old website working. So a new JavaScript feature can't change what old code does, and its name can't clash with names old code already uses. When a name clashes, the feature gets a new name. Removing a feature is even harder, so it almost never happens.
Web compatibility: old pages must keep working
Web compatibility means that a browser keeps running the pages that already exist. The Space Jam movie website from 1996 still works in today's browsers, almost exactly as it was built.
Imagine a browser that ships a change and breaks a popular site. Visitors don't know what changed. They only see that the site works in another browser, so they switch. Other browsers then refuse to ship the change. And a spec that browsers won't follow is just a document nobody uses. So the spec follows what browsers can really ship.
Even a famous mistake has to stay. typeof null gives "object", a bug from the first version of JavaScript in 1995.
var nothing = null;
console.log(typeof nothing);
console.log(typeof nothing === "object");Line 1 stores null, which means "no value". Line 2 prints object, which is wrong: null is not an object. Line 3 prints true.
A proposal once tried to make typeof null give "null". It was turned down, because old code like line 3 counts on the answer "object". The null internals lesson explains where the mistake came from.
New keywords are a problem too. Before 2015, let had no job in JavaScript, so old code could use it as a variable name.
var let = 3;
var async = 4;
var of = 5;
console.log(let + async + of);Lines 1 to 3 make variables named let, async and of. Each of these words has a job in modern JavaScript, but sloppy code, the code without "use strict", still allows them as names. Line 4 prints 12.
Strict mode was new in 2009, so no old page used it. That made it safe to reserve let there.
"use strict";
var let = 3;
console.log(let);Line 1 makes the script strict. Line 2 uses let as a name, which strict code doesn't allow, so the whole script is a SyntaxError.
Not every change is risky. Read each row of the table as: "a change of this kind does this to old websites."
| The kind of change | What it does to old websites |
|---|---|
New syntax that used to be a SyntaxError, such as a?.b | Usually safe. No working page could contain it, because it didn't run before. |
A new method name on a built-in object, such as flatten | Risky. An old library may already use that name in its own way. |
A new global name, such as global | Risky. Old code may test for that name to guess where it is running. |
A new keyword, such as let | Risky. Old code may use the word as a variable name, so sloppy code still allows it. |
A change to what old code does, such as making typeof null give "null" | Almost never allowed. Old code depends on the answer it always got. |
New syntax is the safest change: code like a?.b was a SyntaxError before, so no working page contains it. New names are the risky ones, because old code may already use them.
SmooshGate: why flatten became flat
In 2018, a new array method called flatten was almost finished. Firefox turned it on for testing, and some websites broke. The proposal's author joked that it could be renamed smoosh. Many people took the joke seriously, and the fuss got the name SmooshGate.
The broken sites used MooTools, a library that was popular in the late 2000s. To see what went wrong, you need one idea first: enumerable. Every property has an enumerable setting. A for…in loop visits only the enumerable ones.
var methods = {};
methods.flatten = function () {};
Object.defineProperty(methods, "flat", {
value: function () {},
enumerable: false,
});
for (var key in methods) {
console.log(key);
}Line 2 adds flatten with a plain assignment. A property made this way is enumerable.
Lines 3 to 6 add flat with Object.defineProperty, which lets you choose the settings. Line 5 makes it not enumerable, the way browsers add their built-in methods. Lines 7 to 9 loop over the names, and only flatten is printed.
The second idea is the surprise. Assigning to a property that already exists only changes its value. Its settings stay as they were.
var methods = {};
Object.defineProperty(methods, "flatten", {
value: function () {},
enumerable: false,
writable: true,
});
methods.flatten = function () {};
console.log(Object.keys(methods).length);Lines 2 to 6 make a flatten property that is not enumerable but can be changed. Line 7 assigns a new function to it. The value changes, but the property stays not enumerable, so Object.keys, which lists enumerable names, finds nothing. Line 8 prints 0.
Now the MooTools pattern. It added its own flatten to every array, then copied all of its array methods to its own list type, Elements, with a for…in loop.
Array.prototype.flatten = function () {
return "library flatten";
};
var Elements = {};
for (var key in Array.prototype) {
Elements[key] = Array.prototype[key];
}
console.log(typeof Elements.flatten);Lines 1 to 3 add the library's flatten to Array.prototype, the object all arrays share their methods from. It is a plain assignment, so the property is enumerable.
Lines 5 to 7 copy every enumerable property into Elements. Built-in methods like map are skipped, but flatten is copied, so line 8 prints function. This is what happens in today's browsers.
Now pretend the browser had shipped a built-in flatten. The program below adds it first, the way a browser would.
Object.defineProperty(Array.prototype, "flatten", {
value: function () {},
enumerable: false,
writable: true,
configurable: true,
});
Array.prototype.flatten = function () {
return "library flatten";
};
var Elements = {};
for (var key in Array.prototype) {
Elements[key] = Array.prototype[key];
}
console.log(typeof Elements.flatten);Lines 1 to 6 add a built-in flatten, not enumerable. Lines 7 to 9 are the library's assignment. flatten already exists, so only its value changes, and it stays not enumerable.
Lines 11 to 13 copy the enumerable properties. flatten is skipped, so line 14 prints undefined. Every site that called flatten on an Elements list now throws a TypeError.
flatten itself, so it is enumerable and the loop copies it. Right: the browser already had a built-in flatten. The library's assignment only changes its value, so it stays hidden, and Elements gets no flatten.A replay of the example code, not a view inside a JavaScript engine. It runs on a stand-in for Array.prototype, so this page's arrays don't change.
script
value: function () {}, enumerable: false, writable: true, configurable: true,});Array.prototype.flatten = function () { return "library flatten";};var Elements = {};for (var key in Array.prototype) { Elements[key] = Array.prototype[key];}console.log(typeof Elements.flatten);Press Step and watch the enumerable setting beside line 3. It stays false after line 7. This replays the example code; it isn't a view inside the JavaScript engine. It runs on a stand-in for Array.prototype, so this page's arrays don't change.
Why not make the built-in flatten enumerable? Then every old for…in loop over an array would suddenly visit an extra flatten name, and break different sites. Why not fix MooTools? A fixed version only helps sites that update, and the broken ones are the ones nobody updates. So in May 2018, the committee renamed it.
var prices = [3, [4, [5]]];
console.log(prices.flat().length);
console.log(prices.flat(Infinity).join(" "));
console.log([3, 4].flatMap((p) => [p, p * 2]).join(" "));Line 2 flattens one level, so the array has 3 items and line 2 prints 3. Line 3 passes Infinity, which flattens every level, and prints 3 4 5.
Line 4 uses flatMap, which runs a function on each item and flattens the results one level. Each price becomes a pair, so it prints 3 6 4 8.
contains became includes, and other renamed features
flatten wasn't the first time. In 2014, arrays were going to get a contains method. MooTools had its own array contains, with the same copy problem. So in November 2014, the committee renamed it to includes, and renamed the string method contains to match.
var menu = ["tea", "cake"];
console.log(menu.includes("tea"));
console.log("teacup".includes("cup"));
console.log(typeof menu.contains, typeof "tea".contains);Line 2 asks whether the array holds "tea" and prints true. Line 3 asks whether the string holds "cup" and prints true. Line 4 prints undefined undefined: neither has contains.
The page's own objects still use the old word. The list of classes on an element, element.classList, has a contains method, so you will see both names in real code.
Global names can clash too. Browsers call the global object window, and Node.js calls it global. A proposal tried to add global to browsers as a name that works everywhere. Old code like this stopped working.
var place = typeof global !== "undefined" ? "Node.js" : "a browser";
console.log(place);Line 1 checks whether a name global exists. In a browser it doesn't, so place is "a browser", and line 2 prints a browser.
If browsers added global, the same line would decide it was running in Node.js, and the site would take the wrong path. That broke flickr.com and other sites. So the name became globalThis.
console.log(typeof globalThis);
console.log(globalThis === this);Line 1 prints object. Line 2 prints true: at the top of a classic script, this is the global object too.
The newest rename was for grouping. The plan was an array method groupBy, but old versions of a library called Sugar added their own groupBy to arrays. The next try, group, clashed with other old code. So the committee moved it off arrays entirely.
var menu = [
{ name: "tea", kind: "drink" },
{ name: "cake", kind: "food" },
{ name: "juice", kind: "drink" },
];
var groups = Object.groupBy(menu, (item) => item.kind);
console.log(groups.drink.length, groups.food.length);
console.log(typeof menu.groupBy, typeof menu.group);Lines 1 to 5 make a menu. Line 6 calls Object.groupBy, a static method: one that lives on Object itself, not on every array. It sorts the items into groups by the kind the function returns.
Line 7 prints 2 1: two drinks and one food. Line 8 prints undefined undefined: arrays got neither name.
Read each row of the table as: "the first name was this, it couldn't ship for this reason, so this shipped instead."
| The first name | Why it couldn't ship | What shipped instead |
|---|---|---|
Array.prototype.flatten | MooTools set its own flatten, and its copy loop would skip a built-in one. | flat and flatMap, in 2019 |
String.prototype.contains and Array.prototype.contains | MooTools had the same copy problem with an array contains, and strings were renamed to match. | includes: strings in 2015, arrays in 2016 |
global | Sites such as flickr.com checked typeof global to guess they were in Node.js. | globalThis, in 2020 |
Array.prototype.groupBy, then group | Old versions of the Sugar library, and other code, already used those names on arrays. | Object.groupBy and Map.groupBy, in 2024 |
Symbol.unscopables: hiding new methods from old with blocks
Old code has one more trap: the with statement. Inside with (list) { … }, every plain name is looked up on list first. The with statement lesson explains it.
So every new array method is a new name that with could find first. When 2015 added methods such as values, keys and entries, old with blocks that used variables with those names would have found the methods instead.
The fix is a list called Symbol.unscopables. Names on that list are skipped by with.
var values = [3, 4];
with ([1, 2]) {
console.log(values.join(" "));
}Line 1 makes a variable values. Line 2 starts a with block for the array [1, 2]. That array has a values method, but the name is on the list, so line 3 skips it, finds the variable from line 1, and prints 3 4.
with ([1, 2]), the name values is checked on the array first. The array has a values method, but it is on the Symbol.unscopables list, so with skips it and finds the variable from line 1.Here is what old code would have seen without the list.
var values = [3, 4];
var list = [1, 2];
Object.defineProperty(list, Symbol.unscopables, { value: {} });
with (list) {
console.log(typeof values);
}Line 3 gives this one array its own, empty Symbol.unscopables list. Now nothing is skipped. Inside the block, line 5 finds the array's values method before the variable, and prints function.
var hidden = Array.prototype[Symbol.unscopables];
console.log(hidden.values, hidden.flat, hidden.includes);
console.log(hidden.map);Line 1 reads the list from Array.prototype. Line 2 prints true true true: values, flat and includes are all on it. Line 3 prints undefined: map is older than the list, and old with code already lived with it, so it was never added.
Most new array methods are added to this list as they ship, so old with code keeps working.
Use counters and deprecation: measuring before removing
Before a browser removes or changes anything, it needs to know how many pages would break. Guessing isn't enough. Browsers measure it.
A use counter is a small counter built into the browser. For each page load, it records whether the page used a feature, such as substr. Chrome adds up these counts from many users who allow it, and publishes the share of page loads that use each feature on chromestatus.com. Firefox collects similar numbers.
Here is a tiny model of a use counter, written for this lesson, with four page loads.
var pages = [
["substr", "map"],
["map"],
["escape", "map"],
["map", "substr"],
];
var used = {};
for (var page of pages) {
for (var name of page) {
used[name] = (used[name] || 0) + 1;
}
}
for (var name in used) {
console.log(name + ": " + (used[name] / pages.length) * 100 + "%");
}Lines 1 to 6 list four page loads, and the features each one used. Line 7 makes an empty object, used, for the counts.
Lines 8 to 12 go through each page, and line 10 adds 1 for each feature that page used. Lines 13 to 15 divide each count by the 4 pages and print substr: 50%, map: 100% and escape: 25%.
substr was used on page 1 and page 4, 2 of the 4 pages, so line 14 prints substr: 50%.A replay of the example code, not a view inside a JavaScript engine. The counting code runs for real.
script
var pages = [ ["substr", "map"], ["map"], ["escape", "map"], ["map", "substr"],];for (var page of pages) { for (var name of page) { used[name] = (used[name] || 0) + 1; }}for (var name in used) { console.log(name + ": " + (used[name] / pages.length) * 100 + "%");}Press Step and watch the counts beside line 7 grow, one page at a time. This replays the example code; it isn't a view inside the JavaScript engine.
The real numbers are much smaller. A feature used on even a tiny share of page loads can still mean many thousands of websites. When a browser team does decide to remove a feature, it usually follows these steps:
- Measure how many page loads use it, with a use counter.
- Deprecate it: print a warning in the developer console, so the people who run those sites can find out.
- Wait, and watch the number fall.
- Remove it, if the number is small enough and the other browsers agree.
Chrome removed Object.observe this way in 2016. For the language itself, removal is rare. More often, the spec is changed to describe what browsers already do. That is how the old features in the Annex B lesson got into the spec.
Writing code that keeps working as JavaScript grows
MooTools did nothing strange for its time. Its mistake was changing objects it didn't own. Your code can avoid that mistake, so it keeps working when JavaScript adds new features.
Adding your own methods to Array.prototype causes trouble even today.
Array.prototype.last = function () {
return this[this.length - 1];
};
var prices = [3, 4];
for (var key in prices) {
console.log(key);
}Lines 1 to 3 add a last method to every array with a plain assignment, so it is enumerable. Lines 5 to 7 loop over an array with for…in, which visits every enumerable name. It prints 0, 1, and then last, which isn't an item at all.
And if JavaScript ever ships its own last, your version would replace it on every page that loads your code.
function last(list) {
return list[list.length - 1];
}
var prices = [3, 4];
console.log(last(prices));
console.log(prices.at(-1));Lines 1 to 3 make last a plain function that takes the array as a value. It changes nothing shared, so it can't clash with anything. Line 5 prints 4.
Line 6 uses at(-1), a standard method that counts from the end when given a negative number. It also prints 4.
Sometimes you must support old browsers that lack a standard method. Code that adds a missing standard feature is called a polyfill. A careful polyfill checks first, and adds the method the way a browser would.
if (!Array.prototype.at) {
Object.defineProperty(Array.prototype, "at", {
value: function (index) {
return this[index < 0 ? this.length + index : index];
},
writable: true,
configurable: true,
});
}
console.log([3, 4].at(-1));Line 1 checks whether the browser already has at. Every modern browser does, so lines 2 to 8 are skipped. In an old browser, lines 2 to 8 would add at with Object.defineProperty, not enumerable, so for…in loops don't see it.
Line 4 handles a negative index by counting back from the length. Line 10 prints 4 either way.
Now try being the browser. The playground runs an old website with a MooTools-style library. Pick a name and ship a built-in method with it. Guess first: does the old site still work?
Object.defineProperty(Array.prototype, "flatten", {
value: function () {},
enumerable: false,
writable: true,
configurable: true,
});Array.prototype.flatten = function () {};
Array.prototype.contains = function () {};
var Elements = {};
for (var key in Array.prototype) {
Elements[key] = Array.prototype[key];
}
console.log("flatten: " + typeof Elements.flatten);
console.log("contains: " + typeof Elements.contains);breaksundefinedfunctionThe library assigns its own flatten, but the built-in one was already there and not enumerable, so the loop skips it. Elements.flatten is undefined, and the old site's call to it would throw a TypeError.
Ship flatten or contains: the library's copy loop skips the clashing name, and the old site breaks. Ship flat or includes: the library never uses those names, and everything keeps working. That is exactly why the committee chose them.
- New syntax
a?.b, which used to be a SyntaxError - A built-in
Array.prototype.flatten - A built-in
Array.prototype.flat - A new global named
global - A new global named
globalThis - A static method
Object.groupBy - A built-in
Array.prototype.groupBy - Making
leta reserved word in sloppy code - Making
typeof nullgive"null" - A new array method that is also on
Symbol.unscopables
Put each change in its group: safe to ship, or would break old websites.
Three habits keep your code safe as JavaScript grows: don't add to objects you don't own, such as Array.prototype; loop over arrays with for…of, not for…in; and check for a feature before you add it.
Common mix-ups
Most mix-ups come from thinking that only removing a feature can break things, or that a broken library is the library's problem alone.
- "Adding a feature can't break old code." No. A new name can clash with a name old code already uses, as
flatten,containsandglobaldid. - "The committee renamed
flattentosmoosh." No. That was a joke. The name that shipped isflat. - "Assigning to a property makes it enumerable." No. Assigning to a property that already exists keeps its settings. Only a new property made by assignment is enumerable.
- "Browsers could just tell old sites to update." No. Many old sites have nobody looking after them, so they never update.
- "
letis reserved everywhere." No. Only in strict code. Sloppy code still allows it as a name. - "
typeof nullwill be fixed one day." No. Too much code depends on"object", so the fix was turned down. - "A use counter counts every call." No. It records once per page load whether the page used the feature at all.
The table below puts three of these side by side. Read each row as: "people often think this about the topic, but this is true."
| The topic | What people think | What is true |
|---|---|---|
| Adding a method | It can't break anything, because old code doesn't call it. | Old code may already use the name, or copy methods with a for-in loop. |
| A library's bug | Browsers should ship anyway and let the library fix it. | Old sites never update their copy, so their visitors would get broken pages. |
Symbol.unscopables | It is a list you add your own names to. | It is mainly for the language: it keeps new array methods out of old with blocks. |
Practice exercises
Every program below is a sloppy classic script. For each one, ask which names old code and new code both want, and which properties are enumerable. Guess first, then run it.
What does line 5 print?
var shelf = {};
Object.defineProperty(shelf, "flat", { value: 1, enumerable: false, writable: true });
shelf.flat = 2;
shelf.extra = 3;
console.log(Object.keys(shelf).join(" "), shelf.flat);var shelf = {};
Object.defineProperty(shelf, "flat", { value: 1, enumerable: false, writable: true });
shelf.flat = 2; // still hidden: the property already existed
shelf.extra = 3; // a new property, so it is enumerable
console.log(Object.keys(shelf).join(" "), shelf.flat);Line 3 changes flat to 2, but it stays not enumerable, so Object.keys lists only extra. Line 5 prints extra 2. The comments in the version shown mark which property is hidden.
What does line 4 print? Then rename the variables so the program would also work in strict code.
var let = 1;
var async = 2;
var await = 3;
console.log(let + async + await);var count = 1;
var total = 2;
var wait = 3;
console.log(count + total + wait);Sloppy code allows all three words as names, so line 4 adds 1, 2 and 3 and prints 6. The version shown uses plain names, which work in strict code and modules too, and still prints 6.
What does line 3 print? Then rewrite the program without with.
var keys = "outer";
with ([3, 4]) {
console.log(typeof keys, length);
}var keys = "outer";
var list = [3, 4];
console.log(typeof keys, list.length);keys is skipped, so it is the outer string: "string". length is found on the array: 2. Line 3 prints string 2. The version shown drops with and writes list.length, which says plainly where each name comes from.
The array has 2 items. What does line 9 print? Then fix the program so it counts 2 and still sums the prices.
Array.prototype.sum = function () {
return this.reduce((a, b) => a + b, 0);
};
var prices = [3, 4];
var count = 0;
for (var key in prices) {
count = count + 1;
}
console.log(count);function sum(list) {
return list.reduce((a, b) => a + b, 0);
}
var prices = [3, 4];
var count = 0;
for (var price of prices) {
count = count + 1;
}
console.log(count, sum(prices));The added sum is enumerable, so for…in visits it too, and line 9 prints 3. The version shown makes sum a plain function and loops with for…of, which visits only the items, so it prints 2 7.
Line 1 pretends a browser added a global called global. What does line 3 print? Then write a check that isn't fooled by it.
var global = { name: "pretend" };
var place = typeof global !== "undefined" ? "Node.js" : "a browser";
console.log(place);var global = { name: "pretend" };
var isNode = typeof process !== "undefined" && Boolean(process.versions && process.versions.node);
console.log(isNode ? "Node.js" : "a browser");With global defined, line 2 decides it is running in Node.js and prints Node.js, even in a browser. That is the mistake that broke real sites. The version shown checks for process.versions.node, which only Node.js has, so in a browser it prints a browser.
What does line 9 print? Then change the name the browser ships on line 2 so the old site keeps both of its methods.
var arrayMethods = {};
Object.defineProperty(arrayMethods, "contains", { value: function () {}, writable: true, configurable: true });
arrayMethods.contains = function () { return "library"; };
arrayMethods.flatten = function () { return "library"; };
var Elements = {};
for (var key in arrayMethods) {
Elements[key] = arrayMethods[key];
}
console.log(typeof Elements.contains, typeof Elements.flatten);var arrayMethods = {};
Object.defineProperty(arrayMethods, "includes", { value: function () {}, writable: true, configurable: true });
arrayMethods.contains = function () { return "library"; };
arrayMethods.flatten = function () { return "library"; };
var Elements = {};
for (var key in arrayMethods) {
Elements[key] = arrayMethods[key];
}
console.log(typeof Elements.contains, typeof Elements.flatten);contains stays not enumerable, so the loop skips it, and flatten is copied: line 9 prints undefined function. The version shown ships includes instead, a name the library doesn't use, so both library methods are copied and it prints function function.
Check your understanding
For each question, ask what old code already does with a name, and whether a new feature would change that. Each answer explains why it's right or wrong.
Question 1 of 7Why is the method called flat and not flatten?
Choose an answer to see the explanation.
Question 2 of 7What does assigning to a hidden property print?
Read the code, then predictvar box = {}; Object.defineProperty(box, "size", { value: 1, writable: true }); box.size = 2; console.log(Object.keys(box).length, box.size);Choose an answer to see the explanation.
Question 3 of 7What does a sloppy script with a variable named let print?
Read the code, then predictvar let = 5; console.log(let * 2);Choose an answer to see the explanation.
Question 4 of 7What does entries mean inside with ([1, 2])?
Read the code, then predictvar entries = "mine"; with ([1, 2]) { console.log(entries); }Choose an answer to see the explanation.
Question 5 of 7What does a use counter measure?
Choose an answer to see the explanation.
Question 6 of 7Why did globalThis get such an unusual name?
Choose an answer to see the explanation.
Question 7 of 7What do includes and contains give on an array today?
Read the code, then predictconsole.log(typeof [].includes, typeof [].contains);Choose an answer to see the explanation.
Key takeaways
JavaScript grows by adding, carefully, around everything that already exists. Knowing the rule explains odd names, and helps you write code that won't be the next MooTools.
- Browsers must keep old websites working, and the spec follows what browsers can ship.
- New syntax is usually safe. New names are risky, because old code may already use them.
- Assigning to a property that already exists keeps it not enumerable. That is why a built-in
flattenbroke MooTools' copy loop. flattenbecameflat,containsbecameincludes,globalbecameglobalThis, andgroupBybecameObject.groupBy.Symbol.unscopableskeeps new array methods out of oldwithblocks.- Use counters measure how many page loads use a feature. Removal is rare, and comes after a deprecation warning.
- Don't add to objects you don't own, don't use for…in on arrays, and check before you polyfill.
Remember the one-liner.
Old pages never update, so JavaScript only adds, and picks names no old code uses.
Coming next: The early editions: ES1 to ES5. It tells how JavaScript was first standardized, and about the edition that almost broke it.