JavaScript Arrays

JavaScript Array concat(): Merging Arrays Efficiently

Master the JavaScript Array concat() method for merging arrays. Learn syntax, use cases, performance tips, and best practices with practical examples.

By JavaScript Document Team
arraysarray-methodsfundamentalsimmutability

The concat() method is used to merge two or more arrays, creating a new array without modifying the existing arrays. It's a fundamental method for array composition and is particularly valuable in functional programming patterns where immutability is important.

Understanding concat()

The concat() method returns a new array consisting of the array on which it is called, followed by the array(s) and/or value(s) provided as arguments. It performs a shallow copy of the elements from the original arrays.

Basic Syntax and Usage

// Syntax
array.concat(value1, value2, ..., valueN)

// Basic examples
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = arr1.concat(arr2);

console.log(combined); // [1, 2, 3, 4, 5, 6]
console.log(arr1); // [1, 2, 3] (unchanged)
console.log(arr2); // [4, 5, 6] (unchanged)

// Multiple arrays
const arr3 = [7, 8, 9];
const all = arr1.concat(arr2, arr3);
console.log(all); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

// Mixing arrays and values
const mixed = arr1.concat(4, [5, 6], 7);
console.log(mixed); // [1, 2, 3, 4, 5, 6, 7]

// Empty array concatenation
const empty = [].concat(arr1);
console.log(empty); // [1, 2, 3]
console.log(empty === arr1); // false (new array)

Shallow Copy Behavior

// Primitive values are copied
const primitives = [1, 'hello', true];
const primCopy = [].concat(primitives);
primCopy[0] = 99;
console.log(primitives); // [1, 'hello', true] (unchanged)
console.log(primCopy); // [99, 'hello', true]

// Objects are copied by reference
const objects = [{ id: 1 }, { id: 2 }];
const objCopy = [].concat(objects);
objCopy[0].id = 99;
console.log(objects[0]); // { id: 99 } (changed!)
console.log(objCopy[0]); // { id: 99 }

// Nested arrays
const nested = [[1, 2], [3, 4]];
const nestedCopy = [].concat(nested);
nestedCopy[0].push(3);
console.log(nested); // [[1, 2, 3], [3, 4]] (inner array modified)

// Deep copy alternatives
const deepCopy1 = JSON.parse(JSON.stringify(nested));
const deepCopy2 = structuredClone(nested);

Common Use Cases

Array Merging

// Simple merge
function merge(...arrays) {
  return [].concat(...arrays);
}

const result = merge([1, 2], [3, 4], [5, 6]);
console.log(result); // [1, 2, 3, 4, 5, 6]

// Merge with deduplication
function mergeUnique(...arrays) {
  return [...new Set([].concat(...arrays))];
}

const unique = mergeUnique([1, 2, 3], [2, 3, 4], [3, 4, 5]);
console.log(unique); // [1, 2, 3, 4, 5]

// Merge sorted arrays
function mergeSorted(arr1, arr2) {
  const result = [];
  let i = 0, j = 0;
  
  while (i < arr1.length && j < arr2.length) {
    if (arr1[i] <= arr2[j]) {
      result.push(arr1[i++]);
    } else {
      result.push(arr2[j++]);
    }
  }
  
  return result.concat(arr1.slice(i)).concat(arr2.slice(j));
}

const sorted1 = [1, 3, 5, 7];
const sorted2 = [2, 4, 6, 8];
console.log(mergeSorted(sorted1, sorted2)); // [1, 2, 3, 4, 5, 6, 7, 8]

// Conditional merging
function mergeIf(condition, arr1, arr2) {
  return condition ? arr1.concat(arr2) : arr1;
}

const base = [1, 2, 3];
const extra = [4, 5, 6];
const isVerbose = true;
console.log(mergeIf(isVerbose, base, extra)); // [1, 2, 3, 4, 5, 6]

Array Cloning

// Clone array
function cloneArray(arr) {
  return [].concat(arr);
}

// Alternative cloning methods comparison
const original = [1, 2, 3, { a: 4 }];

const clone1 = original.concat(); // concat
const clone2 = [...original]; // spread
const clone3 = Array.from(original); // Array.from
const clone4 = original.slice(); // slice

// All create shallow copies
clone1[3].a = 99;
console.log(original[3].a); // 99 (all reference same object)

// Safe array append (immutable)
function append(array, ...elements) {
  return array.concat(elements);
}

