JavaScript provides an arguments object inside any non-arrow function and it provides the list of arguments that were used to call the function.

While this allows us to handle a variable number of arguments in our function, we can use rest parameters to the same effect in a less convoluted way.

Where the arguments object comes in uniquely useful is when we want to handle optional parameters in a way that default parameters cannot.

The Case of Array Splice

The array splice method can do several things, one of which is deleting items.

The way the deleteCount parameter works cannot be implemented using default parameters because if we do not pass a value then all items from the start index onwards are removed. If we do pass undefined for deleteCount then that is interpreted as 0 (zero) and no items are deleted.

If we were to implement an array-like object we would not be able to implement our own splice method using default parameters, instead, we would need to handle the different cases by inspecting the arguments object.

class MyArrayLike {
    // ...
    splice(start, deleteCount, ...newItems) {
        const resolvedDeleteCount = (
            arguments.length === 1
                ? this.length
                : (Number(deleteCount) || 0)
        );
        // ...
    }
}

The snippet above will check whether the collection length is the resolved value that should be used when deleteCount is not provided, or fall back to 0 (zero) for invalid numbers.

Inheritance and Overriding

So far, this is great, however, in some cases, we may need to extend our array-like implementation and change a little bit what splice does. Following the usual pattern, we would just override the method and call that base one if we need it.

class MySpecificArrayLike extends MyArrayLike {
    // ...
    splice(start, deleteCount, ...newItems) {
        // ...
        super.splice(start, deleteCount, ...newItems);
    }
}

No problem here, right? Well, it turns out there is one. The problem of deleteCount manifests itself in a different way this time.

We provide all arguments when calling the base method, we provide a value for deleteCount as well even when we call the override without it. In other words, our call to the base method is not consistent.

To resolve this, we need to call the base method and provide the exact same arguments. Thus, the way our overridden method was called is preserved in its entirety making our code truly polymorphic.

class MySpecificArrayLike extends MyArrayLike {
    // ...
    splice(start, deleteCount, ...newItems) {
        // ...
        super.splice.apply(this, arguments);
    }
}

Closing Thoughts

I’ve encountered this issue while working on the observable collections in React MVVM. Read-only observable collections implement all methods, but the mutating ones like splice are marked as protected. An observable collection is defined as well, but all it does is expose the mutating methods as public and thus can be used similarly to how an array is used.

This approach allows you to have your own custom observable collections, controlling how items are added or removed while reusing the entire implementation.

Having found this solution, it is neat and it may actually be the proper way to override methods in JavaScript as it maintains the way the override was called when having to call the base method.

The cases presented are not very common to be fair, it’s the first time I run into this and it is only because I wanted to provide an observable collection that has a very similar interface with an array so its usage will not feel as strange and be seamless.

I do think that we should generally use default parameters when this is the case rather than check the arguments object, however when we extend types it might be best to apply the arguments when wanting to call a base method just to be safe as it works in all cases.