JavaScript Shallow Copy: Methods and Techniques
Learn about shallow copying in JavaScript. Understand different methods to create shallow copies, their limitations, and when to use them.
JavaScript Shallow Copy: Methods and Techniques
A shallow copy creates a new object or array that copies the top-level properties of the original. However, if those properties contain references to objects or arrays, only the references are copied, not the actual nested objects. Understanding shallow copying is crucial for proper data manipulation in JavaScript.
What is a Shallow Copy?
A shallow copy duplicates an object's top-level properties into a new object. For primitive values, the actual values are copied. For reference values (objects, arrays), only the references are copied, meaning both the original and the copy share the same nested objects.
// Example demonstrating shallow copy behavior
const original = {
name: 'John',
age: 30,
address: {
city: 'New York',
zip: '10001',
},
};
// Create a shallow copy
const shallowCopy = { ...original };
// Modifying primitive values
shallowCopy.name = 'Jane';
console.log(original.name); // 'John' (unchanged)
console.log(shallowCopy.name); // 'Jane'
// Modifying nested objects
shallowCopy.address.city = 'Los Angeles';
console.log(original.address.city); // 'Los Angeles' (changed!)
console.log(shallowCopy.address.city); // 'Los Angeles'
// Both objects share the same address reference
console.log(original.address === shallowCopy.address); // true
Shallow Copy Methods for Objects
1. Spread Operator (...)
The spread operator is the most modern and concise way to create shallow copies.
// Basic object spread
const person = {
firstName: 'Alice',
lastName: 'Smith',
skills: ['JavaScript', 'React'],
};
const copiedPerson = { ...person };
// Adding/overriding properties
const updatedPerson = {
...person,
lastName: 'Johnson',
age: 25,
};
console.log(updatedPerson);
// { firstName: 'Alice', lastName: 'Johnson', skills: ['JavaScript', 'React'], age: 25 }
// Combining multiple objects
const defaults = { theme: 'dark', language: 'en' };
const userPrefs = { theme: 'light', fontSize: 16 };
const settings = { ...defaults, ...userPrefs };
console.log(settings); // { theme: 'light', language: 'en', fontSize: 16 }
// Order matters - later spreads override earlier ones
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const combined = { ...obj1, ...obj2 };
console.log(combined); // { a: 1, b: 3, c: 4 }
2. Object.assign()
Object.assign()
copies all enumerable properties from one or more source objects to a target object.
// Basic Object.assign usage
const source = {
name: 'Product',
price: 100,
tags: ['electronics', 'gadget'],
};
// Create a new object
const copy1 = Object.assign({}, source);
// Modify existing object (mutates target)
const target = { id: 1 };
Object.assign(target, source);
console.log(target);
// { id: 1, name: 'Product', price: 100, tags: ['electronics', 'gadget'] }
// Multiple sources
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const obj3 = { c: 5, d: 6 };
const merged = Object.assign({}, obj1, obj2, obj3);
console.log(merged); // { a: 1, b: 3, c: 5, d: 6 }
// Copying with property descriptors
const original = {
name: 'Original',
get timestamp() {
return Date.now();
},
};
const assigned = Object.assign({}, original);
console.log(assigned.timestamp); // undefined (getter not copied as getter)
// Handling null/undefined
Object.assign({}, null); // {} (ignores null)
Object.assign({}, undefined); // {} (ignores undefined)
Object.assign({}, 'abc'); // { '0': 'a', '1': 'b', '2': 'c' }
3. Object.create() with Object.getOwnPropertyDescriptors()
This method preserves property descriptors, including getters and setters.
// Preserving property descriptors
const original = {
_value: 0,
get value() {
return this._value;
},
set value(v) {
this._value = v;
},
regularProp: 'normal',
};
// Object.assign doesn't preserve getters/setters
const assignCopy = Object.assign({}, original);
console.log(Object.getOwnPropertyDescriptor(assignCopy, 'value'));
// { value: 0, writable: true, enumerable: true, configurable: true }
// Object.create with descriptors preserves them
const descriptorCopy = Object.create(
Object.getPrototypeOf(original),
Object.getOwnPropertyDescriptors(original)
);
console.log(Object.getOwnPropertyDescriptor(descriptorCopy, 'value'));
// { get: [Function: get value], set: [Function: set value], enumerable: true, configurable: true }
// Helper function for descriptor-aware copying
function shallowCopyWithDescriptors(obj) {
return Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
}
4. Using a for...in Loop
Manual copying gives you fine-grained control over what gets copied.
// Manual shallow copy
function shallowCopyObject(obj) {
const copy = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = obj[key];
}
}
return copy;
}
// Custom copy with filtering
function selectiveCopy(obj, keys) {
const copy = {};
for (let key of keys) {
if (key in obj) {
copy[key] = obj[key];
}
}
return copy;
}
const user = {
id: 1,
name: 'John',
email: 'john@example.com',
password: 'secret',
role: 'admin',
};
// Copy only specific properties
const publicUser = selectiveCopy(user, ['id', 'name', 'email']);
console.log(publicUser); // { id: 1, name: 'John', email: 'john@example.com' }
// Copy with transformation
function transformCopy(obj, transformer) {
const copy = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
const { newKey = key, value = obj[key] } =
transformer(key, obj[key]) || {};
copy[newKey] = value;
}
}
return copy;
}
const transformed = transformCopy(user, (key, value) => {
if (key === 'name') {
return { newKey: 'fullName', value: value.toUpperCase() };
}
if (key === 'password') {
return null; // Skip this property
}
return { newKey: key, value: value };
});
Shallow Copy Methods for Arrays
1. Spread Operator for Arrays
// Basic array spread
const numbers = [1, 2, 3, 4, 5];
const copiedNumbers = [...numbers];
// Combining arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// Adding elements
const withExtra = [...numbers, 6, 7];
const withPrepended = [0, ...numbers];
// Nested arrays problem
const matrix = [
[1, 2],
[3, 4],
];
const copiedMatrix = [...matrix];
copiedMatrix[0][0] = 99;
console.log(matrix[0][0]); // 99 (original changed!)
// Array of objects
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
const copiedUsers = [...users];
copiedUsers[0].name = 'Alicia';
console.log(users[0].name); // 'Alicia' (original changed!)
2. Array.from()
Array.from()
creates a new array from an array-like or iterable object.
// Basic usage
const original = [1, 2, 3, 4, 5];
const copy = Array.from(original);
// With mapping function
const doubled = Array.from(original, (x) => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// From array-like objects
const nodeList = document.querySelectorAll('div');
const divArray = Array.from(nodeList);
// From Set
const uniqueNumbers = new Set([1, 2, 2, 3, 3, 4]);
const arrayFromSet = Array.from(uniqueNumbers);
console.log(arrayFromSet); // [1, 2, 3, 4]
// From Map
const map = new Map([
['a', 1],
['b', 2],
]);
const arrayFromMap = Array.from(map);
console.log(arrayFromMap); // [['a', 1], ['b', 2]]
// Creating arrays with specific length
const emptyArray = Array.from({ length: 5 });
console.log(emptyArray); // [undefined, undefined, undefined, undefined, undefined]
const sequence = Array.from({ length: 5 }, (_, i) => i);
console.log(sequence); // [0, 1, 2, 3, 4]
3. Array.prototype.slice()
The slice()
method returns a shallow copy of a portion of an array.
// Copy entire array
const fruits = ['apple', 'banana', 'orange', 'grape'];
const copiedFruits = fruits.slice();
// Copy portion of array
const citrus = fruits.slice(2); // ['orange', 'grape']
const firstTwo = fruits.slice(0, 2); // ['apple', 'banana']
const middleTwo = fruits.slice(1, 3); // ['banana', 'orange']
// Negative indices
const lastTwo = fruits.slice(-2); // ['orange', 'grape']
const allButLast = fruits.slice(0, -1); // ['apple', 'banana', 'orange']
// Shallow copy behavior with objects
const users = [
{ id: 1, name: 'Alice', preferences: { theme: 'dark' } },
{ id: 2, name: 'Bob', preferences: { theme: 'light' } },
];
const copiedUsers = users.slice();
copiedUsers[0].preferences.theme = 'blue';
console.log(users[0].preferences.theme); // 'blue' (original changed!)
4. Array.prototype.concat()
concat()
creates a new array by merging arrays.
// Basic concatenation
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = arr1.concat(arr2);
console.log(combined); // [1, 2, 3, 4, 5, 6]
// 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]
// Mixed values
const mixed = arr1.concat(4, [5, 6], 7);
console.log(mixed); // [1, 2, 3, 4, 5, 6, 7]
// Empty concat for copying
const original = [1, 2, 3];
const copy = original.concat();
console.log(copy); // [1, 2, 3]
console.log(original === copy); // false
// Shallow copy behavior
const objects = [{ a: 1 }, { b: 2 }];
const copiedObjects = objects.concat();
copiedObjects[0].a = 99;
console.log(objects[0].a); // 99 (original changed!)
Understanding Shallow Copy Limitations
Nested Objects and Arrays
// Demonstrating shallow copy limitations
const company = {
name: 'Tech Corp',
employees: [
{ id: 1, name: 'Alice', skills: ['JS', 'React'] },
{ id: 2, name: 'Bob', skills: ['Python', 'Django'] },
],
address: {
street: '123 Main St',
city: 'Techville',
coordinates: {
lat: 40.7128,
lng: -74.006,
},
},
};
// Shallow copy
const shallowCopy = { ...company };
// Top-level primitive changes don't affect original
shallowCopy.name = 'New Tech Corp';
console.log(company.name); // 'Tech Corp'
// Nested object changes affect original
shallowCopy.address.city = 'New City';
console.log(company.address.city); // 'New City' (changed!)
// Array changes affect original
shallowCopy.employees[0].name = 'Alicia';
console.log(company.employees[0].name); // 'Alicia' (changed!)
// Even nested array changes
shallowCopy.employees[0].skills.push('Vue');
console.log(company.employees[0].skills); // ['JS', 'React', 'Vue'] (changed!)
// Very deep nesting
shallowCopy.address.coordinates.lat = 0;
console.log(company.address.coordinates.lat); // 0 (changed!)
Working with Different Data Types
// Special values and types
const special = {
string: 'text',
number: 42,
boolean: true,
null: null,
undefined: undefined,
symbol: Symbol('sym'),
bigint: 123n,
date: new Date(),
regex: /pattern/g,
function: function () {
return 'hello';
},
arrow: () => 'world',
array: [1, 2, 3],
object: { nested: true },
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
};
// Spread operator copy
const spreadCopy = { ...special };
// Check what gets copied correctly
console.log(spreadCopy.string === special.string); // true (primitive)
console.log(spreadCopy.date === special.date); // true (same reference)
console.log(spreadCopy.date.getTime() === special.date.getTime()); // true
// Modifying date affects both
spreadCopy.date.setFullYear(2025);
console.log(special.date.getFullYear()); // 2025 (changed!)
// Map and Set are copied by reference
spreadCopy.map.set('newKey', 'newValue');
console.log(special.map.has('newKey')); // true (changed!)
// Function references are copied
console.log(spreadCopy.function === special.function); // true
Practical Examples
1. State Management
// React-style state updates
class StateManager {
constructor(initialState) {
this.state = initialState;
this.listeners = [];
}
setState(updates) {
// Shallow merge
this.state = {
...this.state,
...updates,
};
this.notify();
}
updateNested(path, value) {
// Handle nested updates properly
const keys = path.split('.');
const lastKey = keys.pop();
let current = this.state;
const newState = { ...this.state };
let newCurrent = newState;
for (const key of keys) {
newCurrent[key] = { ...current[key] };
current = current[key];
newCurrent = newCurrent[key];
}
newCurrent[lastKey] = value;
this.state = newState;
this.notify();
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter((l) => l !== listener);
};
}
notify() {
this.listeners.forEach((listener) => listener(this.state));
}
}
// Usage
const store = new StateManager({
user: {
name: 'John',
preferences: {
theme: 'dark',
language: 'en',
},
},
posts: [],
});
store.updateNested('user.preferences.theme', 'light');
2. Configuration Merging
// Configuration system with shallow copying
class Config {
constructor(defaults) {
this.defaults = defaults;
this.overrides = {};
}
set(overrides) {
// Shallow merge overrides
this.overrides = {
...this.overrides,
...overrides,
};
}
get(key) {
const config = {
...this.defaults,
...this.overrides,
};
return key ? config[key] : config;
}
reset() {
this.overrides = {};
}
freeze() {
// Return immutable config
return Object.freeze({
...this.defaults,
...this.overrides,
});
}
}
// Usage
const appConfig = new Config({
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
headers: {
'Content-Type': 'application/json',
},
});
appConfig.set({
apiUrl: 'https://api.staging.com',
timeout: 10000,
});
console.log(appConfig.get());
// Note: headers object is still shared!
3. Array Manipulation Utilities
// Safe array manipulation functions
const ArrayUtils = {
// Add item without mutation
push(array, ...items) {
return [...array, ...items];
},
// Remove item by index
removeAt(array, index) {
return [...array.slice(0, index), ...array.slice(index + 1)];
},
// Update item at index
updateAt(array, index, value) {
const copy = [...array];
copy[index] = value;
return copy;
},
// Insert at index
insertAt(array, index, ...items) {
return [...array.slice(0, index), ...items, ...array.slice(index)];
},
// Remove by value
remove(array, value) {
return array.filter((item) => item !== value);
},
// Remove by predicate
removeWhere(array, predicate) {
return array.filter((item) => !predicate(item));
},
// Update where
updateWhere(array, predicate, updater) {
return array.map((item) => (predicate(item) ? updater(item) : item));
},
// Move item
move(array, fromIndex, toIndex) {
const copy = [...array];
const [item] = copy.splice(fromIndex, 1);
copy.splice(toIndex, 0, item);
return copy;
},
};
// Usage examples
const numbers = [1, 2, 3, 4, 5];
console.log(ArrayUtils.push(numbers, 6, 7)); // [1, 2, 3, 4, 5, 6, 7]
console.log(ArrayUtils.removeAt(numbers, 2)); // [1, 2, 4, 5]
console.log(ArrayUtils.updateAt(numbers, 1, 20)); // [1, 20, 3, 4, 5]
console.log(ArrayUtils.insertAt(numbers, 2, 2.5)); // [1, 2, 2.5, 3, 4, 5]
console.log(numbers); // [1, 2, 3, 4, 5] (original unchanged)
4. Object Utilities
// Safe object manipulation
const ObjectUtils = {
// Omit properties
omit(obj, ...keys) {
const copy = { ...obj };
keys.forEach((key) => delete copy[key]);
return copy;
},
// Pick properties
pick(obj, ...keys) {
const copy = {};
keys.forEach((key) => {
if (key in obj) {
copy[key] = obj[key];
}
});
return copy;
},
// Rename property
renameKey(obj, oldKey, newKey) {
const { [oldKey]: value, ...rest } = obj;
return { ...rest, [newKey]: value };
},
// Map values
mapValues(obj, mapper) {
const copy = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = mapper(obj[key], key);
}
}
return copy;
},
// Filter object
filter(obj, predicate) {
const copy = {};
for (const key in obj) {
if (obj.hasOwnProperty(key) && predicate(obj[key], key)) {
copy[key] = obj[key];
}
}
return copy;
},
// Merge with custom logic
mergeWith(target, source, customizer) {
const result = { ...target };
for (const key in source) {
if (source.hasOwnProperty(key)) {
if (customizer && key in target) {
result[key] = customizer(target[key], source[key], key);
} else {
result[key] = source[key];
}
}
}
return result;
},
};
// Usage
const user = {
id: 1,
name: 'John',
email: 'john@example.com',
password: 'secret',
age: 30,
};
console.log(ObjectUtils.omit(user, 'password'));
// { id: 1, name: 'John', email: 'john@example.com', age: 30 }
console.log(ObjectUtils.pick(user, 'id', 'name', 'email'));
// { id: 1, name: 'John', email: 'john@example.com' }
console.log(ObjectUtils.renameKey(user, 'name', 'fullName'));
// { id: 1, fullName: 'John', email: 'john@example.com', ... }
Performance Considerations
// Performance comparison of shallow copy methods
function performanceTest() {
const testObject = {};
const testArray = [];
// Create large object and array
for (let i = 0; i < 10000; i++) {
testObject[`key${i}`] = i;
testArray.push(i);
}
// Test object copying methods
console.time('Object spread');
const spread1 = { ...testObject };
console.timeEnd('Object spread');
console.time('Object.assign');
const assign1 = Object.assign({}, testObject);
console.timeEnd('Object.assign');
console.time('Manual loop');
const manual1 = {};
for (let key in testObject) {
manual1[key] = testObject[key];
}
console.timeEnd('Manual loop');
// Test array copying methods
console.time('Array spread');
const spread2 = [...testArray];
console.timeEnd('Array spread');
console.time('Array.from');
const from2 = Array.from(testArray);
console.timeEnd('Array.from');
console.time('slice');
const slice2 = testArray.slice();
console.timeEnd('slice');
console.time('concat');
const concat2 = testArray.concat();
console.timeEnd('concat');
}
// Memory usage comparison
function memoryUsage() {
const original = {
largeArray: new Array(1000000).fill(0),
metadata: { created: new Date() },
};
// Shallow copy - shares the largeArray
const shallow = { ...original };
console.log(original.largeArray === shallow.largeArray); // true
// Memory efficient - array is not duplicated
}
Best Practices
1. Choose the Right Method
// For objects
// Use spread for simple cases
const copy1 = { ...original };
// Use Object.assign for dynamic property names
const dynamicKey = 'dynamic';
const copy2 = Object.assign({}, original, { [dynamicKey]: value });
// Use descriptor-aware copy for special properties
const copy3 = Object.create(
Object.getPrototypeOf(original),
Object.getOwnPropertyDescriptors(original)
);
// For arrays
// Use spread for simple copying
const arrCopy1 = [...originalArray];
// Use slice for partial copying
const arrCopy2 = originalArray.slice(start, end);
// Use Array.from for array-like objects
const arrCopy3 = Array.from(nodeList);
2. Document Shallow Copy Behavior
/**
* Updates user preferences (shallow merge)
* @param {Object} updates - New preferences to merge
* @returns {Object} Updated user object (new reference)
* @warning Nested objects are merged by reference
*/
function updateUserPreferences(user, updates) {
return {
...user,
preferences: {
...user.preferences,
...updates,
},
};
}
3. Test for Reference Equality
// Test helper for shallow copy verification
function isShallowCopy(original, copy) {
// Different references
if (original === copy) return false;
// Check all properties
for (let key in original) {
if (original.hasOwnProperty(key)) {
const origValue = original[key];
const copyValue = copy[key];
// Primitive values should be equal
if (typeof origValue !== 'object' || origValue === null) {
if (origValue !== copyValue) return false;
} else {
// Object references should be the same (shallow)
if (origValue !== copyValue) return false;
}
}
}
return true;
}
// Usage in tests
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
console.assert(isShallowCopy(original, copy), 'Should be shallow copy');
Conclusion
Shallow copying is a fundamental concept in JavaScript that:
- Creates new objects/arrays with copied top-level properties
- Shares references to nested objects and arrays
- Is memory efficient for large nested structures
- Is fast for creating copies
- Requires careful handling of nested data
Key takeaways:
- Use spread operator (
...
) for modern, concise shallow copying - Understand that nested objects are shared, not duplicated
- Choose the appropriate method based on your needs
- Consider deep copying when you need complete independence
- Always test your copy behavior with nested structures
Best practices:
- Default to spread operator for readability
- Document when shallow copy behavior matters
- Use immutable update patterns for state management
- Test reference equality when needed
- Consider performance for large objects
Master shallow copying to write more predictable and efficient JavaScript code!