function prepend(array, ...elements) {
  return elements.concat(array);
}

const numbers = [3, 4, 5];
const appended = append(numbers, 6, 7);
const prepended = prepend(numbers, 1, 2);

console.log(numbers); // [3, 4, 5] (unchanged)
console.log(appended); // [3, 4, 5, 6, 7]
console.log(prepended); // [1, 2, 3, 4, 5]

Flattening Arrays

// Flatten one level
function flatten(arr) {
  return [].concat(...arr);
}

const nested = [[1, 2], [3, 4], [5, 6]];
console.log(flatten(nested)); // [1, 2, 3, 4, 5, 6]

// Flatten with mixed values
const mixed = [1, [2, 3], 4, [5, [6, 7]]];
console.log(flatten(mixed)); // [1, 2, 3, 4, 5, [6, 7]]

// Deep flatten using recursion
function deepFlatten(arr) {
  return arr.reduce((flat, item) => {
    return flat.concat(Array.isArray(item) ? deepFlatten(item) : item);
  }, []);
}

console.log(deepFlatten(mixed)); // [1, 2, 3, 4, 5, 6, 7]

// Flatten with depth control
function flattenDepth(arr, depth = 1) {
  if (depth <= 0) return arr.slice();
  
  return arr.reduce((flat, item) => {
    if (Array.isArray(item)) {
      return flat.concat(flattenDepth(item, depth - 1));
    }
    return flat.concat(item);
  }, []);
}

const deep = [1, [2, [3, [4, [5]]]]];
console.log(flattenDepth(deep, 2)); // [1, 2, 3, [4, [5]]]

Advanced Patterns

Functional Composition

// Array transformation pipeline
const pipeline = (...fns) => (arr) =>
  fns.reduce((result, fn) => fn(result), arr);

const addOne = arr => arr.map(n => n + 1);
const double = arr => arr.map(n => n * 2);
const positive = arr => arr.filter(n => n > 0);

const transform = pipeline(
  arr => arr.concat([0, -1, -2]), // Add elements
  positive, // Filter
  addOne, // Transform
  double // Transform again
);

console.log(transform([1, 2, 3])); // [4, 6, 8, 2]

// Immutable array methods
const arrayUtils = {
  push: (arr, ...items) => arr.concat(items),
  pop: arr => arr.slice(0, -1),
  shift: arr => arr.slice(1),
  unshift: (arr, ...items) => items.concat(arr),
  remove: (arr, index) => arr.slice(0, index).concat(arr.slice(index + 1)),
  insert: (arr, index, ...items) => 
    arr.slice(0, index).concat(items, arr.slice(index)),
  update: (arr, index, value) => 
    arr.slice(0, index).concat([value], arr.slice(index + 1))
};

// Usage
const arr = [1, 2, 3, 4, 5];
console.log(arrayUtils.remove(arr, 2)); // [1, 2, 4, 5]
console.log(arrayUtils.insert(arr, 2, 'a', 'b')); // [1, 2, 'a', 'b', 3, 4, 5]
console.log(arr); // [1, 2, 3, 4, 5] (unchanged)

Building Complex Data Structures

// Tree flattening
function flattenTree(node, childrenKey = 'children') {
  const result = [node];
  
  if (node[childrenKey] && Array.isArray(node[childrenKey])) {
    const childResults = node[childrenKey]
      .map(child => flattenTree(child, childrenKey));
    return result.concat(...childResults);
  }
  
  return result;
}

const tree = {
  id: 1,
  name: 'Root',
  children: [
    {
      id: 2,
      name: 'Child 1',
      children: [
        { id: 4, name: 'Grandchild 1' },
        { id: 5, name: 'Grandchild 2' }
      ]
    },
    { id: 3, name: 'Child 2' }
  ]
};

const flattened = flattenTree(tree);
console.log(flattened.map(node => node.name));
// ['Root', 'Child 1', 'Grandchild 1', 'Grandchild 2', 'Child 2']

// Matrix operations
class Matrix {
  constructor(data) {
    this.data = data;
  }
  
  static concat(matrices, axis = 0) {
    if (axis === 0) {
      // Vertical concatenation
      return new Matrix(
        matrices.reduce((result, matrix) => 
          result.concat(matrix.data), []
        )
      );
    } else {
      // Horizontal concatenation
      const result = [];
      const maxRows = Math.max(...matrices.map(m => m.data.length));
      
      for (let i = 0; i < maxRows; i++) {
        const row = matrices.reduce((rowResult, matrix) => 
          rowResult.concat(matrix.data[i] || []), []
        );
        result.push(row);
      }
      
      return new Matrix(result);
    }
  }
  
