Skip to content

Stage 2 preparation #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 26 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Status

Champion: Jordan Harband
Champion: Ruben Bridgewater, Jordan Harband

Author: Ruben Bridgewater <[email protected]>

Expand Down Expand Up @@ -155,8 +155,8 @@ Object.propertyCount(target[, options])
- Throws `TypeError` if target is not an object.
- **`options`** *(optional)*: An object specifying filtering criteria:
- `keyTypes`: Array specifying property types to include:
- Possible values: `'index'`, `'nonIndexString'`, `'symbol'`.
- Defaults to `['index', 'nonIndexString']` (aligning closely with `Object.keys`).
- Possible values: `'string'`, `'symbol'`, and `'all'`.
- Defaults to `'string'` (aligning closely with `Object.keys`).
- Throws `TypeError` if provided invalid values.
- `enumerable`: Indicates property enumerability:
- `true` to count only enumerable properties (default).
Expand All @@ -167,7 +167,6 @@ Object.propertyCount(target[, options])
Defaults align closely with `Object.keys` for ease of adoption, ensuring intuitive behavior without needing explicit configuration in common cases.

The naming of keyTypes and if it's an array or an object or the like is open for discussion.
Important is just, that it's possible to differentiate index from non index strings somehow, as well as symbol properties.

Similar applies to the enumerable option: true, false, and `'all'` seems cleanest, but it's not important how they are named.

Expand Down Expand Up @@ -198,25 +197,25 @@ Object.propertyCount(obj2); // returns 1
See https://tc39.es/ecma262/#array-index

```js
let obj = { '01': 'string key', 1: index, 2: 'index' };
Object.propertyCount(obj, { keyTypes: ['index'] }); // returns 2
let obj = { "01": "string key", 1: "index", 2: "index" };
Object.propertyCount(obj, { keyTypes: ['string'] }); // returns 3

obj = { '0': 'index', '-1': 'string key', '01': 'string key' };
Object.propertyCount(obj, { keyTypes: ['index'] }); // returns 1 (only '0')
obj = { "0": "index", "-1": "string key", "01": "string key" };
Object.propertyCount(obj, { keyTypes: ['string'] }); // returns 3
```

- **String based keys**:

```js
const obj = { '01': 'string key', 1: 'index', 2: 'index' };
Object.propertyCount(obj, { keyTypes: ['nonIndexString'] }); // returns 1
const obj = { "01": "string key", 1: "index", 2: "index" };
Object.propertyCount(obj, { keyTypes: ['string'] }); // returns 3
```

- **Symbol based keys**:

```js
const obj = { [Symbol()]: 'symbol', 1: 'index', 2: 'index' };
Object.propertyCount(obj, { keyTypes: ['symbol'] }); // returns 1
const obj = { [Symbol()]: "symbol", 1: "index", 2: "index" };
Object.propertyCount(obj, { keyTypes: ['symbol'] }); // returns 2
```

## Explicit Semantics
Expand All @@ -233,7 +232,7 @@ The native implementation should strictly avoid creating intermediate arrays or
2. Iterate directly over the object's own property descriptors
- Access the internal property keys directly via the object's internal slots.
- For each own property:
- Determine if the key is a numeric index, a regular non-index string, or a symbol.
- Determine if the key is a string or a symbol.
- Check if the property type matches any specified in `keyTypes`.
- If `enumerable` is not `'all'`, match the property's enumerability against the provided boolean value.
- If the property meets all criteria, increment the counter.
Expand All @@ -244,38 +243,40 @@ See the [spec proposal](./spec.emu) for details.
## Alternatives Considered

- **Multiple separate methods**: Rejected due to increased cognitive load and API complexity.
- **Using booleans for key types**: The default would have diverging boolean defaults for different key types (in other words, 4 possible combinations for only 3 possible states). Thus, an enum is considered a better approach.

## TC39 Stages and Champion

- Ready for **Stage 1** (proposal)
- Ready for **Stage 2**

## Use Cases

- Improved readability and explicit intent
- Significant performance gains
- Reduced memory overhead
- Simpler code
- Significant **performance** gains
- **Reduced memory** overhead
- **Simpler code**

## Precedent

Frequent patterns in widely-used JavaScript runtimes, frameworks, and libraries (Node.js, React, Angular, Lodash) demonstrate the common need for an optimized property counting mechanism.

The regular expression exec/match/matchAll methods produce a "match object" that is an Array, with non-index string properties on it (lastIndex, groups, etc).

## Polyfill

