JavaScript naming conflicts: How existing code can force proposed features to be renamed

[2022-03-07] dev, javascript, jslang
(Ad, please don’t block)

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.

Evolving JavaScript: Don’t break the web!  

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.

Source of conflict: adding methods to built-in prototypes  

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.

Term: monkey patch  

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).”

Reasons against changing built-in prototypes  

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:

  • Different libraries might use the same name for methods they add to Array.prototype.
  • If a name is already used by a library anywhere, it can’t be used for a new feature of JavaScript’s standard library anymore. There are several cases where that was an issue. They are described in the next section.

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.

Examples of proposed prototype methods whose names had to be changed  

  • 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).

Modifying built-in prototypes wasn’t always considered bad style  

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.

Source of conflict: checking for the existence of a property  

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).

Source of conflict: checking for the existence of a global variable  

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.

Source of conflict: creating local variables via with  

JavaScript’s 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'
  );
}

Conflicts due to 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).

Unscopables: preventing conflicts caused by 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.

Conclusion  

We have seen four ways in which proposed JavaScript constructs can name-clash with existing code:

  • Adding methods to built-in prototypes
  • Checking for the existence of a property
  • Checking for the existence of a global variable
  • Creating local variables via with

Some sources of conflict are difficult to predict, but a few general rules exist:

  • Don’t change global data.
  • Avoid checking for existence or non-existence of global data.
  • Be aware that built-in values might get additional properties in the future (own or inherited ones).

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?

Further reading