  toString() {
    return this.data.map(row => row.join(' ')).join('\n');
  }
}

const m1 = new Matrix([[1, 2], [3, 4]]);
const m2 = new Matrix([[5, 6], [7, 8]]);

console.log(Matrix.concat([m1, m2], 0).toString());
// 1 2
// 3 4
// 5 6
// 7 8

console.log(Matrix.concat([m1, m2], 1).toString());
// 1 2 5 6
// 3 4 7 8

Performance Optimizations

// Efficient concatenation for many arrays
function efficientConcat(arrays) {
  // Calculate total length
  const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
  const result = new Array(totalLength);
  
  let offset = 0;
  for (const array of arrays) {
    for (let i = 0; i < array.length; i++) {
      result[offset + i] = array[i];
    }
    offset += array.length;
  }
  
  return result;
}

// Benchmark
const manyArrays = Array.from({ length: 1000 }, (_, i) => 
  Array.from({ length: 100 }, (_, j) => i * 100 + j)
);

console.time('concat spread');
const result1 = [].concat(...manyArrays);
console.timeEnd('concat spread');

console.time('efficient concat');
const result2 = efficientConcat(manyArrays);
console.timeEnd('efficient concat');

// Lazy concatenation using generators
function* lazyConcat(...arrays) {
  for (const array of arrays) {
    yield* array;
  }
}

// Memory efficient for large arrays
const lazy = lazyConcat(
  Array.from({ length: 1000000 }, (_, i) => i),
  Array.from({ length: 1000000 }, (_, i) => i + 1000000)
);

// Only processes what's needed
for (const value of lazy) {
  if (value > 10) break;
  console.log(value);
}

concat() vs Other Methods

concat() vs push()

// concat() - returns new array
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const concatenated = arr1.concat(arr2);
console.log(arr1); // [1, 2, 3] (unchanged)
console.log(concatenated); // [1, 2, 3, 4, 5, 6]

// push() - modifies original array
const arr3 = [1, 2, 3];
arr3.push(...[4, 5, 6]); // Note: spread needed for array elements
console.log(arr3); // [1, 2, 3, 4, 5, 6] (modified)

// push() without spread adds array as single element
const arr4 = [1, 2, 3];
arr4.push([4, 5, 6]);
console.log(arr4); // [1, 2, 3, [4, 5, 6]]

// Performance comparison
function benchmarkConcat(size) {
  const base = Array.from({ length: size }, (_, i) => i);
  const toAdd = Array.from({ length: size }, (_, i) => i + size);
  
  console.time(`concat (${size} elements)`);
  const result1 = base.concat(toAdd);
  console.timeEnd(`concat (${size} elements)`);
  
  console.time(`push spread (${size} elements)`);
  const result2 = [...base];
  result2.push(...toAdd);
  console.timeEnd(`push spread (${size} elements)`);
}

benchmarkConcat(10000);

concat() vs Spread Operator

// Spread operator
const spread1 = [1, 2, 3];
const spread2 = [4, 5, 6];
const spreadResult = [...spread1, ...spread2];
console.log(spreadResult); // [1, 2, 3, 4, 5, 6]

// concat()
const concatResult = spread1.concat(spread2);
console.log(concatResult); // [1, 2, 3, 4, 5, 6]

// Differences with special objects
const arrayLike = {
  0: 'a',
  1: 'b',
  length: 2,
  [Symbol.isConcatSpreadable]: true
};

console.log([].concat(arrayLike)); // ['a', 'b']
console.log([...arrayLike]); // TypeError (not iterable)

// Symbol.isConcatSpreadable
const special = {
  0: 1,
  1: 2,
  length: 2
};

special[Symbol.isConcatSpreadable] = true;
console.log([0].concat(special)); // [0, 1, 2]

special[Symbol.isConcatSpreadable] = false;
console.log([0].concat(special)); // [0, { 0: 1, 1: 2, length: 2 }]

// Multiple levels of spreading
const multi1 = [1, [2, 3]];
const multi2 = [4, [5, 6]];

console.log(multi1.concat(multi2)); // [1, [2, 3], 4, [5, 6]]
console.log([...multi1, ...multi2]); // [1, [2, 3], 4, [5, 6]]