```js
// NOTE: do not use this polyfill in a production environment
const validTypes = new Set(['index', 'nonIndexString', 'symbol']);
const validTypes = new Set(['string', 'symbol']);

Object.propertyCount = function (target, options) {
Object.propertyCount = function propertyCount(target, options) {
if (typeof target !== 'object' || target === null) {
throw new TypeError(`Expected target to be an object. Received ${typeof target}`);
}

if (options === undefined) {
if (typeof options === 'undefined') {
return Object.keys(target).length;
}

const { keyTypes = ['index', 'nonIndexString'], enumerable = true } = options || {};
const { keyTypes = ['string'], enumerable = true } = options || {};

for (const type of keyTypes) {
if (!validTypes.has(type)) {
Expand All @@ -289,16 +290,8 @@ Object.propertyCount = function (target, options) {

let props = [];

if (keyTypes.includes('index') || keyTypes.includes('nonIndexString')) {
let stringProps = enumerable === true ? Object.keys(target) : Object.getOwnPropertyNames(target);

if (!keyTypes.includes('nonIndexString')) {
stringProps = stringProps.filter(key => String(parseInt(key, 10)) === key && parseInt(key, 10) >= 0);
} else if (!keyTypes.includes('index')) {
stringProps = stringProps.filter(key => String(parseInt(key, 10)) !== key || parseInt(key, 10) < 0);
}

props = stringProps;
if (keyTypes.includes('string')) {
props = enumerable === true ? Object.keys(target) : Object.getOwnPropertyNames(target);
}

if (keyTypes.includes('symbol')) {
Expand All @@ -319,6 +312,7 @@ Object.propertyCount = function (target, options) {
- **Performance**: Native implementation will significantly outperform existing approaches by eliminating intermediate arrays.
- **Flexibility**: Enumerable properties counted by default; easy inclusion/exclusion.
- **Simplicity**: Improved code readability and clarity.
- **Future proofing**: The second argument is an options object and this potentially allows future additions (e.g., to include inherited properties, or only writable properties, etc).

## Conclusion

Expand Down
66 changes: 28 additions & 38 deletions spec.emu
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,37 @@
<script src="./spec.js"></script>
<pre class="metadata">
title: Object.propertyCount
stage: 0
stage: 1
contributors: Ruben Bridgewater, Jordan Harband
</pre>

<emu-clause id="sec-fundamental-objects" number="20">
<h1>Fundamental Objects</h1>

<emu-clause id="sec-object-objects">
<h1>Object Objects</h1>

<emu-clause id="sec-properties-of-the-object-constructor" number="2">
<h1>Properties of the Object Constructor</h1>

<emu-clause id="sec-object.propertycount" number="20">
<h1>Object.propertyCount ( _target_ [ , _options_ ] )</h1>
<p>When the `Object.propertyCount` method is called, the following steps are taken:</p>
<emu-alg>
1. If _target_ is not an Object, throw a TypeError exception.
1. Let _resolvedOptions_ be ? GetOptionsObject(_options_).
1. Let _keyTypes_ be CreateArrayFromList(« *"index"*, *"nonIndexString"* »).
1. Let _keyTypesOption_ be ? Get(_resolvedOptions_, *"keyTypes"*).
1. If _keyTypesOption_ is not *undefined*, then
1. If _keyTypesOption_ is not an Object, throw a TypeError exception.
1. Set _keyTypes_ to ? CreateListFromArrayLike(_keyTypesOption_).
1. If _keyTypes_ contains any value other than *"index"*, *"nonIndexString"*, or *"symbol"*, or if any of those values are repeated, throw a TypeError exception.
1. Let _enumerable_ be ? Get(_resolvedOptions_, *"enumerable"*).
1. If _enumerable_ is *undefined*, set _enumerable_ to *true*.
1. If _enumerable_ is not one of *true*, *false*, or *"all"*, throw a TypeError exception.
1. Let _count_ be 0.
1. Let _ownKeys_ be _target_.[[OwnPropertyKeys]]().
1. For each element _key_ of _ownKeys_, do
1. Let _desc_ be ? _target_.[[GetOwnProperty]](_key_).
1. If _desc_ is not *undefined*, and either _enumerable_ is *"all"* or _enumerable_ is _desc_.[[Enumerable]], then
1. If _key_ is a Symbol and _keyTypes_ contains *"symbol"*, increment _count_ by 1.
1. Else if _key_ is an array index and _keyTypes_ contains *"index"*, increment _count_ by 1.
1. Else if _keyTypes_ contains *"nonIndexString"*, increment _count_ by 1.
1. Return _count_.
</emu-alg>
</emu-clause>
</emu-clause>
</emu-clause>
<emu-clause id="sec-demo-clause">
<h1>Object.propertyCount ( _target_ [ , _options_ ] )</h1>
<p>When the `Object.propertyCount` method is called, the following steps are taken:</p>
<emu-alg>
1. If _target_ is not an Object, throw a TypeError exception.
1. Let _resolvedOptions_ be ? GetOptionsObject(_options_).
1. Let _keyTypes_ be ? Get(_resolvedOptions_, *"keyTypes"*).
1. If _keyTypes_ is *undefined*, then
1. Set _keyTypes_ to CreateArrayFromList(« *"string"* »).
1. Else, perform the following,
1. If _keyTypes_ is not an Object, throw a TypeError exception.
1. Set _keyTypes_ to ? CreateListFromArrayLike(_keyTypes_, ~all~).
1. If _keyTypes_ contains any value other than *"string"*, or *"symbol"*, throw a TypeError exception.
1. Let _enumerable_ be ? Get(_resolvedOptions_, *"enumerable"*).
1. If _enumerable_ is *undefined*, set _enumerable_ to *true*.
1. Else if _enumerable_ is not one of *true*, *false*, or *"all"*, throw a TypeError exception.
1. Let _count_ be 0.
1. Let _ownKeys_ be _target_.[[OwnPropertyKeys]]().
1. For each element _key_ of _ownKeys_, perform the following steps, do
1. Let _desc_ be _target_.[[GetOwnProperty]](_key_).
1. If _desc_ is not *undefined*, then
1. If _enumerable_ is not *"all"*, then
1. If _enumerable_ is not equal to _desc_.[[Enumerable]], continue to the next _key_.
1. If _key_ is a Symbol and _keyTypes_ contains *"symbol"*, increment _count_ by 1.
1. If _key_ is a String and _keyTypes_ contains *"string"*, increment _count_ by 1.
1. Return 𝔽(_count_).
</emu-alg>
</emu-clause>

<!-- Copied from ECMA-402 GetOptionsObject -->
Expand Down