The JavaScript Array shift() Method is a built-in JavaScript array method used to remove the first element from an array. It modifies the original array by removing the element at index 0 and shifting all remaining elements one position to the left. The shift() method returns the removed element and automatically decreases the array’s length by one. It is commonly used for processing queues, managing task lists, handling incoming messages, and implementing First-In, First-Out (FIFO) data structures.
Syntax and Formal Specification
The method signature is shift(). It takes no arguments. Any arguments passed are silently ignored.
const queue = ["first", "second", "third"]; const removed = queue.shift(); console.log(removed); // "first" console.log(queue); // ["second", "third"]
Parameters
-
None.
shift()accepts no arguments. Passing arguments has no effect and does not throw.
const arr = [1, 2, 3]; arr.shift("ignored", 42); console.log(arr); // [2, 3]
Return Value
-
The first element of the array, if the array is non-empty.
-
undefinedif the array is empty.
const empty = []; console.log(empty.shift()); // undefined console.log(empty.length); // 0 console.log(empty); // []
The Generic Algorithm
Like other mutating array methods, shift() is generic. It operates on any object with a length property and integer-indexed properties. The specification algorithm, simplified:
-
Let
Obe the object on whichshift()is invoked (this). -
Convert
O.lengthto an integerlen. -
If
len === 0, setO.length = 0and returnundefined. -
Read
first = O[0]. -
For
kfrom1tolen - 1, copyO[k]toO[k - 1]. -
Delete the property
O[len - 1]. -
Set
O.length = len - 1. -
Return
first.
The leftward shift loop is the source of shift()‘s linear cost: every remaining element moves one slot closer to index 0.
const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 }; const first = Array.prototype.shift.call(arrayLike); console.log(first); // "a" console.log(arrayLike[0]); // "b" console.log(arrayLike[1]); // "c" console.log(arrayLike[2]); // undefined console.log(arrayLike.length); // 2
Strings cannot serve as this because they are immutable; indexed assignments fail silently in sloppy mode or throw in strict mode.
"use strict"; try { Array.prototype.shift.call("abc"); } catch (e) { console.log(e.constructor.name); // "TypeError" }
Performance: O(n) Removal and the Quadratic Trap
shift() is O(n) per call. Every remaining element moves one position lower in memory, and this cost grows with array size. Iterating shift() in a loop yields O(n²) behavior—a subtle but serious performance trap.
// Slow: O(n) per call, O(n^2) overall const queue = []; for (let i = 0; i < 100000; i++) queue.push(i); for (let i = 0; i < 100000; i++) queue.shift(); // Very slow // Fast: O(1) amortized per call const stack = []; for (let i = 0; i < 100000; i++) stack.push(i); for (let i = 0; i < 100000; i++) stack.pop(); // Cheap
For 100,000 elements, the shift() loop cumulatively performs roughly 5 billion internal assignments; the pop() loop performs about 100,000. Engines cannot optimize shift() away because the shifting is observable through indexed access and iterators.
When shift() Is Acceptable
shift() is fine when:
-
The array is small (dozens to hundreds of elements).
-
The operation is infrequent (one or two removals per action).
-
Readability outranks micro-optimization.
// Perfectly reasonable const nextJob = ["compile", "test", "deploy"]; const first = nextJob.shift(); console.log(first); // "compile" console.log(nextJob); // ["test", "deploy"]
Alternatives for Efficient FIFO
For queues over large arrays, avoid shift() and use one of the following patterns.
1. Head index over push() — Maintain a pointer to the logical front; periodically compact.
class Queue { constructor() { this.items = []; this.head = 0; } enqueue(x) { this.items.push(x); } dequeue() { if (this.head >= this.items.length) return undefined; return this.items[this.head++]; } get size() { return this.items.length - this.head; } } const q = new Queue(); for (let i = 0; i < 5; i++) q.enqueue(i); console.log(q.dequeue()); // 0 console.log(q.dequeue()); // 1 console.log(q.size); // 3
Periodically compact when head becomes large:
compact() { if (this.head > 1000) { this.items = this.items.slice(this.head); this.head = 0; } }
2. Ring buffer — Fixed-size circular queue with amortized O(1) operations at both ends.
class RingBuffer { constructor(capacity) { this.buffer = new Array(capacity); this.capacity = capacity; this.head = 0; this.tail = 0; this.size = 0; } enqueue(x) { if (this.size === this.capacity) throw new Error("Full"); this.buffer[this.tail] = x; this.tail = (this.tail + 1) % this.capacity; this.size++; } dequeue() { if (this.size === 0) return undefined; const x = this.buffer[this.head]; this.buffer[this.head] = undefined; this.head = (this.head + 1) % this.capacity; this.size--; return x; } } const rb = new RingBuffer(3); [1, 2, 3].forEach(n => rb.enqueue(n)); console.log(rb.dequeue()); // 1 rb.enqueue(4); console.log(rb.dequeue()); // 2
3. Reversed push/pop — If order is not user-visible, store newest at the end and pop instead of shift.
// FIFO with pop: enqueue pushes to a staging array, dequeue pops const staging = []; const delivery = []; function enqueue(x) { staging.push(x); } function dequeue() { if (delivery.length === 0) { while (staging.length) delivery.push(staging.pop()); } return delivery.pop(); } enqueue("a"); enqueue("b"); enqueue("c"); console.log(dequeue()); // "a" console.log(dequeue()); // "b"
This “two-stack queue” provides amortized O(1) enqueue and dequeue using only push() and pop().
Distinguishing shift() from pop(), splice(), and slice()
-
shift()— Mutates. Removes first element. O(n). -
pop()— Mutates. Removes last element. O(1). -
splice(0, 1)— Mutates. Removes first element. O(n) with higher constants thanshift(). -
slice(1)— Returns new array without first element. Non-mutating.
const base = [1, 2, 3]; const a = base.slice(); console.log(a.shift()); // 1 console.log(a); // [2, 3] const b = base.slice(); console.log(b.pop()); // 3 console.log(b); // [1, 2] const c = base.slice(); console.log(c.splice(0, 1)); // [1] console.log(c); // [2, 3] console.log(base.slice(1)); // [2, 3] console.log(base); // [1, 2, 3]
For pure first-element removal, shift() is faster than splice(0, 1) because splice engages the general-purpose insertion/removal algorithm.
shift() on arguments Objects
Legacy code sometimes uses Array.prototype.shift.call(arguments) to remove the first argument inside a function. Rest parameters make this unnecessary in modern JavaScript.
function legacy() { const first = Array.prototype.shift.call(arguments); return { first, remaining: Array.from(arguments) }; } console.log(legacy("a", "b", "c")); // { first: "a", remaining: ["b", "c"] } function modern(...args) { const first = args.shift(); return { first, remaining: args }; } console.log(modern("a", "b", "c")); // { first: "a", remaining: ["b", "c"] }
shift() on Frozen and Sealed Arrays
Frozen arrays reject shift() with a TypeError in strict mode. Sealed arrays also reject it because shift() requires deleting the final index, which sealing prevents.
"use strict"; const frozen = Object.freeze([1, 2, 3]); try { frozen.shift(); } catch (e) { console.log(e.constructor.name); // "TypeError" } const sealed = Object.seal([1, 2, 3]); try { sealed.shift(); } catch (e) { console.log(e.constructor.name); // "TypeError" }
shift() and Typed Arrays
Typed arrays (Uint8Array, Float64Array, etc.) do not expose shift() on their prototypes, and Array.prototype.shift cannot be applied to them because their length is read-only and their indexed properties are not deletable. Use subarray:
const typed = new Uint8Array([1, 2, 3, 4]); const withoutFirst = typed.subarray(1); console.log(withoutFirst); // Uint8Array [2, 3, 4] console.log(typed); // Uint8Array [1, 2, 3, 4]