Real-World Examples

Data Processing Pipeline

class DataPipeline {
  constructor(data = []) {
    this.data = data;
    this.transforms = [];
  }
  
  static from(source) {
    if (Array.isArray(source)) {
      return new DataPipeline(source);
    }
    if (typeof source === 'string') {
      return new DataPipeline(source.split(''));
    }
    return new DataPipeline([source]);
  }
  
  concat(...sources) {
    const additional = sources.reduce((acc, source) => {
      if (source instanceof DataPipeline) {
        return acc.concat(source.data);
      }
      return acc.concat(source);
    }, []);
    
    return new DataPipeline(this.data.concat(additional));
  }
  
  transform(fn) {
    return new DataPipeline(fn(this.data));
  }
  
  filter(predicate) {
    return this.transform(data => data.filter(predicate));
  }
  
  map(mapper) {
    return this.transform(data => data.map(mapper));
  }
  
  unique() {
    return this.transform(data => [...new Set(data)]);
  }
  
  sort(compareFn) {
    return this.transform(data => data.slice().sort(compareFn));
  }
  
  chunk(size) {
    return this.transform(data => {
      const chunks = [];
      for (let i = 0; i < data.length; i += size) {
        chunks.push(data.slice(i, i + size));
      }
      return chunks;
    });
  }
  
  flatten(depth = 1) {
    return this.transform(data => {
      if (depth <= 0) return data;
      return data.reduce((flat, item) => {
        if (Array.isArray(item) && depth > 0) {
          return flat.concat(
            new DataPipeline(item).flatten(depth - 1).toArray()
          );
        }
        return flat.concat(item);
      }, []);
    });
  }
  
  toArray() {
    return this.data.slice();
  }
}

// Usage
const pipeline = DataPipeline.from([1, 2, 3])
  .concat([3, 4, 5], DataPipeline.from([5, 6, 7]))
  .unique()
  .map(n => n * 2)
  .filter(n => n > 6)
  .sort((a, b) => b - a);

console.log(pipeline.toArray()); // [14, 12, 10, 8]

Event System

class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  on(event, handler) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(handler);
    return this;
  }
  
  once(event, handler) {
    const onceHandler = (...args) => {
      handler(...args);
      this.off(event, onceHandler);
    };
    return this.on(event, onceHandler);
  }
  
  off(event, handler) {
    if (!this.events[event]) return this;
    
    const index = this.events[event].indexOf(handler);
    if (index !== -1) {
      // Use concat to create new array without the handler
      this.events[event] = this.events[event]
        .slice(0, index)
        .concat(this.events[event].slice(index + 1));
    }
    
    return this;
  }
  
  emit(event, ...args) {
    if (!this.events[event]) return this;
    
    // Create a copy to avoid issues if handlers modify the array
    const handlers = [].concat(this.events[event]);
    handlers.forEach(handler => handler(...args));
    
    return this;
  }
  
  // Merge events from another emitter
  merge(otherEmitter) {
    Object.keys(otherEmitter.events).forEach(event => {
      if (!this.events[event]) {
        this.events[event] = [];
      }
      this.events[event] = this.events[event]
        .concat(otherEmitter.events[event]);
    });
    return this;
  }
}

// Usage
const emitter1 = new EventEmitter();
const emitter2 = new EventEmitter();

emitter1.on('data', data => console.log('Emitter 1:', data));
emitter2.on('data', data => console.log('Emitter 2:', data));

// Merge emitters
emitter1.merge(emitter2);
emitter1.emit('data', 'test'); // Both handlers called

Query Builder

class QueryBuilder {
  constructor() {
    this.conditions = [];
    this.orderBy = [];
    this.groupBy = [];
  }
  
  where(condition) {
    const clone = this.clone();
    clone.conditions = clone.conditions.concat([condition]);
    return clone;
  }
  
  and(condition) {
    return this.where({ type: 'AND', condition });
  }
  
  or(condition) {
    return this.where({ type: 'OR', condition });
  }
  
  orderByAsc(field) {
    const clone = this.clone();
    clone.orderBy = clone.orderBy.concat([{ field, direction: 'ASC' }]);
    return clone;
  }
  
  orderByDesc(field) {
    const clone = this.clone();
    clone.orderBy = clone.orderBy.concat([{ field, direction: 'DESC' }]);
    return clone;
  }
  
