Ok, but they’re already shipped and in the spec - they will never be removed. Your opinion is noted but doesn’t change that fact.
I'm agree with you, we don't need to delete them. We can deprecate them. No one developer will use those methods if are deprecated.
We should try to solve this fast...
The names are common in other languages.
Kotlin has
list.sort()
(mutating) docs
list.sorted()
(non-mutating) docs
Java has
Arrays.sort(array)
(mutating) docs
stream.sorted()
(non-mutating) docs
Python has
array.sort()
(mutating) docs
sorted(array)
(non-mutating) docs
Swift has
collection.sort()
(mutating) docs
collection.sorted()
(non-mutating) docs
So many languages has the .first()
, .last()
, .isEmpty()
and the usefull .clear()
for their arrays, but JS does not. It does not means anything
Js is a universal language used in all browsers and all devices, we can't just copy and paste other bad languages designs (personal perspective, I did argument on this post)
We need to think about what is better to write and understand, avoid stun developers with a lot of redundant methods and give them exactly what they need, this is related with the scalability of all JS projects
We don't deprecate things in the language, and your faith that developers will follow guidance is endearing, but empirically largely false.
There's a long-standing convention that .toFoobar()
methods returns some representation of the object without modifying it. Such as .toString()
, .toUpperCase()
, .toURL()
, etc.
Could you link to that? I thought lookup was amortized O(1). And monomorphic inline caches reduce that even further to a simple (and easily predicted) conditional jump.
There might be at most about 100 or so externally visible function object types in the entire spec today, with the largest objects (Date
, Array
, and %TypedArray% prototypes) having less than 20 properties in total. Assuming the growth factor is chosen correctly (normally 0.8), you won't run into the non-linear performance problems in hash tables with any remotely modern form of open addressing until you have thousands of entries on a single object.
So I'm highly skeptical of that.
IIRC: it was when change-array-by-copy tried to add >10 methods all at once. I was surprised at first too. But now I can't remember what I saw it (either an issue, meeting notes, or in another repo when they brought this up as an argument for "no more array methods")
The no-more-array-methods leaning is mostly driven by how often these end up breaking existing websites. It was something like 1 in 3 of most recently added array methods caused websites issues. Array.prototype.group
was moved to {Map,Object}.groupBy
for this reason. Static methods tend to cause fewer issues.
Rather than a deprecation warning (which AFAIK is not going to happen in the ECMAScript standard), couldn't userland code just use something like clone
or structuredClone
with a linter that bans to*
methods?
Mutating original arrays was a bad design. It could make sense in the beginning just to make the language more similar to Java, but it makes no sense to consider adding more mutating stuff to arrays in the future.
What I expect is that new array methods in the future have a name starting with to
without having a mutating counterpart. We’ll add a Array.prototype.toWhatever()
without having a Array.prototype.whatever()
mutating version.
So, in my humble opinion, if we had to add a @deprecated
it would have to be to .reverse()
, .sort()
, etc.
There are a few remaining holes in Array
's mutable APIs I want to see filled, though, all for performance reasons:
- Bulk pushing and unshifting into arrays
- Array set with both a start and end index (as well as a typed array equivalent)
- Array length reservation (spec-wise, a no-op, but engines should be taking it as a hint)
Mutation is king in perf-sensitive stuff. Sometimes, it is truly faster to create new objects, but it's rare. Once you start needing to meet effective performance targets of 100k+ ops per second on even mobile for non-trivial operations like path template evaluation (here's the tests, to show how it'd be called in practice) or physics calculations, you lose access to most nice things like immutability.
splice()
does that already, no?
For typed arrays, you can use .set()
, possibly in combination with .subarray()
if you want to copy only a part of an array (since you mentioned an end index).
For what it’s worth, there are very, very, very early talks about maybe adding standard Queue and Stack, which would probably support things like bulk shifting and pushing. Quoting one engine implementor:
take a personal pet peeve: we don't have some pretty basic data structures, like Queue, and people are using arrays to poorly emulate. how does something like that weigh against 'make iterators even more helpful'
there are crimes being done in the engine to support the expected algorithmic complexity of queue vs stack vs random-access arrays
yes, you can use [arrays] as such, but we'd also like have data structures fit for purpose
(Don’t expect any concrete proposals for these new data structures for a while. This is very early discussion.)
I still would need them for arrays anyways. I'm not pushing these to something I'm popping or unshifting from afterwards. I'm pushing them to something I'm then iterating and sometimes indexing. Stacks and queues don't work for that.
Really, what I truly would prefer is a FixedArray
abstraction with the same methods as typed arrays. But that seems to not be possible currently.
I mean in the sense of typed array .set(array, offset)
.
I know. I just would prefer to not be doing that when processing hundreds of thousands of buffers a second. I've also seen measurable perf increases in some cases by going back to Node's native buffer set APIs, especially in filesystem reading. (Their network ingress implementation is dog slow, being stuck in single digit megabits per core. Bun's is not, and it's much more limited by the underlying JS engine.)
In fact, this overhead directly causes JS apps to require larger read/write buffers than C apps to maintain equivalent performance. For a server to process 5 Gbps, it needs to process 4096-byte buffers at a little over 150k per second, but 65536-byte buffers at a little over 20k per second. For a simple copying pipe, you're doing 1 write and 1 read per process. For compression and decompression, that's 2 reads and 2 writes, meaning you're doing as much as 600k subarrays per second with 4096-byte buffers, but only about 80k subarrays per second with 65536-byte buffers.
On modern high-end processors, memmove/memcpy and memset can be scheduled at near petabit speeds on a single core, bandwidth permitting. To take Zen 3 and 4 as an example: rep movsb
(memcpy) and rep stosb
(memset) on Zen 4 can copy 16 bytes per cycle, or a full 4096-byte page in as little as 256 cycles (≈ 50-80 ns/op, or 384-640 Tbps). Conversely, a single load from L1 cache normally has a 4-5 cycle latency on most high-end cores, and L2 (where most JS property reads would come from) hangs around 14-16 cycles of latency. That means you can only do about 15-18 dependent L2 accesses or about 51-64 dependent L1 accesses in the same amount of time. Creating a temporary subarray might actually hit half of the dependent L2 access count abovr in JIT code if not successfully elided (and I've not seen JS engines do that even in simpler cases like with object literals returned from inlined functions and sent to other inline functions). And for promise creation and scheduling, you're blowing past all of it.
The overhead is non-negligible in high-performance servers and other perf-sensitive scenarios. And while WebAssembly is useful for avoiding this with stuff like audio, it isn't really an option for networking, especially video, because you'd be copying to/from buffers anyways.