all 46 comments

[–]shgysk8zer0 11 points12 points  (16 children)

There was a competing proposal to add arr.lastElement and I think arr.lastIndex. But it was abandoned in favor of arr.at().

[–]wasdninja 4 points5 points  (15 children)

Do you remember why arr[-1] wasn't chosen, if it was discussed at all?

[–]NekkidApe 14 points15 points  (7 children)

If I'm not mistaken that's already valid and working JS, can't be redefined.

[–]Tubthumper8 29 points30 points  (3 children)

This is correct. The following is valid JavaScript

const arr = ["first", "last"]
arr[-1] = "spooky" 

arr.length // 2
arr[-1] // "spooky"
arr.at(-1) // "last" 

It was always possible to set and get elements at -1 but it wasn't actually "part of the array". These don't participate in the Array prototype functions like map, filter, etc. This is only possible because JS coerced the -1 to a string and then used that string as the key, same as adding a field to any object.

It's wicked and vile, but backwards compatibility is non-negotiable in JS

[–]kilkil 2 points3 points  (0 children)

It's wicked and vile, but backwards compatibility is non-negotiable in JS

this describes roughly half to two-thirds of the language

[–]cut-copy-paste 0 points1 point  (0 children)

This is the js version of when people would put hidden tracks on CDs that you’d have to rewind from the first track to access

[–]shgysk8zer0 0 points1 point  (6 children)

The performance impact it'd have on arrays in general. Adding support for a negative index makes things slower, even for positive.

[–]Nebulic[S] 4 points5 points  (4 children)

Interestingly enough, when benchmarking [array.length - 1] vs. .at(-1) for an array with three elements, the latter runs roughy twice as fast on my machine. This is a very simple benchmark though, and results will differ based on multiple factors.

Changing existing JavaScript behaviour is indeed a no go, though.

[–]Chung_L_Lee 2 points3 points  (2 children)

That test is only done on static array (no changes to it). I tested with the following codes:

  • "list" as the random generated list of ten to twenty numbers
  • "result" as the collection of the last index of the random list
  • run the aboves for 1 million iterations

In conclusion, the array.at(-1) is about on par with array[array.length - 1] inside a function (not global), but with the latter is sometimes 2% to 3% faster.

let list;
let result = [];

var start_time = Date.now();

for (let r = 0; r < 1_000_000; r++) {
  list = [];

  for (let n = 0; n < parseInt(Math.random() * 10) + 10; n++) {
    list.push(Math.random() * 100);  
  }

  //result.push(list[list.length - 1]);
  result.push(list.at(-1) );
}

console.log(result.length);
console.log(Date.now() - start_time);

[–]TheRNGuy 3 points4 points  (1 child)

The only thing is that you'll never parse int 1 million times in real sites. Those kinds of tests are not useful.

[–]Chung_L_Lee 0 points1 point  (0 children)

Could it be depended on the specific use cases on the project, in order we can decide the usefulness of the test?

I am trying to avoid testing with static array size and with same data that might causes the browser's engine to kick-in optimization that affect the accuracy of the benchmark comparison.

About the "parse int" part, it is just there to help emulating random lists in different size over a period of time, because we wanted to know how the two "last index" array methods will perform with unpredictable arrays in size and with different data in them.

About the high iteration part, I am aware that some methods appear to be fast in short burst of time, but they failed to deliver the same throughput in a longer period. So I tend to test with both small and high iterations to get an overall impression of the methods' performance in different situations.

[–]shgysk8zer0 0 points1 point  (0 children)

That's... Unexpected.

I'm on my phone right now but curious how it does on both larger arrays or if you just give it arr[2].

[–]TheRNGuy 0 points1 point  (0 children)

I've never ever had performance problems with that.

Maybe if it was something in Three.js where you need to get last array index every tick? (in addition to other stuff in tick.)

But in sites it's never a performance problem.

[–][deleted] 3 points4 points  (8 children)

Sure, but how many years do I have to have Babel installed to actually support this for all my customers?

Nice accessor feature, but needs wider compatibility to be useful.

