What Is the JavaScript Array push() Method?
push() is a built-in, prototype-based method on Array.prototype that appends one or more elements to the end of an array, mutating the original array in place, and returning the array’s new length. It is the counterpart to pop(), which removes the last element. Together, push() and pop() implement the LIFO (last-in, first-out) stack behavior that underpins countless algorithms, parsers, and state machines in JavaScript.
Syntax and Formal Specification
The method adheres to a variadic signature: push(element1, element2, ..., elementN). It accepts zero or more arguments, appends them sequentially to the calling array, and returns the new length property of the object upon which it was invoked.
const fruits = ["apple", "banana"]; const newLength = fruits.push("cherry", "date"); console.log(fruits); // ["apple", "banana", "cherry", "date"] console.log(newLength); // 4
Parameters
-
element1, ..., elementN— Zero or more values of any type (primitives, objects, functions,null,undefined, nested arrays). Each argument is appended as its own element in the exact order provided. -
No fixed arity —
push()accepts any number of arguments, including none.
const arr = [1, 2, 3]; console.log(arr.push()); // 3 (no arguments, no change) console.log(arr); // [1, 2, 3]
Return Value
An integer equal to the new length of the array after the append. Because it returns a number, push() is not chainable with other array methods.
const stack = []; stack.push("a"); // 1 stack.push("b", "c"); // 3 console.log(stack); // ["a", "b", "c"]
The Generic Algorithm
push() is intentionally generic. The ECMAScript specification does not restrict it to Array instances. Instead, the algorithm operates on any object possessing a length property and integer-indexed properties. When invoked, the method:
-
Reads
this.lengthand converts it to an integer (len). -
Iterates through the provided arguments, assigning each to
this[len],this[len+1], and so forth. -
Sets
this.lengthto the final value oflenafter all arguments have been assigned.
This genericity allows push() to operate on array-like objects via Function.prototype.call or Reflect.apply.
const collection = { length: 0, add(item) { Array.prototype.push.call(this, item); } }; collection.add("alpha"); collection.add("beta"); console.log(collection.length); // 2 console.log(collection[0]); // "alpha" console.log(collection[1]); // "beta"
Strings are explicitly unsuitable for this technique because they are immutable; assignment to indexed properties fails silently in sloppy mode or throws a TypeError in strict mode.
"use strict"; Array.prototype.push.call("abc", "d"); // TypeError: Cannot assign to read only property '3' of string 'abc'
Mutability and the Immutability Trade-off
push() is a mutating method. It alters both the content and the length of the original array in place. This is the correct choice when the array is a local accumulator, a buffer, or a data structure whose identity must be preserved (e.g., a reactive store or a class-owned collection).
const buffer = []; function log(message) { buffer.push(`[${new Date().toISOString()}] ${message}`); } log("Server started"); log("Request received"); console.log(buffer.length); // 2
When immutability is required—such as in Redux reducers, React state updates, or functional pipelines—push() is the wrong tool. The idiomatic alternatives are:
| Approach | Mechanism | Use Case |
|---|---|---|
arr.concat([item]) |
Returns a new array; original untouched | When item is already known |
[...arr, item] |
Spread syntax creates a shallow copy | Modern, readable, preferred for single or few items |
arr.concat(items) |
Concatenates arrays without spreading elements | When items is already an array |
// Mutating (fine for local accumulators) const local = [1, 2]; local.push(3); // local: [1, 2, 3] // Immutable (required for state management) const state = [1, 2]; const nextState = [...state, 3]; // state: [1, 2], nextState: [1, 2, 3]
A critical distinction: concat flattens array arguments one level. If you call arr.push([1, 2]), the entire array [1, 2] becomes a single element at the end. Calling arr.concat([1, 2]) spreads 1 and 2 as separate elements.
const a = [0]; a.push([1, 2]); console.log(a); // [0, [1, 2]] const b = [0]; console.log(b.concat([1, 2])); // [0, 1, 2]
Performance: Amortized Constant Time and the Reallocation Boundary
The theoretical time complexity of push() is O(1) per operation. In practice, it is amortized O(1). JavaScript engines implement arrays as dynamic slot arrays. When the underlying storage is full, the engine must allocate a larger block of memory and copy existing elements into it. This reallocation incurs an O(n) cost, but because the new capacity is typically grown by a factor (commonly around 1.5× to 2×), the expensive copy operation occurs at exponentially decreasing frequencies. Over a sequence of n pushes, the total cost remains O(n), yielding an amortized O(1) per push.
This behavior contrasts sharply with unshift(), which prepends elements. Because the start of the array is occupied, unshift() typically requires reallocating memory and copying all existing elements on every operation, resulting in linear time per call and quadratic behavior over a series of unshifts.
// Fast: O(1) amortized const append = []; for (let i = 0; i < 100000; i++) append.push(i); // Slow: O(n) per call, O(n^2) total const prepend = []; for (let i = 0; i < 100000; i++) prepend.unshift(i);
The Sparse Array Caveat
For sparse arrays (arrays with holes, created via new Array(n) without filling), engines may use dictionary-mode representations. In such cases, push() performance characteristics differ, and some engines exhibit constant time for pushes into sparse arrays implemented as hash tables. However, this is an implementation detail, not a guarantee. Dense arrays initialized with literal syntax ([]) generally provide the most predictable and optimized push performance.
const dense = []; // Fast-path internal representation const sparse = new Array(1000); // May trigger dictionary mode
Preallocation vs. push()
For performance-critical accumulation with a known final size, preallocating and assigning by index can outperform push() because it avoids capacity checks and reallocation logic.
// Known size: preallocate and assign const n = 1000000; const preallocated = new Array(n); for (let i = 0; i < n; i++) preallocated[i] = i; // Unknown size: push is natural and efficient const dynamic = []; for (let i = 0; i < n; i++) dynamic.push(i);
Benchmarking in the target runtime is the only reliable way to determine the better approach for a specific workload.
push() with Spread Syntax: Merging and Argument Limits
The spread operator (...) combines naturally with push() to merge arrays:
const target = [1, 2, 3]; const source = [4, 5, 6]; target.push(...source); console.log(target); // [1, 2, 3, 4, 5, 6]
This pattern appends each element of source individually. It is essential to recognize the argument count limit. JavaScript engines impose a maximum number of arguments that can be passed to a function. Spreading a very large array (typically in the range of tens of thousands to hundreds of thousands of elements, depending on the engine) into push() will throw a RangeError.
const huge = new Array(200000).fill(0); const target = []; // target.push(...huge); // RangeError: Maximum call stack size exceeded
For merging large arrays, concat() or an iterative push loop is safer:
function appendAll(target, source) { for (let i = 0; i < source.length; i++) { target.push(source[i]); } return target; } const merged = appendAll([], huge); console.log(merged.length); // 200000
