0

If I have an array

let ar = ['a', 'b', 'c'];
ar.prop1 = 'Hello';
ar.prop2 = 'world';

How do I iterate over just the properties (prop1 and prop2) in this case?

ee2Dev
  • 1,938
  • 20
  • 29
  • 2
    Why do you use an array for this purpose? Use an object instead. Arrays are used as indexed collections, objects as keyed collections. You’re trying to use an array for both purposes at the same time, but want to enumerate only the keyed part. That’s just asking for bugs. – Sebastian Simon Mar 24 '21 at 23:56
  • Does this answer your question? [How to loop through an array containing objects and access their properties](https://stackoverflow.com/questions/16626735/how-to-loop-through-an-array-containing-objects-and-access-their-properties) – MVB76 Mar 24 '21 at 23:57
  • @MVB76 No, the array in question does not contain objects. – Sebastian Simon Mar 24 '21 at 23:57
  • @SebastianSimon I don’t have a choice. I use this data structure since it is returned by a library function I want to use. But for further processing, I don’t need the array elements but just the properties – ee2Dev Mar 25 '21 at 00:04
  • 2
    There is not a simple way to separate out the array entries from the property names. This is a bad/wrong use of an array. I you want to store separate properties, then create a parent object, put the properties on that parent object and put the array into a property of the parent object. Then, you have them separate. – jfriend00 Mar 25 '21 at 00:07
  • 1
    The only hack I know of is to use `Object.getOwnProperties()` and then filter out the numeric property names. You can see why code shouldn't be written this way because it takes a hack to separate them. – jfriend00 Mar 25 '21 at 00:08
  • @jfriend00 I thought of such a hack like iterating over properties (which also contain the elements) and then filtering by if !isNaN(prop) ... but that seems such a bad hack with the risk of side effects that’s why I posted this – ee2Dev Mar 25 '21 at 00:12
  • @ee2Dev But positive integers from 0 to 2⁵³ − 1 (inclusive; as number type, coerced to string) are guaranteed to be array indexes. If you have a key like `"1a"`, `"-1"`, or `"9007199254740992"`, it’s definitely not an array index. (That’s why a `Number.isSafeInteger` check should also be included). – Sebastian Simon Mar 25 '21 at 00:17

2 Answers2

3

I'm not aware of any way to directly iterate just the plain properties. But, you can construct a list of just the plain properties as follows:

ar.keys() gives you only the array element indexes (not plain properties)

Object.keys(ar) gives you all properties and array indexes.

So, you can start with all the properties and indexes and then filter out the ones that are array element indexes and be left with just the properties.

let ar = ['a', 'b', 'c'];
ar.prop1 = 'Hello';
ar.prop2 = 'world';

let arrayIndexes = new Set(Array.from(ar.keys(), i => "" + i));
console.log("arrayIndexes (converted to string)", Array.from(arrayIndexes));

let props = Object.keys(ar).filter(k => !arrayIndexes.has(k));
console.log("props", props);

Or, the same concept with fewer temporary objects:

let ar = ['a', 'b', 'c'];
ar.prop1 = 'Hello';
ar.prop2 = 'world';

let arrayIndexes = new Set();
for (let i of ar.keys()) {
    arrayIndexes.add("" + i);
}
console.log("arrayIndexes (converted to string)", Array.from(arrayIndexes));

let props = Object.keys(ar).filter(k => !arrayIndexes.has(k));
console.log("props", props);

If you want all own properties, including non-enumerable properties, you could do this:

let ar = ['a', 'b', 'c'];
ar.prop1 = 'Hello';
ar.prop2 = 'world';

let arrayIndexes = new Set();
for (let i of ar.keys()) {
    arrayIndexes.add("" + i);
}
console.log("arrayIndexes (converted to string)", Array.from(arrayIndexes));

let props2 = Object.keys(Object.getOwnPropertyDescriptors(ar)).filter(k => !arrayIndexes.has(k));
console.log("props2", props2);

Examining Characters in Property Name

Note: I attempted a solution that iterates all the properties (including array indexes) and filters out things that will be interpreted as array indexes by examining the characters in the property name. This proved to be troublesome. It's not just as simple as whether it's a numeric value or not or converts to a number. To be considered an array index, here's what I've discovered so far:

  1. It must contain only digits 0-9 (no plus or minus sign)
  2. It must pass Number.isSafeInteger() because it can be a number, but too large to be an integer without some loss of precision. When converted to a number, it must also be less than ar.length.
  3. The property name must not start with a "0" unless it's only "0". For example x["01"] will not be treated as an array index equivalent to x[1].

Here are some numeric-like strings that are not interpreted as array indexes:

"01"
"00"
"9999999999999999999999999999999"
"-1"
"-0"
"1.0"

Here's is such a test, though I don't find it simpler than the above tests:

let ar = ['a', 'b', 'c'];
ar.prop1 = 'Hello';
ar.prop2 = 'world';
ar["-1"] = 'minus one';
ar["0"] = 'a';
ar["-0"] = 'z';
ar["01"] = 'zzzz';
ar["1.0"] = 'aaa';
ar["999"] = 'Z';
ar["99999999999999999999999999999999"] = 'ZZ';
ar["+0"] = 'bbb';

let props = Object.keys(ar).filter(k => {
    // a property that isn't "0", but starts with "0" will not be an array index
    // for example, "01" is not an array index
    if (k.length > 1 && k.startsWith("0")) {
        return true;
    }
    // if not entirely made up of digits 0-9, keep it
    if (!/^\d+$/.test(k)) {
        return true;
    }
    // convert to number
    let index = +k;
    // if it's not a safe integer or it's longer than the length,
    // then it must not be an array index
    if (!Number.isSafeInteger(index) || index >= ar.length) {
        return true;
    }
    // if it passed all these tests, then it must be an array index so filter it out
    return false;

});
console.log("props", props);
jfriend00
  • 683,504
  • 96
  • 985
  • 979
2

You can extract the keys from the array portion and identify the non-configurable properties.

let ar = ['a', 'b', 'c'];
ar.prop1 = 'Hello';
ar.prop2 = 'world';

const keys = [...ar.keys()].map(k => `${k}`);
console.log(
  Object.getOwnPropertyNames(ar).filter(n => {
    const desc = Object.getOwnPropertyDescriptor(ar, n);
    return desc.enumerable && desc.configurable && !keys.includes(n);
  }),
);
Chase
  • 3,028
  • 14
  • 15
  • Clever! This is guaranteed to work because `Array.prototype.keys` creates an iterator based only on numeric integer indexes from 0 to _length_ − 1. – Sebastian Simon Mar 25 '21 at 00:29