Why hidden copies matter
Big O counts the work an operation does as the input grows. Most of the time you can see that work as a loop. Slicing and concatenation are different: they look like one cheap line, but they build a brand new array or string, and building it means copying every element or character that goes into it. The cost is hidden in the call.
The rule to keep in mind: an operation that returns a new array or string takes time proportional to the size of the result. That is why a harmless-looking slice inside a loop can turn a linear algorithm into a quadratic one.
Slicing
slice(start, end) works on both arrays and strings. It returns a new value holding the elements from start up to, but not including, end, and leaves the original alone.
const nums: number[] = [10, 20, 30, 40, 50];
const part = nums.slice(1, 4);
console.log(part, nums);
part[0] = 99;
console.log(part, nums);
const word: string = "algorithm";
console.log(word.slice(0, 4), word.slice(-4));
[ 20, 30, 40 ] [ 10, 20, 30, 40, 50 ]
[ 99, 30, 40 ] [ 10, 20, 30, 40, 50 ]
algo ithm
Changing part did not touch nums, which shows that part is a separate array. The copy is shallow: if the elements are objects, both arrays point at the same objects. A slice of k elements costs O(k) time and O(k) extra space. Slicing the whole array with nums.slice() costs O(n).
Reading nums[i] or word[i], by contrast, is O(1). If you only need to look at a range, loop over the indices instead of slicing it out.
A slice inside a loop
Here is a function that slices a prefix on every iteration, just to read its last element:
function sumPrefixCopy(arr: number[]): number {
let total = 0;
for (let i = 0; i < arr.length; i++) {
const prefix = arr.slice(0, i + 1);
total += prefix[prefix.length - 1];
}
return total;
}
The loop runs n times, and iteration i copies i + 1 elements. That is 1 + 2 + ... + n copied elements, which is n(n + 1) / 2, so the function is O(n²). Counting the copied elements for arrays of length 10, 20 and 40 gives 55, 210 and 820. Doubling the input multiplies the work by about four, the signature of quadratic growth. Replacing the slice with arr[i] makes the same function O(n).
Concatenation
Joining two arrays with concat or with the spread syntax also builds a new array:
const a = [1, 2], b = [3, 4, 5];
const joined = a.concat(b);
const spread = [...a, ...b];
console.log(joined, spread, a);
[ 1, 2, 3, 4, 5 ] [ 1, 2, 3, 4, 5 ] [ 1, 2 ]
Both forms cost O(n + m) for arrays of length n and m, and the original a is unchanged. Strings behave the same way in the analysis: s1 + s2 produces a new string whose length is the sum of the two. Strings are immutable, so there is no way to append in place. JavaScript engines have tricks that sometimes avoid the full copy, but those are optimizations you can't count on, and complexity analysis assumes the straightforward cost.
Building a result by repeated concatenation
Consider growing an array with concat inside a loop:
let result: number[] = [];
for (let i = 0; i < n; i++) {
result = result.concat([i]);
}
Each iteration copies the whole array built so far and adds one element. The copied element counts for n = 10, 20, 40 come out as 55, 210 and 820, the same triangle numbers as before: O(n²) in total. Compare it with result.push(i), which adds to the existing array. A push is amortized O(1), so the loop is O(n).
The same trap exists with strings. Prepending characters one by one, as in r = c + r to reverse a string, is the quadratic pattern when you analyse it by copying. When you collect many pieces, push them into an array and call join("") once at the end: that is O(n) overall.
Operations that move elements
Some array methods change the array in place but still touch many elements. unshift and shift add and remove at the front, so every other element has to change position: O(n) in general. Inserting or removing in the middle with splice is O(n) for the same reason. Only operations at the end, push and pop, are cheap.
| Operation | Time |
|---|---|
arr[i], str[i] | O(1) |
push, pop | O(1) amortized |
slice of k elements | O(k) |
concat, spread, + on strings | O(n + m) |
shift, unshift, splice | O(n) |
Common mistakes
- Counting a
sliceor a spread as one step. Count it as the size of the new value. - Slicing inside a loop when an index range would do. Pass
startandendindices to your function instead of sliced copies. - Building an array with
result = result.concat(...)orresult = [...result, x]in a loop. Usepush. - Forgetting the extra space. A slice of
kelements also usesO(k)memory, which matters when a problem asks forO(1)extra space.
Key takeaways
- Operations that return a new array or string cost time proportional to the new value's size.
- A
sliceorconcatinside a loop ofniterations is oftenO(n²)in total. - Indexing and
pushare cheap; prefer them when you build or inspect data. - Always read the line, not just the loop, when you add up the cost of an algorithm.