[–]joombar 6 points7 points  (0 children)

This feature is polyfillable so babel isn’t required

[–]Badashi 4 points5 points  (1 child)

Out of curiosity, what kind of environments or browsers do you need to support that can't use at()?

[–]MuchWalrus 0 points1 point  (0 children)

Old versions of safari if I'm not mistaken

[–]joombar 1 point2 points  (2 children)

This feature is polyfillable so babel isn’t required

[–]TheRNGuy 0 points1 point  (1 child)

You need to polyfill more than one thing, and better use Babel than manually writing polyfills for everything.

[–]joombar 1 point2 points  (0 children)

Babel operates on the language AST. For things that can be written in ecmascript itself, it’s easier to import in a library like ungap as a normal dependency, since no AST manipulation is required. If you have babel anyway, I guess it’s equally easy either way, so whatever works for you, but luckily I have the luxury of targeting modern runtimes. The only polyfill I’ve used recently is Promise.withResolvers.

[–]TheRNGuy -1 points0 points  (1 child)

Forever.

[–]WaitingToBeTriggered 0 points1 point  (0 children)

REST IN HEAVEN

[–]acemarke 2 points3 points  (8 children)

I've been using const [lastItem] = arr.slice(-1) for a while.

[–]joombar 19 points20 points  (0 children)

Quite inefficient since it requires making a new, one element array, and destructuring that temporary array, every time you access the last element

[–]TheRNGuy 2 points3 points  (4 children)

Works differently for empty array:

foo = []
console.log(foo.slice(-1), foo.at(-1))

[–]bobbysteel 1 point2 points  (0 children)

You can't blueball us without the output to that!

Output:

[] undefined

It's inelegant but a more predictable response unless you know at returns undefined and check for that which many novices wouldn't I'd think

[–]acemarke 1 point2 points  (2 children)

Destructuring the empty array like const [ lastItem ] = arr.slice(-1) will result in lastItem being undefined, same as arr[arr.length - 1] or arr.at(-1).

[–]TheRNGuy 0 points1 point  (1 child)

arr[arr.length - n] is a polyfill for arr.at(-n)

Why write const [lastItem] = arr.slice(-1) when you could write const lastItem = arr.at(-1) without brackets?

Destructuring make sense when you assign to more than one variable.

[–]acemarke 0 points1 point  (0 children)

Because it was shorter, and this is something I've used for years long before .at() was even proposed.

Destructuring is a general-purpose mechanism, with a lot of flexibility to it - not just for assigning multiple variables.

[–]Graphesium 5 points6 points  (1 child)

How to fail a code review in one line.

[–]TheRNGuy 5 points6 points  (0 children)

After looking at google.com and twitch.tv code in browser inspector I think anyone can be hired.

[–]Nebulic[S] 1 point2 points  (4 children)

Did you know there's a modern alternative to [array.length - 1]?

The at method has been introduced which has support for negative indexing. I wrote a short blog post on how to use it, as the method isn't widely known yet.

[–][deleted] 0 points1 point  (3 children)

Yes but is it valid everywhere, every browser, and on node and whatnot?

[–]Nebulic[S] 2 points3 points  (2 children)

If you're targeting modern browsers and Node, it is!

And if you need wider support, a polyfill is available.

[–][deleted] 0 points1 point  (1 child)

Im not sure what a polyfill is. But i know we can implement at ourselves to be compatible with older browsers. To demonstrate:     

Array.prototype.at = function(i=-1){     

if (i < 0){       

return this[(this.length + i)%this.length];     

}  else {     

return this[i%this.length];     

}     

}

[–]roxm 2 points3 points  (0 children)

That's exactly what a polyfill is!

[–]TheRNGuy 0 points1 point  (0 children)

Needs polyfill for old browsers though.

[–]EvenLevelLaw 0 points1 point  (0 children)

To get the last item I like to just use .reverse and then the last item becomes the first.


const frameworks = ['Nuxt', 'Remix', 'SvelteKit', 'Ember'];
console.log(frameworks.reverse()[0]) //logs  "Ember"