Why does Array.prototype.slice.call(arguments) turn the arguments object into a real array, and what replaces that idiom in modern JavaScript?
answer
- the methods never demanded a real array
- length plus numbered keys is enough
- call aims the method at the receiver
- three modern replacements, one still special
basics
~20 sArray methods are specified as generic: they read only a length property and integer-keyed properties from whatever this they are given. call supplies the array-like arguments object as that this. Modern code uses a rest parameter, Array.from, or spread instead.
solid answer
~40 sMost `Array.prototype` methods are deliberately generic — the specification defines them in terms of `this` converted to an object, its `length` property, and its integer-indexed properties, never requiring a real array. `Array.prototype.slice.call(arguments)` therefore works because `arguments` is array-like: it has `length` and numeric keys. `slice` walks those indices and builds a genuine array, so you get `map`, `filter` and the rest. The same trick borrows any generic method, for example `Array.prototype.join.call({ 0: 'a', 1: 'b', length: 2 }, '-')` giving `'a-b'`. Since ES2015 you rarely need it: a rest parameter `function f(...args)` gives a real array directly, `Array.from(arrayLike)` converts anything with `length` or an iterator, and `[...iterable]` spreads anything iterable. The distinction that still matters is that spread demands an *iterable*, while `Array.from` also accepts a plain array-like.
code
javascript · 14 linesconst likeArray = { 0: 'a', 1: 'b', length: 2 };
// Borrowing: the method only needs length + index properties
console.log(Array.prototype.join.call(likeArray, '-')); // 'a-b'
console.log(Array.prototype.slice.call(likeArray)); // ['a', 'b']
// Modern conversion
console.log(Array.from(likeArray)); // ['a', 'b']
try {
console.log([...likeArray]); // throws
} catch (e) {
console.log(e.constructor.name); // 'TypeError'
}go deeper
Know that arguments is not a real array and that this idiom converts it into one, and that modern code writes a rest parameter instead.
Explain the mechanism: array methods are specified over length plus integer-keyed properties, and call is what points them at a non-array receiver.
Distinguish Array.from from spread on the iterable-versus-array-like axis, and flag borrowing of mutating methods as a hazard on receivers you do not own.
Generalise it to interface design: methods written against a minimal structural contract stay reusable across types, which is why the array methods outlived the array.
## Generic by design When the specification defines `Array.prototype.slice`, it does not say "let A be the array". It converts `this` to an object, reads its `length` property, coerces that to a length, and then reads properties named `"0"`, `"1"`, and so on. Nothing in that algorithm requires the receiver to be an actual array. This genericity is intentional, and it is what method borrowing exploits. An **array-like** object is any object with a `length` property and integer-keyed properties. The classic example is `arguments`, the automatic object available inside a non-arrow function, which is array-like but is not an array and has none of the array methods on its prototype. ```js function old() { console.log(Array.isArray(arguments)); // false const args = Array.prototype.slice.call(arguments); console.log(Array.isArray(args)); // true return args.map(x => x * 2); } old(1, 2, 3); // [2, 4, 6] ``` `call` is doing the essential work: it invokes `slice` with `arguments` as the receiver. Without it there is no way to point the method at a non-array. Calling with no arguments makes `slice` copy from index 0 to `length`, so the result is a full, genuine array. ## The general borrowing pattern The same shape works for other generic methods and for other array-likes, including strings: ```js const likeArray = { 0: 'a', 1: 'b', length: 2 }; Array.prototype.join.call(likeArray, '-'); // 'a-b' Array.prototype.map.call('abc', c => c.toUpperCase()); // ['A', 'B', 'C'] Array.prototype.indexOf.call(likeArray, 'b'); // 1 ``` `apply` gets borrowed the same way, most famously for spreading an array into a variadic function before spread syntax existed: `Math.max.apply(null, nums)`. Borrowing is not universal. Methods that mutate need a writable receiver, so `Array.prototype.push.call(frozenLike, 'x')` fails under strict mode, and borrowing a mutating method onto an object you did not expect to change is a real source of bugs. Reading methods are the safe ones. ## What replaced it Since ES2015 there are three clearer tools, and new code should prefer them: ```js // 1. Rest parameter - args is already a real array, no conversion at all function modern(...args) { return args.map(x => x * 2); } // 2. Array.from - converts array-likes AND iterables Array.from({ 0: 'a', 1: 'b', length: 2 }); // ['a', 'b'] Array.from('abc'); // ['a', 'b', 'c'] // 3. Spread - converts iterables only [...'abc']; // ['a', 'b', 'c'] ``` The rest parameter is the best answer for the `arguments` case specifically, because it never creates the intermediate object at all, and it is the *only* option inside an arrow function, which has no `arguments` object of its own. ## The distinction that still bites `Array.from` and spread are not interchangeable: ```js const likeArray = { 0: 'a', 1: 'b', length: 2 }; Array.from(likeArray); // ['a', 'b'] [...likeArray]; // TypeError: likeArray is not iterable ``` Spread consumes an iterator, so the source must implement the iteration protocol. `Array.from` tries the iterator first and falls back to the `length`-and-indices reading. `arguments` happens to be iterable, so spread works on it, but a hand-rolled array-like or a host-provided collection may not be — and then only `Array.from` or the borrowing idiom works. `Array.from` also takes an optional mapping function as its second argument, `Array.from(src, fn)`, which avoids allocating an intermediate array — a small but real advantage over `[...src].map(fn)`. ## What to say in an interview The complete answer has three parts: array methods are specified generically over `length` plus index properties; `call` is what points such a method at a non-array receiver; and the idiom is legacy because rest parameters, `Array.from` and spread cover it more clearly. Adding the `Array.from`-versus-spread distinction shows you know *why* the old idiom has not vanished entirely.
- Why does Array.from work on { 0: 'a', 1: 'b', length: 2 } while spread throws on it?Spread consumes an iterator, so the source must implement the iteration protocol; a plain object with numeric keys does not, hence the `TypeError`. `Array.from` tries the iterator first and, when there is none, falls back to reading `length` and the integer-keyed properties — exactly the array-like contract that method borrowing relies on.
- Are there Array.prototype methods that are unsafe to borrow?The mutating ones. `push`, `sort`, `splice` and friends write back to the receiver and its `length`, so borrowing them onto an object you did not intend to modify silently changes it, and onto a frozen or non-writable target they throw in strict mode. Read-only methods such as `slice`, `map`, `join` and `indexOf` are the safe ones.
- Inside an arrow function, what does Array.prototype.slice.call(arguments) refer to?Not that arrow's arguments — arrows have no `arguments` binding of their own, so the identifier resolves up the scope chain to an enclosing non-arrow function's object, or is a `ReferenceError` at the top level. This is one more reason to use a rest parameter, which works identically in both function forms.
saying these in an interview costs you the question
- Says slice special-cases the arguments object
- Claims arguments is a real array
- Thinks spread works on any object with length
- Believes Array.from only accepts iterables
- Borrows a mutating method onto a shared object