2

I got "Maximum call stack size exceeded." error when using Array.apply() to convert a large Uint8Array to an Array.

I found that apply() passes my parameter array as arguments in Array's constructor and function's arguments are stored in stack. Therefore, if I pass a large array to apply(), it'll exceed the stack.

After some searching, I changed to use Array.from() and it solved my issue.

I read the algorithm in ECMAScript but I can't understand it. So, could someone tell me the difference between apply() and from()?

IFZR
  • 316
  • 1
  • 12
  • Not sure if this is a duplicate of that question. This question is asking about the difference between these two, which is not what the other question is asking, nor is it addressed in any of the answers – CertainPerformance Feb 20 '20 at 11:28
  • Do you also get an error if you do `Array(...yourArray)` ? Or maybe that doesn't work with typed arrays. – Felix Kling Feb 20 '20 at 11:31
  • @FelixKling You mean something like this `Array(new Uint8Array(data))`. It can run without error but it just created an array that has 1 Uint8Array element. Btw, I got the answer below, thank you. – IFZR Feb 21 '20 at 01:26
  • No, I really meant `Array(...data)`.... using argument spread, the modern alternative to `.apply`. But given the explanation it should result in the same issue. – Felix Kling Feb 21 '20 at 07:11
  • @FelixKling I tried using argument spread and it still caused the error `RangeError: Maximum call stack size exceeded` as expected – IFZR Feb 24 '20 at 01:43

1 Answers1

3

Array.apply calls Function.prototype.apply with a calling context (a this value) of Array. If the second parameter is an array-like object, it will call the Array constructor with all of the elements of that array-like object as arguments. Eg:

Array.apply(null, [1, 2, 3, 4])

results in, and is equivalent to

Array(1, 2, 3, 4)

But argument lists have a size limit. It depends on the engine, but it looks like you probably shouldn't try to pass more than 10,000 arguments or so.

In contrast, Array.from invokes the iterator of the array-like object. If the iterator doesn't exist, but the object has a length property, it will iterate from 0 up to the value of the length - 1 and create an array from those values. Here, since a Uint8Array has an iterator, that iterator is what gets invoked when you use Array.from.

Iterators don't have the same sort of size limit that argument lists have (unless they're spread into an argument list, or something similar). The iterator's .next method is called, returning a value to put into the array, until the iterator is exhausted. It's pretty similar to the following:

const arr = [0, 1, 2];

// Create a new array by iterating through arr's iterator
// just like Array.from is doing:
const newArr = [];
const iterator = arr[Symbol.iterator]();
let iterObj = iterator.next();
while (!iterObj.done) {
  newArr.push(iterObj.value);
  iterObj = iterator.next(); 
}
console.log(newArr);

There are no limits on how long the iterator may be, either in the above code, or in Array.from.

So, if the iterable or array-like object you have is very large, constructing an array from it with Array.apply (or by spreading into the Array constructor) may throw an error, whereas using Array.from will not.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320