Absolutely and Immutably so
push
[1, 2].concat([3])
// [1,2,3]
shift
[1,2,3].filter((e, i, a) => i != 0)
// [2,3]
pop
[1,2,3].filter((e, i, a) => i != a.length - 1)
// [1,2]
unshift
[1,2,3].concat([0]).map((e, i, a) => i == 0 ? a[a.length - 1] : a[i-1])
// [0,1,2,3]
Using the 2nd param index and 3rd param array inside map and filter makes things pretty flexible. You can try more by using that approach.
Another (easier) way but slightly reckless and NOT immutable
push
Array.prototype.pushChain = function(item) {
this.push(item)
return this
}
[1,2,3].pushChain(4)
//[1,2,3,4]
Meaning you can use the prototype of an array and use the simpler methods you want to chain but simply return this and you are good but the downside is you could mess up other code relying on the Array to be as it is or possibly overriding some other method you didn't intend.
OR
Another approach is make a JavaScript ES6+ type class like so
class ImmutableChainedArray {
constructor(arr) {
this._arr = Array.isArray(arr) ? arr : []
}
push(item) {
this._arr = this._arr.concat([item])
return this
}
pop() {
this._arr = this._arr
.filter((e, i, a) => i != a.length - 1)
return this
}
shift() {
this._arr = this._arr
.filter((e, i, a) => i != 0)
return this
}
unshift(item) {
this._arr = this._arr
.concat([item])
.map((e, i, a) => i == 0 ? a[a.length - 1] : a[i-1])
return this
}
// the rest of them are easy...
val() {
return this._arr
}
}
then use like
const ica = new ImmutableChainedArray([1])
ica.unshift(0).push(2).shift().pop().val()
// ends up with [1]