slice()
ES3+Returns a shallow copy of a portion of an array into a new array object.
Syntax
array.slice(start, end)Parameters
start number optionalIndex at which to start extraction
end number optionalIndex before which to end extraction
Return Value
A new array containing the extracted elements
Examples
const fruits = ['apple', 'banana', 'cherry', 'date'];
console.log(fruits.slice(1, 3));
console.log(fruits.slice(-2)); 📌 When to Use
Use slice() whenever you need a contiguous portion of an array - or a copy of the whole thing - without touching the original. Its non-destructive contract is the whole point: pagination windows (items for page 3), previews (first five comments), tails (last ten log lines via negative indices), and defensive copies before handing an array to code you do not control are all slice() territory. The perennial confusion is with splice(): slice() reads and returns a new array, splice() surgically mutates the source; if the original must survive, slice() is your method. Compared with structuring alternatives - destructuring for "first element plus rest", at(-1) for a single trailing element - slice() wins when the result is genuinely a sub-array you will iterate or render. For whole-array copies, [...arr] and arr.slice() are equivalent in behavior and performance; pick one style and stay consistent. Two properties define its limits. The copy is shallow: nested objects are shared references, so slice() alone does not isolate you from mutations of the elements themselves. And its indices follow the half-open [start, end) convention - the end index is excluded - which composes cleanly with arithmetic like slice(page * size, (page + 1) * size).
⚠️ Common Mistakes
Confusing slice() with splice(). The one-letter difference separates a safe read from a destructive edit: slice(1, 3) returns a copy and leaves the array alone, while splice(1, 3) rips three elements out of the original and returns the removals. Reaching for the wrong one either silently mutates shared state or silently fails to.
Off-by-one errors from the exclusive end index. slice(0, 2) yields two elements (indices 0 and 1), not three - the element AT the end index is never included. The half-open convention is deliberate (length = end - start, and adjacent slices tile without overlap), but until it becomes reflex, verify boundaries with a small literal array in the console.
Treating the result as a deep copy. slice() copies one level: the new array is independent (push/pop/sort on it leave the source alone), but the objects inside are the same references, so copy[0].name = "x" edits the original's element too. For true isolation of nested data, use structuredClone() or rebuild the objects explicitly.
Expecting an error on out-of-range indices. slice() clamps silently: slice(5) on a three-element array returns [], and slice(0, 999) just returns everything. Convenient for pagination edges, but it also means a wrong index calculation produces an empty or short result instead of a stack trace pointing at the bug.
Mixing up which argument is which in the negative form: arr.slice(-2) means "the last two elements", but arr.slice(0, -2) means "everything except the last two". Reading negative indices as "count from the end" for each argument independently resolves the ambiguity.
✅ Best Practices
Copy before you mutate: const sorted = arr.slice().sort(...) protects the source from sort()'s in-place behavior, and the same guard applies before reverse() and splice(). In ES2023+ environments, toSorted(), toReversed(), and toSpliced() fold the copy step into the operation itself.
Lean on negative indices for tail access: arr.slice(-3) reads "last three items" without any arr.length arithmetic, and it degrades gracefully - if the array has fewer than three elements it simply returns them all, no clamping code required.
Build immutable replacements from two slices around the target: [...arr.slice(0, i), newItem, ...arr.slice(i + 1)] swaps one element without mutation. Where ES2023 is available, arr.with(i, newItem) says the same thing in one call and one allocation.
Derive pagination indices in one place: a getPage(items, page, size) helper that wraps items.slice((page - 1) * size, page * size) keeps the off-by-one arithmetic centralized and testable, rather than re-deriving start/end math at every call site.
Use slice() with no arguments as the explicit "defensive copy" idiom when returning internal arrays from a class or module. Callers get a snapshot they can mutate freely, and your internal state stays encapsulated - one line of insurance against action-at-a-distance bugs.
⚡ Performance Notes
slice() costs O(k) time and memory where k is the size of the extracted window, not the size of the source - slicing 10 items out of a million-element array is cheap. Because elements are copied by reference, the per-element cost is a pointer write regardless of how heavy the objects are; V8 additionally fast-paths whole-array clones of packed arrays, making arr.slice() and [...arr] effectively identical in speed (both compile down to an optimized elements-copy for common cases). The costs to watch are repetition and retention: re-slicing on every render or every scroll event churns the garbage collector, so memoize derived windows; and unlike some languages, JavaScript slices are copies rather than views, so a slice does NOT keep the giant source array alive - but it also means there is no zero-copy subarray for plain arrays (TypedArrays offer subarray() for that). For string building from array portions, slice().join() remains the idiomatic and well-optimized route.
🌍 Real World Example
Implementing Pagination
Client-side pagination is pure index arithmetic, and slice() is its natural engine: page number and page size determine a half-open window [start, end), and the exclusive end index means consecutive pages tile perfectly with no overlaps or gaps. The clamping behavior does the edge-case work for free - the final partial page just comes back shorter, and a page number past the data returns an empty array rather than throwing, which the UI can interpret as "no more results". The negative-index variant at the bottom covers the other everyday request, "most recent N items", without any length calculations. The source array is never modified, so sorting or filtering can be layered on top independently.
const allProducts = [...]; // 1000 products
function getPage(items, pageNum, pageSize = 10) {
const start = (pageNum - 1) * pageSize;
const end = start + pageSize;
return items.slice(start, end);
}
const page1 = getPage(allProducts, 1); // items 0-9
const page2 = getPage(allProducts, 2); // items 10-19
// Get last 5 items
const recentItems = allProducts.slice(-5);