Skip to content
This repository has been archived by the owner on Nov 17, 2020. It is now read-only.

Commit

Permalink
Rename from "contains" to "includes"
Browse files Browse the repository at this point in the history
Per the November 2014 TC39 meeting, we're going to try doging the web-compatibility problems by using the name "includes" instead.
  • Loading branch information
domenic committed Nov 20, 2014
1 parent 4fafe65 commit 4b6b953
Show file tree
Hide file tree
Showing 36 changed files with 183 additions and 185 deletions.
48 changes: 24 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# `Array.prototype.contains` Proposal
# `Array.prototype.includes` Proposal

## Status

This proposal is officially in stage 1 of [the TC39 process](https://docs.google.com/document/d/1QbEE0BsO4lvl7NFTn5WXWeiEIBfaVUF7Dk0hpPpPDzU/edit); the author believes it is ready to advance to stage 2, but has not yet had time to present to the committee.

However, its further advancement is blocked by the fact that [it is not web-compatible](http://esdiscuss.org/topic/having-a-non-enumerable-array-prototype-contains-may-not-be-web-compatible). There are [thoughts](http://esdiscuss.org/topic/array-prototype-contains-solutions) of how we could work toward a solution, but nothing is clear yet.
This proposal was formerly for `Array.prototype.contains`, but that name [is not web-compatible](http://esdiscuss.org/topic/having-a-non-enumerable-array-prototype-contains-may-not-be-web-compatible). Per the November 2014 TC39 meeting, the name of both `String.prototype.contains` and `Array.prototype.contains` was changed to `includes` to dodge that bullet.

## Motivation

When using ECMAScript arrays, it is commonly desired to determine if the array contains an element. The prevailing pattern for this is
When using ECMAScript arrays, it is commonly desired to determine if the array includes an element. The prevailing pattern for this is

```js
if (arr.indexOf(el) !== -1) {
Expand All @@ -20,44 +20,44 @@ with various other possibilities, e.g. `arr.indexOf(el) >= 0`, or even `~arr.ind

These patterns exhibit two problems:

- They fail to "say what you mean": instead of asking about whether the array contains an element, you ask what the index of the first occurrence of that element in the array is, and then compare it or bit-twiddle it, to determine the answer to your actual question.
- They fail to "say what you mean": instead of asking about whether the array includes an element, you ask what the index of the first occurrence of that element in the array is, and then compare it or bit-twiddle it, to determine the answer to your actual question.
- They fail for `NaN`, as `indexOf` uses Strict Equality Comparison and thus `[NaN].indexOf(NaN) === -1`.

## Proposed Solution

We propose the addition of an `Array.prototype.contains` method, such that the above patterns can be rewritten as
We propose the addition of an `Array.prototype.includes` method, such that the above patterns can be rewritten as

```js
if (arr.contains(el)) {
if (arr.includes(el)) {
...
}
```

This has almost the same semantics as the above, except that it uses the SameValueZero comparison algorithm instead of Strict Equality Comparison, thus making `[NaN].contains(NaN)` true.
This has almost the same semantics as the above, except that it uses the SameValueZero comparison algorithm instead of Strict Equality Comparison, thus making `[NaN].includes(NaN)` true.

Thus, this proposal solves both problems seen in existing code.

We additionally add a `fromIndex` parameter, similar to `Array.prototype.indexOf` and `String.prototype.contains`, for consistency.
We additionally add a `fromIndex` parameter, similar to `Array.prototype.indexOf` and `String.prototype.includes`, for consistency.

## FAQs

### Why `contains` instead of `has`?
### Why `includes` instead of `has`?

If you survey existing APIs, `has` is used for conceptual "keys," whereas `contains` is used for conceptual "values." That is:
If you survey existing APIs, `has` is used for conceptual "keys," whereas `includes` is used for conceptual "values." That is:

- Keys inside a key-value map: `Map.prototype.has(key)`, `WeakMap.prototype.has(key)`, `Reflect.has(target, propertyKey)`
- Sets, whose elements are conceptually both keys and values: `Set.prototype.has(value)`, `WeakSet.prototype.has(value)`, `Reflect.Loader.prototype.has(name)`
- Strings, which are conceptually maps from indices to code points: `String.prototype.contains(searchString, position)`
- Strings, which are conceptually maps from indices to code points: `String.prototype.includes(searchString, position)`

The best consistency here is with `String`, not with `Map` or `Set`.

Finally, the web has classes like [DOMStringList](https://developer.mozilla.org/en-US/docs/Web/API/DOMStringList) and [DOMTokenList](http://dom.spec.whatwg.org/#interface-domtokenlist) which are array-like, and have methods named `contains` with the same semantics. Meshing with those, and in the case of `DOMStringList` potentially replacing them, is a nice side benefit.
The web has classes like [DOMStringList](https://developer.mozilla.org/en-US/docs/Web/API/DOMStringList) and [DOMTokenList](http://dom.spec.whatwg.org/#interface-domtokenlist) which are array-like, and have methods named `contains` with the same semantics as our `includes`. Unfortunately, meshing with those is not web-compatible, as explained above; we will have to accept this inconsistency.

### But `String.prototype.contains` works on strings, not characters!?
### But `String.prototype.includes` works on strings, not characters!?

Yes, that's true. The best way to think about this is that `String.prototype.indexOf` and `String.prototype.contains` behave like their `Array.prototype` counterparts in the special case of a single character. But the string versions can also be used in the more general case of a larger string.
Yes, that's true. The best way to think about this is that `String.prototype.indexOf` and `String.prototype.includes` behave like their `Array.prototype` counterparts in the special case of a single character. But the string versions can also be used in the more general case of a larger string.

So in this way, the relationship between `String.prototype.contains` and `Array.prototype.contains` is the same as the relationship between `String.prototype.indexOf` and `Array.prototype.indexOf`.
So in this way, the relationship between `String.prototype.includes` and `Array.prototype.includes` is the same as the relationship between `String.prototype.indexOf` and `Array.prototype.indexOf`.

### Why SameValueZero?

Expand All @@ -72,21 +72,21 @@ There are four equality algorithms in the current ES6 draft:

Using Abstract Equality Comparison would be bonkers, of course. Using SameValue is not a good idea for the same reasons it is not used by `Map` and `Set`. (Briefly: `-0`s can sneak into your code fairly easily via arithmetic operations, but you almost always desire `-0` to be treated the same as `+0`, so distinguishing them will just cause spurious failures.) This leaves Strict Equality Comparison and SameValueZero as the two possibilities.

SameValueZero is generally the better choice, as it allows you to detect if an array contains a `NaN`. The argument for Strict Equality Comparison boils down to "bug compatibility" with `Array.prototype.indexOf`. But one of the purposes of `Array.prototype.contains` is to steer users away from creating these sort of bugs.
SameValueZero is generally the better choice, as it allows you to detect if an array includes a `NaN`. The argument for Strict Equality Comparison boils down to "bug compatibility" with `Array.prototype.indexOf`. But one of the purposes of `Array.prototype.includes` is to steer users away from creating these sort of bugs.

This introduces a slight refactoring hazard from `Array.prototype.indexOf` to `Array.prototype.contains`: they will indeed behave differently for arrays containing `NaN`s. However, it seems much more likely that code will become _less_ buggy via this refactoring, instead of causing problems. Introducing a new method, and accompanying it with the appropriate messaging around this case, should help.
This introduces a slight refactoring hazard from `Array.prototype.indexOf` to `Array.prototype.includes`: they will indeed behave differently for arrays containing `NaN`s. However, it seems much more likely that code will become _less_ buggy via this refactoring, instead of causing problems. Introducing a new method, and accompanying it with the appropriate messaging around this case, should help.

## Illustrative Examples

```js
assert([1, 2, 3].contains(2) === true);
assert([1, 2, 3].contains(4) === false);
assert([1, 2, 3].includes(2) === true);
assert([1, 2, 3].includes(4) === false);

assert([1, 2, NaN].contains(NaN) === true);
assert([1, 2, NaN].includes(NaN) === true);

assert([1, 2, -0].contains(+0) === true);
assert([1, 2, +0].contains(-0) === true);
assert([1, 2, -0].includes(+0) === true);
assert([1, 2, +0].includes(-0) === true);

assert(["a", "b", "c"].contains("a") === true);
assert(["a", "b", "c"].contains("a", 1) === false);
assert(["a", "b", "c"].includes("a") === true);
assert(["a", "b", "c"].includes("a", 1) === false);
```
14 changes: 6 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
{
"name": "array.prototype.contains",
"name": "array.prototype.includes",
"version": "1.0.0",
"description": "Tests and a polyfill for the ES proposal for Array.prototype.contains",
"description": "Tests and a polyfill for the ES proposal for Array.prototype.includes",
"main": "reference-implementation/index.js",
"scripts": {
"test": "test262-harness --prelude=reference-implementation/index.js --consoleCommand=\"node --harmony\" --runner=console test/*.js"
},
"repository": {
"type": "git",
"url": "https://github.com/domenic/Array.prototype.contains.git"
},
"repository": "domenic/Array.prototype.includes",
"keywords": [
"ecmascript",
"array",
"test262",
"contains"
"contains",
"includes"
],
"author": "Domenic Denicola <domenic@domenicdenicola.com> (http://domenic.me/)",
"author": "Domenic Denicola <d@domenic.me> (https://domenic.me/)",
"license": "BSD-2-Clause",
"devDependencies": {
"test262-harness": "bterlson/test262-harness"
Expand Down
2 changes: 1 addition & 1 deletion reference-implementation/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `Array.prototype.contains` Reference Implementation
# `Array.prototype.includes` Reference Implementation

The reference implementation is meant to be a line-by-line transcription of the specification from ECMASpeak into JavaScript, as much as is possible.

Expand Down
2 changes: 1 addition & 1 deletion reference-implementation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ var ToString = require("especially/abstract-operations").ToString;
var abs = require("especially/math").abs;
var define_built_in_data_property = require("especially/meta").define_built_in_data_property;

define_built_in_data_property(Array.prototype, "contains", function contains(searchElement) {
define_built_in_data_property(Array.prototype, "includes", function includes(searchElement) {
var fromIndex = arguments[1];

var O = ToObject(this);
Expand Down
8 changes: 4 additions & 4 deletions spec.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Array.prototype.contains ( searchElement [ , fromIndex ] )
# Array.prototype.includes ( searchElement [ , fromIndex ] )

NOTE `contains` compares _searchElement_ to the elements of the array, in ascending order, using the SameValueZero algorithm, and if found at any position, returns **true**; otherwise, **false** is returned.
NOTE `includes` compares _searchElement_ to the elements of the array, in ascending order, using the SameValueZero algorithm, and if found at any position, returns **true**; otherwise, **false** is returned.

The optional second argument _fromIndex_ defaults to 0 (i.e. the whole array is searched). If it is greater than or equal to the length of the array, **false** is returned, i.e. the array will not be searched. If it is negative, it is used as the offset from the end of the array to compute _fromIndex_. If the computed index is less than 0, the whole array will be searched.

When the `contains` method is called, the following steps are taken:
When the `includes` method is called, the following steps are taken:

1. Let _O_ be the result of calling ToObject passing the **this** value as the argument.
1. ReturnIfAbrupt(_O_).
Expand All @@ -26,4 +26,4 @@ When the `contains` method is called, the following steps are taken:
1. Increase _k_ by 1.
1. Return **false**.

The `length` property of the `contains` method is **1**.
The `length` property of the `includes` method is **1**.
11 changes: 0 additions & 11 deletions test/Array.prototype.contains_function-name.js

This file was deleted.

2 changes: 1 addition & 1 deletion test/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `Array.prototype.contains` Tests
# `Array.prototype.includes` Tests

These tests are written in test262 format. To run them against the reference implementation (which only works in Node 0.11), do

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// This code is governed by the BSD license found in the LICENSE file.

/*---
description: Array.prototype.contains sees a new element added by a getter that is hit during iteration
description: Array.prototype.includes sees a new element added by a getter that is hit during iteration
author: Domenic Denicola
---*/

Expand All @@ -15,7 +15,7 @@ var arrayLike = {
}
};

var result = Array.prototype.contains.call(arrayLike, 'c');
var result = Array.prototype.includes.call(arrayLike, 'c');

if (result !== true) {
$ERROR('Expected array-like to contain "c", which was added by the getter for the 1st element');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,30 @@
// This code is governed by the BSD license found in the LICENSE file.

/*---
description: Array.prototype.contains works on array-like objects
description: Array.prototype.includes works on array-like objects
author: Domenic Denicola
---*/

var arrayLike1 = { length: 5, 0: "a", 1: "b" };

var result1 = Array.prototype.contains.call(arrayLike1, "a");
var result1 = Array.prototype.includes.call(arrayLike1, "a");
if (result1 !== true) {
$ERROR('Expected array-like to contain "a"');
}

var result2 = Array.prototype.contains.call(arrayLike1, "c");
var result2 = Array.prototype.includes.call(arrayLike1, "c");
if (result2 !== false) {
$ERROR('Expected array-like not to contain "c"');
}

var arrayLike2 = { length: 2, 0: "a", 1: "b", 2: "c" };

var result3 = Array.prototype.contains.call(arrayLike2, "b");
var result3 = Array.prototype.includes.call(arrayLike2, "b");
if (result3 !== true) {
$ERROR('Expected array-like to contain "b"');
}

var result4 = Array.prototype.contains.call(arrayLike2, "c");
var result4 = Array.prototype.includes.call(arrayLike2, "c");
if (result4 !== false) {
$ERROR('Expected array-like to not contain "c"');
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// This code is governed by the BSD license found in the LICENSE file.

/*---
description: Array.prototype.contains should terminate if getting an index throws an exception
description: Array.prototype.includes should terminate if getting an index throws an exception
negative: Test262Error
includes: [Test262Error.js]
---*/
Expand All @@ -17,4 +17,4 @@ var trappedZero = {
}
};

Array.prototype.contains.call(trappedZero, 'a');
Array.prototype.includes.call(trappedZero, 'a');
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// This code is governed by the BSD license found in the LICENSE file.

/*---
description: Array.prototype.contains should terminate if ToNumber ends up being called on a symbol fromIndex
description: Array.prototype.includes should terminate if ToNumber ends up being called on a symbol fromIndex
negative: TypeError
---*/

Expand All @@ -13,4 +13,4 @@ var trappedZero = {
}
};

Array.prototype.contains.call(trappedZero, 'a', Symbol());
Array.prototype.includes.call(trappedZero, 'a', Symbol());
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// This code is governed by the BSD license found in the LICENSE file.

/*---
description: Array.prototype.contains should terminate if an exception occurs converting the fromIndex to a number
description: Array.prototype.includes should terminate if an exception occurs converting the fromIndex to a number
negative: Test262Error
includes: [Test262Error.js]
---*/
Expand All @@ -20,4 +20,4 @@ var trappedZero = {
}
};

Array.prototype.contains.call(trappedZero, 'a', fromIndex);
Array.prototype.includes.call(trappedZero, 'a', fromIndex);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// This code is governed by the BSD license found in the LICENSE file.

/*---
description: Array.prototype.contains should terminate if an exception occurs getting the length
description: Array.prototype.includes should terminate if an exception occurs getting the length
negative: Test262Error
includes: [Test262Error.js]
---*/
Expand All @@ -22,4 +22,4 @@ var throwingLength = {
}
};

Array.prototype.contains.call(throwingLength, 'a', fromIndexTrap);
Array.prototype.includes.call(throwingLength, 'a', fromIndexTrap);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// This code is governed by the BSD license found in the LICENSE file.

/*---
description: Array.prototype.contains should terminate if ToLength ends up being called on a symbol length
description: Array.prototype.includes should terminate if ToLength ends up being called on a symbol length
negative: TypeError
---*/

Expand All @@ -19,4 +19,4 @@ var badLength = {
}
};

Array.prototype.contains.call(badLength, 'a', fromIndexTrap);
Array.prototype.includes.call(badLength, 'a', fromIndexTrap);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// This code is governed by the BSD license found in the LICENSE file.

/*---
description: Array.prototype.contains should terminate if an exception occurs converting the length to a number
description: Array.prototype.includes should terminate if an exception occurs converting the length to a number
negative: Test262Error
includes: [Test262Error.js]
---*/
Expand All @@ -24,4 +24,4 @@ var badLength = {
}
};

Array.prototype.contains.call(badLength, 'a', fromIndexTrap);
Array.prototype.includes.call(badLength, 'a', fromIndexTrap);
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
// This code is governed by the BSD license found in the LICENSE file.

/*---
description: Array.prototype.contains should search the whole array, as
description: Array.prototype.includes should search the whole array, as
the optional second argument fromIndex defaults to 0
author: Robert Kowalski
---*/

if ([10, 11].contains(10) !== true) {
if ([10, 11].includes(10) !== true) {
$ERROR('Expected that the whole array was searched');
}

if ([10, 11].contains(11) !== true) {
if ([10, 11].includes(11) !== true) {
$ERROR('Expected that the whole array was searched');
}

Expand All @@ -25,10 +25,10 @@ var arrayLike = {
}
};

if (Array.prototype.contains.call(arrayLike, '1') !== true) {
if (Array.prototype.includes.call(arrayLike, '1') !== true) {
$ERROR('Expected that the whole array-like was searched');
}

if (Array.prototype.contains.call(arrayLike, '2') !== true) {
if (Array.prototype.includes.call(arrayLike, '2') !== true) {
$ERROR('Expected that the whole array-like was searched');
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
// This code is governed by the BSD license found in the LICENSE file.

/*---
description: Array.prototype.contains returns false if fromIndex
description: Array.prototype.includes returns false if fromIndex
is greater or equal to the length of the array
author: Robert Kowalski
---*/

if ([1, 2].contains(2, 3) !== false) {
if ([1, 2].includes(2, 3) !== false) {
$ERROR('Expected that the array was not searched');
}

if ([1, 2].contains(2, 2) !== false) {
if ([1, 2].includes(2, 2) !== false) {
$ERROR('Expected that the array was not searched');
}

Expand All @@ -25,10 +25,10 @@ var arrayLikeWithTrap = {
}
};

if (Array.prototype.contains.call(arrayLikeWithTrap, 'c', 2) !== false) {
if (Array.prototype.includes.call(arrayLikeWithTrap, 'c', 2) !== false) {
$ERROR('Expected that the array was not searched');
}

if (Array.prototype.contains.call(arrayLikeWithTrap, 'c', 3) !== false) {
if (Array.prototype.includes.call(arrayLikeWithTrap, 'c', 3) !== false) {
$ERROR('Expected that the array was not searched');
}
Loading

0 comments on commit 4b6b953

Please sign in to comment.