  groupByField(field) {
    const clone = this.clone();
    clone.groupBy = clone.groupBy.concat([field]);
    return clone;
  }
  
  merge(otherQuery) {
    const clone = this.clone();
    clone.conditions = clone.conditions.concat(otherQuery.conditions);
    clone.orderBy = clone.orderBy.concat(otherQuery.orderBy);
    clone.groupBy = [...new Set(clone.groupBy.concat(otherQuery.groupBy))];
    return clone;
  }
  
  clone() {
    const clone = new QueryBuilder();
    clone.conditions = this.conditions.slice();
    clone.orderBy = this.orderBy.slice();
    clone.groupBy = this.groupBy.slice();
    return clone;
  }
  
  build() {
    return {
      where: this.conditions,
      orderBy: this.orderBy,
      groupBy: this.groupBy
    };
  }
}

// Usage
const baseQuery = new QueryBuilder()
  .where({ field: 'status', value: 'active' })
  .orderByDesc('createdAt');

const userQuery = baseQuery
  .and({ field: 'role', value: 'user' })
  .groupByField('department');

const adminQuery = baseQuery
  .and({ field: 'role', value: 'admin' })
  .orderByAsc('name');

console.log(userQuery.build());
console.log(adminQuery.build());

Error Handling and Edge Cases

// Safe concat wrapper
function safeConcat(array, ...values) {
  if (!Array.isArray(array)) {
    throw new TypeError('First argument must be an array');
  }
  
  // Filter out undefined and null but keep other falsy values
  const filtered = values.filter(v => v !== undefined && v !== null);
  
  try {
    return array.concat(...filtered);
  } catch (error) {
    console.error('Concat error:', error);
    return array.slice(); // Return copy of original
  }
}

// Handling circular references
function concatDeep(arr1, arr2, seen = new WeakSet()) {
  const result = [];
  
  const process = (item) => {
    if (item && typeof item === 'object') {
      if (seen.has(item)) {
        return '[Circular]';
      }
      seen.add(item);
      
      if (Array.isArray(item)) {
        return item.map(process);
      }
      
      // Clone object
      const cloned = {};
      for (const key in item) {
        cloned[key] = process(item[key]);
      }
      return cloned;
    }
    return item;
  };
  
  return result.concat(arr1.map(process), arr2.map(process));
}

// Type-safe concat
function typedConcat(array, ...values) {
  if (array.length === 0) {
    return array.concat(...values);
  }
  
  const type = typeof array[0];
  const sameType = values.flat().filter(v => typeof v === type);
  
  if (sameType.length !== values.flat().length) {
    console.warn('Type mismatch in concat operation');
  }
  
  return array.concat(sameType);
}

// Examples
const numbers = [1, 2, 3];
const mixed = typedConcat(numbers, [4, 5], 'six', [7, 8]);
console.log(mixed); // [1, 2, 3, 4, 5, 7, 8] (string filtered out)

Best Practices

// 1. Use concat for immutability
const original = [1, 2, 3];
const modified = original.concat(4); // Good: creates new array
// original.push(4); // Bad: mutates original

// 2. Prefer spread for simple cases
const simple1 = [1, 2, 3];
const simple2 = [4, 5, 6];
const combined = [...simple1, ...simple2]; // Clean and readable

// 3. Use concat for complex operations
const complex = arr1
  .concat(processArray(arr2))
  .concat(arr3.filter(condition))
  .concat(conditionalArray ? arr4 : []);

// 4. Be aware of performance implications
// For many small concatenations, consider building result once
const parts = [[1, 2], [3, 4], [5, 6], /* ... many more */];
const inefficient = parts.reduce((acc, part) => acc.concat(part), []);
const efficient = [].concat(...parts);

// 5. Document shallow copy behavior
/**
 * Merges user arrays, creating a new array.
 * Note: User objects are not cloned (shallow copy).
 */
function mergeUsers(users1, users2) {
  return users1.concat(users2);
}

Conclusion

The concat() method is an essential tool for array manipulation in JavaScript, particularly when immutability is important. Its ability to merge arrays without modifying the originals makes it invaluable for functional programming patterns and state management. While modern alternatives like the spread operator offer similar functionality, concat() remains relevant for its clarity, browser compatibility, and specific features like Symbol.isConcatSpreadable support. Understanding when and how to use concat() effectively helps write cleaner, more maintainable code that avoids unintended mutations.