{"id":13812760,"url":"https://github.com/tc39/proposal-rm-builtin-subclassing","last_synced_at":"2025-04-19T17:35:54.049Z","repository":{"id":54640994,"uuid":"265717739","full_name":"tc39/proposal-rm-builtin-subclassing","owner":"tc39","description":"Remove ES6 built-in subclassing","archived":false,"fork":false,"pushed_at":"2024-04-30T22:52:46.000Z","size":73,"stargazers_count":38,"open_issues_count":14,"forks_count":5,"subscribers_count":24,"default_branch":"master","last_synced_at":"2025-03-12T21:15:05.047Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tc39.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-21T00:41:53.000Z","updated_at":"2025-02-15T14:13:15.000Z","dependencies_parsed_at":"2022-08-13T22:31:07.796Z","dependency_job_id":null,"html_url":"https://github.com/tc39/proposal-rm-builtin-subclassing","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tc39%2Fproposal-rm-builtin-subclassing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tc39%2Fproposal-rm-builtin-subclassing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tc39%2Fproposal-rm-builtin-subclassing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tc39%2Fproposal-rm-builtin-subclassing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tc39","download_url":"https://codeload.github.com/tc39/proposal-rm-builtin-subclassing/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246174509,"owners_count":20735413,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-04T04:00:55.176Z","updated_at":"2025-03-30T23:32:47.554Z","avatar_url":"https://github.com/tc39.png","language":null,"funding_links":[],"categories":["JavaScript regex evolution"],"sub_categories":["Regex processors, utilities, and more"],"readme":"# Restricting subclassing support in built-in methods\n\nChampions: Shu-yu Guo (Google), Yulia Startsev (Mozilla)\n\nStage: 1\n\nLast presentation: [Notes](https://github.com/tc39/notes/blob/master/meetings/2020-06/june-3.md#restrict-subclassing-support-for-built-in-methods-stage-1), [slides](https://docs.google.com/presentation/d/1vJeJFueDwrj8ebXFdGsEO1J_Q-DzfU01dLEGVd26A9o/edit#slide=id.p)\n\nSubclass instance creation via @@species in built-in methods of `Array`, `RegExp`, `Promise`, and _TypedArray_, as well as property lookups of e.g. `\"exec\"` on the receiver in built-in methods of `RegExp`, are considered by many implementers and some committee members to be one of TC39’s greatest mistakes for the language. It imposes great complexity on the implementation and mental model of the language, the cost which has resulted in many security vulnerabilities.\n\nThis proposal seeks to remove subclassing support via @@species and related machinery, such as property lookups of `\"flags\"` and `\"exec\"` in certain `RegExp` built-ins.\n\nThis is a significant, fill-or-kill, backwards incompatible change that seeks to remove all built-in subclassing machinery. Doing finer-grained removal for certain methods or constructors will result in more confusion and does not remedy the complexity and maintenance burden costs.\n\n# Motivation\n\nSupporting subclassing in built-ins via @@species and related machinery incurs significant burden:\n\n- Increased implementation complexity and maintainability\n- Performance cliffs\n- Security bugs\n\n[Natalie Silvanoich](https://github.com/natashenka) from Project Zero gave a [talk](https://docs.google.com/presentation/d/11fkQeEisoszNGF8SrautVT1ltSnsQBWRxJ4usoc-g_o/edit#slide=id.g2b34aaab4a_0_10) to TC39 in 2018 that called out the effect of @@species on security vulnerabilities. Below is a list of security bugs caused by @@species:\n\nChrome:\n- https://bugs.chromium.org/p/chromium/issues/detail?id=920491\n- https://bugs.chromium.org/p/chromium/issues/detail?id=840106\n- https://bugs.chromium.org/p/chromium/issues/detail?id=804971\n- https://bugs.chromium.org/p/chromium/issues/detail?id=800356\n- https://bugs.chromium.org/p/chromium/issues/detail?id=726622\n- https://bugs.chromium.org/p/chromium/issues/detail?id=726636\n- https://bugs.chromium.org/p/chromium/issues/detail?id=799952 (no security impact in release build)\n\nFirefox\n\n- https://bugzilla.mozilla.org/show_bug.cgi?id=1537924\n\nSupporting built-in subclassing has also negatively affected authoring of new built-ins. Current and future spec authors, often in deference to consistency, propagate the @@species machinery.\n\n@@species has also negatively affected other specifications that interact with JavaScript, like WebAssembly, that were not aware of this corner of the specification.\n\n# Taxonomy of subclassing\n\n@domenic has an excellent taxonomy for subclassing built-ins in JS, which I adopt here and modified slightly. JS currently supports all of the following types of subclassing of built-ins.\n\n## Type I: minimal support\n\nType I is supported if creating subclasses of built-ins is possible. For example, if derived class can call `super()`. Support for this is provided via `new.target`.\n\n### Example\n\n```js\nclass A extends Array {\n  constructor(a,b,c) {\n    super(a,b,c);\n  }\n}\nnew A(1,2,3);      // return type: A\n```\n\nWithout Type I support, and doesn't support `new.target` the following would be true\n\n```js\nclass A extends Array {\n  constructor(a,b,c) {\n    super(a,b,c);\n  }\n}\nnew A(1,2,3);      // return type: Array\n```\n\n\n### Cost benefit\n\n㊟ **There is crucial dependence on Type I subclassing and it is worth the implementation and language cost.**\n\nType I is used by user libraries, as well as by the web platform in WebIDL and DOM.\n\n## Type II: subclass instance creation in built-in methods\n\nType II is supported if built-in methods create new instances of the subclass. For example, if `Array.prototype.map` or `Array.from` returns instances of subclasses of `Array`. Support for this is _subsumed_ by the support for Type III below via `this.constructor[`@@species`]`.\n\n### Example\n\n```js\nclass A extends Array { }\n\nA.from([1,2,3])    // return type: A\n .map(x =\u003e x + 1); // return type: A\n```\n\n### Cost benefit\n\n㊟ **Beneficial, but at cost.**\n\nType II is the intuition enjoyed by many developers. It enables user libraries to subclass built-ins like `Array` without also having to maintain overrides of all instance-creating methods.\n\nHowever, it incurs implementation complexity in that built-in methods have overrideable behavior that results in arbitrary code being executed via `this.constructor`. In implementations, this results in a proliferation of slow-paths and invariants that cause JIT code to deoptimize. Failure to do so may and have resulted in serious security vulnerabilities in browsers. In the language, `this.constructor` resulting in possible arbitrary code execution in some built-ins increases difficulty of reasoning.\n\n## Type III: customizable subclass instance creation in built-in methods\n\nType III is supported if built-in methods create new instances of the subclass's choosing. For example, if `Array.prototype.map` or `Array.from` returns instances of subclasses of `Array` via `SubclassConstructor[`@@species`]`. Support for this is provided by delegating to `this.constructor[`@@species`]` inside built-in methods with custom values for the @@species property.\n\nThe main difference between Type II and Type III is user expectation, not implementation. Type II is the user expectation that built-in methods, when called on instances of subclasses, have some way of querying the class of those instances and create instances of the subclass. Type III is the addition that subclasses themselves can override Type II behavior programmatically.\n\n### Example\n\n```js\nclass A extends Array {\n  static [Symbol.species] = Array;\n}\n\nA.from([1,2,3])    // return type: A\n .map(x =\u003e x + 1); // return type: Array\n```\n\n### Cost benefit\n\n㊟ **Not useful, and at great cost.**\n\nType III gives subclasses expressivity to actually _opt out_ of Type II support. If `NodeList.prototype.map`, as inherited from `Array.prototype.map`, actually wanted to return an `Array` instead of a `NodeList`, it would opt out via setting `NodeList[`@@species`]` to `Array`. In more complex cases, each instance-creating method may have its own consideration, and a single @@species value on the constructor is insufficient.\n\nSupporting @@species compounds the cost already incurred by Type II by making the paths even more complex and the invariants more brittle. In the language, the fact that developers have to think at all about @@species, which is not generally useful, is harmful.\n\nThere are no known compelling use cases that are worth this cost.\n\n## Type IV: delegation to property lookups in built-in methods\n\nType IV is supported if built-in methods consult properties on instances instead of internal slots. For example, if `RegExp.prototype[`@@match`]` calls `this.exec` instead of the built-in RegExp exec. Support for this is provided by, well, delegating to property lookups.\n\nNote that `RegExp`'s @@match, @@matchAll, @@replace, @@search, and @@split symbols themselves aren't strictly only for subclassing support, as they are not used in `RegExp` methods themselves. Instead, they are used as a protocol for `String` so that completely custom `RegExp` instances may be consumed.\n\n### Example\n\nI hope this example demonstrates that one cannot subclass `RegExp` piecemeal and have a good time.\n\n```js\nfunction R() { }\nObject.setPrototypeOf(R, RegExp);\nObject.setPrototypeOf(R.prototype, RegExp.prototype);\nR.prototype.exec = function() {\n  console.log(\"overridden\");\n  return null;\n};\n// Define a new .global since RegExp#global throws on\n// non-RegExp-branded `this`\nObject.defineProperty(R.prototype, \"global\", { value: false });\nconsole.log(\"some string\".match(new R(\"foo\")))     // logs \"overridden\"\n```\n\n### Cost benefit\n\n㊟ **Harmful, and at great cost.**\n\nType IV is harmful expressivity. It is very difficult for implementations to provide robust fast paths at all for `RegExp`, which users have high performance expectations of. The cost of this, depending on the number of overrideable properties, is the cost of Type II and III combined, and then some.\n\nIt also makes the language significantly more complex to reason about for built-ins that support it. Users should not be subclassing `RegExp`s piecemeal, and overriding subsets of behaviors via `exec` or one of the flag properties like `global` and expect to have a good time. And similarly for `Promise`s. It also makes the spec very hard to understand (cf PromiseCapabilities).\n\n(Since `RegExp`'s symbols aren't for subclassing, they are not considered harmful in this context.)\n\n# Proposed new old semantics\n\nWe propose to remove support for Type II, Type III, and Type IV subclassing, and only keep Type I.\n\n## `Array`\n\n### Prototype methods\n\nThe following methods on `Array.prototype` will create and return an `Array` exotic object in the current Realm. They will no longer consult `this.constructor[`@@species`]`.\n\n- `Array.prototype.concat`\n- `Array.prototype.filter`\n- `Array.prototype.flat`\n- `Array.prototype.flatMap`\n- `Array.prototype.map`\n- `Array.prototype.slice`\n- `Array.prototype.splice`\n\nThis means a subclass calling these methods on subclass instances will always get `Array` instances back.\n\nBefore this change:\n\n```javascript\nclass MyArray extends Array { /* ... */ }\nlet ma = (new MyArray(42)).map((x) =\u003e x);\nconsole.log(ma instanceof MyArray) // true\n```\n\nAfter this change:\n\n```javascript\nclass MyArray extends Array { /* ... */ }\nlet ma = (new MyArray(42)).map((x) =\u003e x);\nconsole.log(ma instanceof MyArray) // false\n// Result is an Array, not a MyArray.\n```\n\n### Constructor methods\n\nThe following methods on `Array` will create and return an `Array` exotic object in the current Realm. They will no longer conditionally use the `this` value as a constructor if IsConstructor(`this`) is true.\n\n- `Array.from`\n- `Array.fromAsync`\n- `Array.of`\n\nThis means any subclass calling these methods on the subclass constructor will always get `Array` instances back.\n\nBefore this change:\n\n```javascript\nclass MyArray extends Array { /* ... */ }\nlet ma = MyArray.from([1,2,3]);\nconsole.log(ma instanceof MyArray) // true\n```\n\nAfter this change:\n\n```javascript\nclass MyArray extends Array { /* ... */ }\nlet ma = MyArray.from([1,2,3]);\nconsole.log(ma instanceof MyArray) // false\n// Result is an Array, not a MyArray.\n```\n\n### Removing @@species\n\n`Array[`@@species`]` will be removed. This means that the following will no longer be possible using\nthe `@@species` symbol like this:\n\n```js\nclass A extends Array {\n  static [Symbol.species] = OtherArray;\n}\n\nA.from([1,2,3])     // return type: A\n  .map(x =\u003e x + 1); // return type: OtherArray\n```\n\n\n## `RegExp`\n\nRegExp subclassing machinery involves both @@species and dynamic property lookups on the `this` value in various prototype methods. Property lookups will be removed in favor of internal slots. @@species will be removed in favor of creating `RegExp` objects in the current Realm.\n\nNotably, the protocol for `RegExp`-likes (e.g. @@match) is not removed as part of this proposal since they are not used by `RegExp` instance or constructor methods themselves.\n\n### Prototype methods\n\nMethods on `RegExp.prototype` will have the following changes where applicable.\n\n- If `this` does not have [[RegExpMatcher]], throw a TypeError.\n- When creating new `RegExp` instances, a %RegExp% instance will be created in the current Realm instead of consulting `this.constructor[`@@species`]`.\n- `this.`[[OriginalFlags]] will be consulted instead of `this.flags`.\n- `this.`[[OriginalFlags]] will be consulted instead of `this.dotAll`.\n- `this.`[[OriginalFlags]] will be consulted instead of `this.global`.\n- `this.`[[OriginalFlags]] will be consulted instead of `this.ignoreCase`.\n- `this.`[[OriginalFlags]] will be consulted instead of `this.sticky`.\n- `this.`[[OriginalFlags]] will be used instead of `this.unicode`.\n- `this.`[[OriginalSource]] will be used instead of `this.source`.\n- RegExpBuiltinExec will be used instead of `this.exec`.\n\nThese changes apply to the following methods.\n\n- `RegExp.prototype[`@@match`]`\n- `RegExp.prototype[`@@matchAll`]`\n- `RegExp.prototype[`@@replace`]`\n- `RegExp.prototype[`@@search`]`\n- `RegExp.prototype[`@@split`]`\n- `RegExp.prototype.test`\n\n### `String` prototype methods\n\nNotably, the `String` prototype methods are not proposed to be modified.\n\n### Removing @@species\n\n`RegExp[`@@species`]` will be removed.\n\n\n### Example\n\nBefore this change:\n\n```js\nclass R extends RegExp {\n  exec() {\n    return \"overridden\";\n  }\n}\nconsole.log(\"some string\".match(new R(\"foo\")))     // \"overridden\"\n```\n\nAfter this change:\n\n```js\nclass R extends RegExp {\n  exec() {\n    return \"overridden\";\n  }\n}\nconsole.log(\"some string\".match(new R(\"foo\")))     // null\n```\n\n## `Promise`\n\n### Prototype methods\n\nThe following methods on `Promise.prototype` will create and return a `Promise` object in the current Realm. They will no longer consult `this.constructor[`@@species`]`.\n\n- `Promise.prototype.finally`\n- `Promise.prototype.then`\n\nThis means a subclass calling these methods on subclass instances will always get `Promise` instances back.\n\nBefore this change:\n\n```javascript\nclass MyPromise extends Promise { /* ... */ }\nlet mp = (new MyPromise(executor)).then(() =\u003e {});\nconsole.log(mp instanceof MyPromise) // true\n```\n\nAfter this change:\n\n```javascript\nclass MyPromise extends Promise { /* ... */ }\nlet mp = (new MyPromise(executor)).then(() =\u003e {});\nconsole.log(mp instanceof MyPromise) // false\n// Result is a Promise, not a MyPromise.\n```\n\n### Constructor methods\n\nThe following methods on `Promise` will create and return a `Promise` object in the current Realm. They will ignore the `this` value.\n\n- `Promise.all`\n- `Promise.allSettled`\n- `Promise.any`\n- `Promise.race`\n- `Promise.reject`\n- `Promise.resolve`\n\nThis means any subclass calling these methods on the subclass constructor will always get `Promise` instances back.\n\n\nBefore this change:\n\n```javascript\nclass MyPromise extends Promise { /* ... */ }\nlet mp = MyPromise.resolve(() =\u003e {});\nconsole.log(mp instanceof MyPromise) // true\n```\n\nAfter this change:\n\n```javascript\nclass MyPromise extends Promise { /* ... */ }\nlet mp = MyPromise.resolve(() =\u003e {});\nconsole.log(mp instanceof MyPromise) // false\n// Result is an Promise, not a MyPromise.\n```\n\n### Removing @@species\n\n`Promise[`@@species`]` will be removed.\n\n## _TypedArray_\n\n### Constructor\n\n- The _TypedArray_`(` _typedArray_ `)` constructor will no longer use SpeciesConstructor in step 16 and will use %ArrayBuffer%.\n- Step 17 becomes unnecessary and is removed.\n\n### Prototype methods\n\nThe following methods on _TypedArray_`.prototype` will create and return a _TypedArray_ object in the current Realm. They will no longer consult `this.constructor[`@@species`]`.\n\n- _TypedArray_`.prototype.filter`\n- _TypedArray_`.prototype.map`\n- _TypedArray_`.prototype.slice`\n- _TypedArray_`.prototype.subarray`\n\nThis means a subclass calling these methods on subclass instances will always get _TypedArray_ instances back.\n\nBefore this change:\n\n```javascript\nclass MyBuffer extends Uint8Array { /* ... */ }\nlet mb = (new MyBuffer(42)).filter((x) =\u003e true);\nconsole.log(mb instanceof MyBuffer) // true\n```\n\nAfter this change:\n\n```javascript\nclass MyBuffer extends Uint8Array { /* ... */ }\nlet mb = (new MyBuffer(42)).filter((x) =\u003e true);\nconsole.log(mb instanceof MyBuffer) // false\n// Result is a Uint8Array, not a MyBuffer.\n```\n\n### Constructor methods\n\nThe following methods on _TypedArray_ will create and return an _TypedArray_ exotic object in the current Realm. They will ignore the `this` value.\n\n- _TypedArray_`.from`\n- _TypedArray_`.of`\n\nThis means any subclass calling these methods on the subclass constructor will always get _TypedArray_ instances back.\n\n```javascript\nclass MyBuffer extends Uint8Array { /* ... * / }\nlet mb = MyBuffer.from([1,2,3]);\n// Result is an Uint8Array, not a MyBuffer.\n```\n\n### Removing @@species\n\n_TypedArray_`[`@@species`]` will be removed.\n\n## `ArrayBuffer`\n\n### Prototype methods\n\n`ArrayBuffer.prototype.slice` will create and return an %ArrayBuffer% object in the current Realm. It will no longer consult `this.constructor[`@@species`]`.\n\n### Removing @@species\n\n`ArrayBuffer[`@@species`]` will be removed.\n\n## `SharedArrayBuffer`\n\n### Prototype methods\n\n`SharedArrayBuffer.prototype.slice` will create and return an %SharedArrayBuffer% object in the current Realm. It will no longer consult `this.constructor[`@@species`]`.\n\n### Removing @@species\n\n`SharedArrayBuffer[`@@species`]` will be removed.\n\n## `Map`\n\n### Removing @@species\n\n`Map[`@@species`]` will be removed. It is currently not used.\n\n## `Set`\n\n### Removing @@species\n\n`Set[`@@species`]` will be removed. It is currently not used.\n\n## Meta\n\n`Symbol.species` will remain as a vestigial symbol if any user code wants to use it in its own subclassing protocol.\n\n# Web compatibility\n\nBuilt-in subclassing was added as part of ES6. All major browsers have shipped support for it for years: Chrome since 51, Firefox since 41, and Safari since 10. The compatibility risk of unshipping @@species is very real.\n\nThere is a cross-vendor concerted effort to assess compatibility risk. Current efforts include, but are not limited to:\n\n1. Using Chrome UseCounter data to get a conservative picture of usage of subclassing mechanisms, namely @@species and `.constructor`.\n1. Using queries on HTTP Archive.\n1. Using a crawler to check more in depth URLs from queries on HTTP Archive.\n1. Using instrumented builds of browsers to manually check for breakage.\n\nVery preliminary numbers suggest that there is significant number of occurrences (up to 2% of all page visits in Chrome) of modifying `.constructor` or @@species on `Array`, `RegExp`, and `Promise`, and much less so in _TypedArray_ constructors (up to 0.04% of all page visits in Chrome). Manual inspection of the web sites that do such modifications reveal that they are not real uses of built-in subclassing but are instead of an [outdated core-js shim](https://github.com/zloirock/core-js/blob/9ed55e682ff0a9814b6bb0c15c8c058bdfb8d954/src/%24.species.js) unconditionally installing a `function() { return this; }` as the @@species getter.\n\nThus, the working hypothesis is that most of the real uses are false positives due to outdated shims, and this change is by and large web compatible.\n\n## Hunch by subclassing type\n\n- Removing Type II has the biggest compatibility risk\n- Removing Type III is likely to be compatible\n- Removing Type IV is likely to be compatible\n\n## Notable libraries that break\n\n### Node.js `Buffer`s and the `Buffer` [polyfill](https://github.com/feross/buffer)\n\n`Buffer` is a subclass of `Uint8Array`. Uses of `Uint8Array.prototype.map`, `Uint8Array.prototype.filter`, `Uint8Array.prototype.subarray`, and `Uint8Array.prototype.slice` will produce `Uint8Array`s in the proposed semantics instead of `Buffer`s. Note that `Buffer` overrides `slice`, but inherits the other three methods.\n\n# Exit criteria\n\nIf removing Type III and Type IV is not web compatible, this proposal shall be withdrawn.\n\nIf removing Type II is not web compatible (or if there is renewed consensus in TC39 to uphold developer intuition) but removing Type III and Type IV subclassing is web compatible, then this proposal shall explore alternative ways to support only Type II with less implementation and security burden. If no good alternative arises, and implementers deem the benefits of removing Type III and Type IV alone does not justify changing behavior, this proposal shall be withdrawn.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftc39%2Fproposal-rm-builtin-subclassing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftc39%2Fproposal-rm-builtin-subclassing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftc39%2Fproposal-rm-builtin-subclassing/lists"}