-2

I need to get the last key of an object which has a value. So if this would be the object...

const obj = { first: 'value', second: 'something', third: undefined, fourth: undefined }

... I need to get second

obj = { first: 'value', second: 'something', third: 'else', fourth: undefined }
// result should be: 'third'
obj = { any: 'value', other: 'something', thing: undefined, bla: undefined }
// result should be: 'other'
user3142695
  • 15,844
  • 47
  • 176
  • 332
  • 1
    How do you define "last", those are the fields of the object. – buræquete Feb 21 '17 at 00:19
  • @bureaquete last field in this order... – user3142695 Feb 21 '17 at 00:19
  • 1
    @user3142695 do you mean your question is only for this specific object? – buræquete Feb 21 '17 at 00:20
  • 5
    "last" has no meaning for object properties. the order isn't relevant – Stephen Thomas Feb 21 '17 at 00:21
  • @bureaquete added some examples... – user3142695 Feb 21 '17 at 00:21
  • Do you know anything about the objects in question such as the keys **before** you attempt to find the value you're after? I ask as you'll need to define the order of key iteration. For example, is it possible to have something like `var inspectInThisOrder = ['first', 'second', third', 'fourth']`? – Phil Feb 21 '17 at 00:34
  • 1
    I'm voting to close this question as off-topic because without further information, it is impossible to solve. – Phil Feb 21 '17 at 00:46
  • 1
    The properties of an object don't have an intrinsic order, so your question doesn't actually make sense. There is no "last" key. Browsers typically list properties in alphabetical order, e.g. in the developer console, but that's just as a convenience to the developer. Arrays are structures with intrinsic order. – Stephen Thomas Feb 21 '17 at 00:25
  • @Kinduser There is no "last" property. Object properties are unordered, did you read the answer? – Andrew Li Feb 21 '17 at 00:28
  • well, that's because there is no solution. – Stephen Thomas Feb 21 '17 at 00:32
  • @StephenThomas: [Object properties have order now](http://stackoverflow.com/questions/30076219/does-es6-introduce-a-well-defined-order-of-enumeration-for-object-properties). But using it is usually not useful. – T.J. Crowder Feb 21 '17 at 08:14

3 Answers3

5

Object properties do have a certain order differing from insertion order that isn't really useful in this case as described by T.J. Crowder. This Stack Overflow question is a good read on the subject, and I recommend reading further than the first answer.

So instead of relying on objects which have no guarantee to order1, use something that does guarantee order. You can use something like a Map, which is similar to an object with key/value pairs, but the pairs are ordered. For example:

const obj = new Map([
    ["first", "foo"],
    ["second", "bar"],
    ["third", undefined],
    ["fourth", undefined]
]);

const result = Array.from(obj.keys()).reduce((lastKey, currKey) => obj.get(currKey) !== undefined ? currKey : lastKey);

console.log(result);

The Map constructor takes in an iterable to create a Map out of. The array of arrays construct a Map with key and value pairs of the subarrays. Thus, the following Map is created and stored into obj:

+----------+-----------+
| Key      | Value     |
+----------+-----------+
| "first"  | "foo"     |
| "second" | "bar"     |
| "third"  | undefined |
| "fourth" | undefined |
+----------+-----------+

Then, the line Array.from(obj.keys()) creates an array from the keys of the Map which are first, second, third, and fourth. It then uses Array.prototype.reduce to deduce the last key which has a defined value.

The reduce callback uses lastKey which is the accumulator/last key with the defined value, and currKey which is the current key being processed. It then checks if obj.get(currKey) is not undefined. If it is not undefined, then it is returned and assigned to the accumulator. This goes through the entire array and the final value (accumulator) is returned to result. The result is the last key that had a defined value.2


1It should be noted that in ES2015, there are a selection of methods that do actually guarantee returning the keys in the insertion order. These include Object.assign, Object.defineProperties, Object.getOwnPropertyNames, Object.getOwnPropertySymbols, and Reflect.ownKeys. You can rely on these, instead of using Map.

2There are many other ways to get the last key. You could filter the array and slice it, like Reuven Chacha did. I think reducing it is more descriptive but some other approaches are more straightforward in operation.

Community
  • 1
  • 1
Andrew Li
  • 55,805
  • 14
  • 125
  • 143
  • `they're just an unordered collection of properties`. And actually **I've ordered them** by introducing `i` counting variable. I wish someone like T.J. Crowder said something about this case. – kind user Feb 21 '17 at 00:38
  • 1
    @Kinduser: If both order and key-lookup are required, a `Map` as shown by Andrew is exactly the right way to do this. Doing it with an object is just wrong, because object property order isn't simply insertion order: Properties whose names are integer indexes as defined by the specification come first. (And when you get into inheritance, it gets more complicated.) – T.J. Crowder Feb 21 '17 at 08:09
3

If you need to get the "last property with a value," you're using the wrong data structure. An object is just not a good fit. You want to use an array or a Map. I'd probably use a Map as shown by Andrew Li, since it defines a useful order and also key-based lookup.

If you insist on using an object: Object properties do have an order as of ES2015 (aka "ES6"). That order is only enforced by certain operations; it isn't enforced by for-in or Object.keys. It is enforced by most other operations, including Object.getOwnPropertyNames (ES2015), the upcoming Object.values in the ES2017 specification, and (interestingly) JSON.stringify.

However, the order is unlikely to be very useful to you. Assuming we're only talking about "own" properties whose names are not Symbols, the order is: Any property that's an "integer index" as defined by the specification1, in numeric order, followed by any other properties, in the order they were added to the object.

You see why an object is the wrong structure for this: Integer indexes get priority over other properties, so given:

const obj = { first: 'value', second: 'something', third: undefined, 42: 'value' };

...using the object's order, we'll say that the last value is 'something' when it should be 'value'. Using a Map, you'd get the right answer.

So, I don't recommend using an object for this, but if you insist on doing so, here's how you can do it on a fully-compliant ES2015+ JavaScript engine:

function test(testNumber, obj) {
  const last = Object.getOwnPropertyNames(obj).reduce((currentValue, key) => {
    const value = obj[key];
    return typeof value === "undefined" ? currentValue : value;
  }, undefined);
  console.log(`Test #${testNumber}: '${last}'`);
}
// This says 'something' (value of `second`):
test(1, { first: 'value', second: 'something', third: undefined, fourth: undefined });

// This says 'else' (value of `third`):
test(2, { first: 'value', second: 'something', third: 'else', fourth: undefined });

// This says 'something' (value of `other`):
test(3, { any: 'value', other: 'something', thing: undefined, bla: undefined });

// BUT! This says 'something' (value of `second`), not 'value' (value of `42`)!
test(4, { first: 'value', second: 'something', third: undefined, 42: 'value' });
.as-console-wrapper {
  max-height: 100% !important;
}

Note that last result.

Community
  • 1
  • 1
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
1

Very tricky, iteration through object does not necessarily guarantees order. ES2015 provides several methods that are iterating through symbol\string type keys by following the creation order.

Assuming last key which has a value is the last entry that was defined in the object with truthy value:

function getLastKeyWithTruthyValue(obj) {
    Object.getOwnPropertyNames(obj).filter(key => !!obj[key]).slice(-1)[0]
}
  1. Get the array of the property names in the creation order (Object.getOwnPropertyNames is supported in the most of the modern browsers, see browser compatibility)
  2. Filter out the falsy values (using filter(Boolean) can be a cool method to filter, as described in this cool answer. If any value which is not undefined is required, use val => val !== undefined as the filter callback.
  3. Get the last item in the filtered array (will return undefined in case that all values are falsy).
Community
  • 1
  • 1
Reuven Chacha
  • 879
  • 8
  • 20
  • @ user3142695: Reuven's `.filter(key => !!obj[key])` will filter out **all** falsy values (including `0`, `""`, etc.), not just `undefined`, so if that's not what you want, you'll have to adjust that. The above also creates two unnecessary temporary arrays (but it's unlikely to matter). – T.J. Crowder Feb 21 '17 at 08:59
  • It was mentioned that he looks for a key that has a value. @user3142695 didn't mention that he looks for a value that is not undefined, Anyway, I agree it might be the use case according to the example. Thanks for the comment, edited the answer. – Reuven Chacha Feb 21 '17 at 09:16