Sometimes the name of a proposed feature (a method, a global variable, etc.) clashes with existing code and has to be changed. This blog post explains how that can happen and lists features that were renamed.
One core principle for evolving JavaScript is to not “break the web”: All existing code must continue to work after a new feature is added to the language.
The downside is that existing quirks can’t be removed from the language. But the upsides are considerable: Old code continues to work, upgrading to new ECMAScript versions is simple, etc.
For more information on this topic, see section “Evolving JavaScript: Don’t break the web” in “JavaScript for impatient programmers”.
When a name is chosen for a new feature such as a method name, one important test is to add that feature in a nightly release (an early pre-release) of a browser and check if any websites exhibit bugs. The next sections cover four sources of conflict where that was the case in the past and features had to be renamed.
In JavaScript, we can add methods to built-in values by changing their prototypes:
// Creating a new Array method
Array.prototype.myArrayMethod = function () {
return this.join('-');
};
assert.equal(
['a', 'b', 'c'].myArrayMethod(), 'a-b-c'
);
// Creating a new string method
String.prototype.myStringMethod = function () {
return '¡' + this + '!';
};
assert.equal(
'Hola'.myStringMethod(), '¡Hola!'
);
It’s fascinating that the language can be changed in this manner. This kind of runtime modification is called a monkey patch. The next subsection explains that term. Then we’ll look at the downsides of such modifications.
If we add methods to built-in prototypes, we are modifying a software system at runtime. Such modifications are called monkey-patches. I try to avoid jargon, including this term, but it’s good to be aware of it. There are two possible explanations for its meaning (quoting Wikipedia):
It came “from an earlier term, guerrilla patch, which referred to changing code sneakily – and possibly incompatibly with other such patches – at runtime. The word guerrilla, homophonous with gorilla (or nearly so), became monkey, possibly to make the patch sound less intimidating.”
It “refers to ‘monkeying about’ with the code (messing with it).”
With any kind of global namespace, there is always a risk of name clashes. That risk goes away when there are mechanisms to resolve conflicts – for example:
Global modules are identified via bare module specifiers or URLs. Name clashes among the former are prevented via the npm registry. Name clashes among the latter are prevented via domain name registries.
Symbols were added to JavaScript to avoid name clashes between methods. For example, any object can become iterable by adding a method whose key is Symbol.iterator
. Since each symbol is unique, this key never clashes with any other property key.
However, methods with string keys can cause name clashes:
Array.prototype
.Ironically, being careful with adding a method can make matters even worse – for example:
if (!Array.prototype.libraryMethod) {
Array.prototype.libraryMethod = function () { /*...*/ };
}
Here, we check if a method already exists. If not, we add it.
This technique works if we are implementing a polyfill that adds a new JavaScript method to engines that don’t support it. (That’s a legitimate use case for modifying built-in prototypes, by the way. Maybe the only one.)
However, if we use this technique for a normal library method and JavaScript later gets a method with the same name, then the two implementations work differently and all code that uses the library method breaks when it uses the built-in method.
The ES6 method String.prototype.includes()
was originally .contains()
, which clashed with a method that was added globally by the JavaScript framework MooTools (bug report).
The ES2016 method Array.prototype.includes()
was originally .contains()
which clashed with a method added by MooTools (bug report).
The ES2019 method Array.prototype.flat()
was originally .flatten()
which clashed with MooTools (bug report, blog post).
You may be wondering: How could the creators of MooTools have been so careless? However, adding methods to built-in prototypes wasn’t always considered bad style. Between ES3 (December 1999) and ES5 (December 2009), JavaScript was a stagnant language. Frameworks such as MooTools and Prototype improved it. The downsides of their approaches only became obvious after JavaScript’s standard library grew again.
The ES2022 method Array.prototype.at()
was originally .item()
. It had to be renamed because the following libraries checked for property .item
to determine if an object is an HTML collection (and not an Array): Magic360, YUI 2, YUI 3 (related section in the proposal).
Since ES2020, we can access the global object via globalThis
. Node.js has always used the name global
for this purpose. The original plan was to standardize that name for all platforms.
However, the following pattern is used frequently to determine the current platform:
if (typeof global !== 'undefined') {
// We are not running on Node.js
}
This pattern (and similar ones) would break if browsers also had a global variable named global
. Therefore, the standardized name was changed to globalThis
.
with
with
statement Using JavaScript’s with
statement has been discouraged for a long time and was even made illegal in strict mode, which was introduced in ECMAScript 5. Among other locations, strict mode is active in ECMAScript modules.
The with
statement turns the properties of an object into local variables:
const myObject = {
ownProperty: 'yes',
};
with (myObject) {
// Own properties become local variables
assert.equal(
ownProperty, 'yes'
);
// Inherited properties become local variables, too
assert.equal(
typeof toString, 'function'
);
}
with
The framework Ext.js uses code that is loosely similar to the following fragment:
function myFunc(values) {
with (values) {
console.log(values); // (A)
}
}
myFunc([]); // (B)
When the ES6 method Array.prototype.values()
was added to JavaScript, it broke myFunc()
if it was called with an Array (line B): The with
statement turned all properties of the Array values
into local variables. One of them was the inherited property .values
. Therefore, the statement in line A logged Array.prototype.values
, not the parameter values
anymore (bug report 1, bug report 2).
with
The public symbol Symbol.unscopables
lets an object hide some properties from the with
statement. It is used only once in the standard library, for Array.prototype
:
assert.deepEqual(
Array.prototype[Symbol.unscopables],
{
__proto__: null,
at: true,
copyWithin: true,
entries: true,
fill: true,
find: true,
findIndex: true,
flat: true,
flatMap: true,
includes: true,
keys: true,
values: true,
}
);
The list of unscopables consists of values
and methods introduced alongside or after it.
We have seen four ways in which proposed JavaScript constructs can name-clash with existing code:
with
Some sources of conflict are difficult to predict, but a few general rules exist:
The safest way for a library to provide functionality for JavaScript values is via functions. Should JavaScript get a pipe operator, we could even use them like methods.
Question for readers: Did I forget any interesting cases where proposed names had to be changed?
Section “Evolving JavaScript: Don’t break the web” in “JavaScript for impatient programmers”
Section “Polyfills: emulating native web platform features” in “JavaScript for impatient programmers”
Section “Strict mode vs. sloppy mode” in “JavaScript for impatient programmers”
Section “The with
statement” in “Speaking JavaScript”
Chapter “Symbols” in “JavaScript for impatient programmers”
Section “Property key Symbol.unscopables
” in “Exploring ES6”
Blog post “A pipe operator for JavaScript: introduction and use cases”