JavaScript Object.assign(): Copying and Merging Objects
Master Object.assign() for copying and merging objects in JavaScript. Learn shallow copying, property merging, and common use cases with examples.
Object.assign() is a method that copies all enumerable own properties from one or more source objects to a target object. It returns the modified target object. This powerful method is essential for object manipulation, cloning, and merging in JavaScript.
Understanding Object.assign()
Object.assign() performs a shallow copy of properties from source objects to a target object, overwriting properties with the same keys.
Basic Syntax
// Syntax
Object.assign(target, ...sources);
// Basic example
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
const result = Object.assign(target, source);
console.log(target); // { a: 1, b: 3, c: 4 }
console.log(result); // { a: 1, b: 3, c: 4 }
console.log(result === target); // true (modifies target)
// Multiple sources
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { c: 3 };
const merged = Object.assign({}, obj1, obj2, obj3);
console.log(merged); // { a: 1, b: 2, c: 3 }
// Order matters - later sources overwrite earlier ones
const first = { name: 'John', age: 30 };
const second = { name: 'Jane', city: 'NYC' };
const third = { age: 25, country: 'USA' };
const combined = Object.assign({}, first, second, third);
console.log(combined);
// { name: 'Jane', age: 25, city: 'NYC', country: 'USA' }
Shallow Copying
Object.assign() creates a shallow copy, meaning nested objects are copied by reference.
Understanding Shallow Copy
// Shallow copy example
const original = {
name: 'John',
address: {
street: '123 Main St',
city: 'Boston',
},
hobbies: ['reading', 'gaming'],
};
const shallowCopy = Object.assign({}, original);
// Modifying primitive properties
shallowCopy.name = 'Jane';
console.log(original.name); // 'John' (unchanged)
console.log(shallowCopy.name); // 'Jane'
// Modifying nested objects
shallowCopy.address.city = 'New York';
console.log(original.address.city); // 'New York' (changed!)
console.log(shallowCopy.address.city); // 'New York'
// Modifying arrays
shallowCopy.hobbies.push('cooking');
console.log(original.hobbies); // ['reading', 'gaming', 'cooking'] (changed!)
// The nested objects are the same reference
console.log(original.address === shallowCopy.address); // true
console.log(original.hobbies === shallowCopy.hobbies); // true
Creating Independent Copies
// Method 1: Manual deep copy for known structure
function deepCopyPerson(person) {
return Object.assign({}, person, {
address: Object.assign({}, person.address),
hobbies: [...person.hobbies],
});
}
// Method 2: JSON serialization (limited)
function jsonDeepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
// Method 3: Recursive deep copy
function deepCopy(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map((item) => deepCopy(item));
}
const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepCopy(obj[key]);
}
}
return clonedObj;
}
// Usage
const original = {
name: 'John',
address: { city: 'Boston' },
hobbies: ['reading'],
};
const deepCopied = deepCopy(original);
deepCopied.address.city = 'LA';
deepCopied.hobbies.push('sports');
console.log(original.address.city); // 'Boston' (unchanged)
console.log(original.hobbies); // ['reading'] (unchanged)
Common Use Cases
1. Cloning Objects
// Simple object cloning
const user = { name: 'John', age: 30 };
const userClone = Object.assign({}, user);
// Using spread operator (modern alternative)
const userClone2 = { ...user };
// Cloning with modifications
const updatedUser = Object.assign({}, user, { age: 31, city: 'NYC' });
console.log(updatedUser); // { name: 'John', age: 31, city: 'NYC' }
// Function to clone with defaults
function cloneWithDefaults(obj, defaults) {
return Object.assign({}, defaults, obj);
}
const defaultSettings = { theme: 'light', fontSize: 16 };
const userSettings = { fontSize: 18 };
const finalSettings = cloneWithDefaults(userSettings, defaultSettings);
console.log(finalSettings); // { theme: 'light', fontSize: 18 }
2. Merging Objects
// Merging configuration objects
const defaultConfig = {
host: 'localhost',
port: 8080,
timeout: 5000,
retries: 3,
};
const userConfig = {
host: 'api.example.com',
timeout: 10000,
};
const environmentConfig = {
port: 443,
secure: true,
};
// Merge all configs (later sources override earlier ones)
const finalConfig = Object.assign(
{},
defaultConfig,
userConfig,
environmentConfig
);
console.log(finalConfig);
// {
// host: 'api.example.com',
// port: 443,
// timeout: 10000,
// retries: 3,
// secure: true
// }
// Conditional merging
function mergeConfigs(...configs) {
return configs.reduce((merged, config) => {
if (config && typeof config === 'object') {
return Object.assign(merged, config);
}
return merged;
}, {});
}
3. Creating Objects with Prototypes
// Creating object with specific prototype
const animalMethods = {
speak() {
console.log(`${this.name} makes a sound`);
},
move() {
console.log(`${this.name} moves`);
},
};
function createAnimal(name, species) {
return Object.assign(Object.create(animalMethods), {
name,
species,
});
}
const dog = createAnimal('Buddy', 'Dog');
dog.speak(); // 'Buddy makes a sound'
// Mixing in behaviors
const canFly = {
fly() {
console.log(`${this.name} flies in the sky`);
},
};
const canSwim = {
swim() {
console.log(`${this.name} swims in water`);
},
};
function createBird(name) {
return Object.assign(Object.create(animalMethods), canFly, {
name,
species: 'Bird',
});
}
function createDuck(name) {
return Object.assign(Object.create(animalMethods), canFly, canSwim, {
name,
species: 'Duck',
});
}
const duck = createDuck('Donald');
duck.fly(); // 'Donald flies in the sky'
duck.swim(); // 'Donald swims in water'
4. Options Pattern
// Function with options object
function createServer(options) {
const defaults = {
port: 3000,
host: 'localhost',
protocol: 'http',
middleware: [],
routes: {},
};
const config = Object.assign({}, defaults, options);
console.log(
`Starting ${config.protocol} server on ${config.host}:${config.port}`
);
return config;
}
// Usage with partial options
createServer({ port: 8080 });
// Starting http server on localhost:8080
createServer({
port: 443,
protocol: 'https',
middleware: ['auth', 'logging'],
});
// Starting https server on localhost:443
// Class with configurable options
class DataProcessor {
constructor(options = {}) {
const defaults = {
batchSize: 100,
timeout: 5000,
retries: 3,
async: true,
};
Object.assign(this, defaults, options);
}
process(data) {
console.log(
`Processing ${data.length} items with batch size ${this.batchSize}`
);
}
}
const processor = new DataProcessor({ batchSize: 50, retries: 5 });
Working with Different Data Types
Primitive Values as Sources
// Primitives are wrapped and their properties are copied
const target = {};
// String source
Object.assign(target, 'hello');
console.log(target); // { '0': 'h', '1': 'e', '2': 'l', '3': 'l', '4': 'o' }
// Number source (no enumerable properties)
const withNumber = Object.assign({}, 123);
console.log(withNumber); // {}
// Boolean source (no enumerable properties)
const withBoolean = Object.assign({}, true);
console.log(withBoolean); // {}
// null and undefined are ignored
const withNull = Object.assign({}, null, undefined, { a: 1 });
console.log(withNull); // { a: 1 }
// Mixed sources
const mixed = Object.assign(
{},
'hi', // Adds indices as properties
{ name: 'John' },
null, // Ignored
{ age: 30 }
);
console.log(mixed); // { '0': 'h', '1': 'i', name: 'John', age: 30 }
Arrays and Object.assign()
// Arrays are objects with numeric keys
const arr1 = ['a', 'b', 'c'];
const arr2 = ['d', 'e'];
const merged = Object.assign([], arr1, arr2);
console.log(merged); // ['d', 'e', 'c']
// Note: arr2 overwrites arr1 at indices 0 and 1
// Better array merging
const properMerge = [...arr1, ...arr2];
console.log(properMerge); // ['a', 'b', 'c', 'd', 'e']
// Converting array-like to array
const nodeList = document.querySelectorAll('div');
const divArray = Object.assign([], nodeList);
// Array with additional properties
const specialArray = Object.assign([], ['a', 'b'], {
customProp: 'value',
2: 'c', // Overwrites index 2
length: 5, // This could break array behavior!
});
console.log(specialArray); // ['a', 'b', 'c']
console.log(specialArray.customProp); // 'value'
Property Descriptors and Enumeration
Only Enumerable Properties Are Copied
const source = {};
// Enumerable property
Object.defineProperty(source, 'enumerable', {
value: 'This will be copied',
enumerable: true,
writable: true,
configurable: true,
});
// Non-enumerable property
Object.defineProperty(source, 'nonEnumerable', {
value: 'This will NOT be copied',
enumerable: false,
writable: true,
configurable: true,
});
// Symbol property
const sym = Symbol('mySymbol');
source[sym] = 'Symbol value';
const target = Object.assign({}, source);
console.log(target.enumerable); // 'This will be copied'
console.log(target.nonEnumerable); // undefined
console.log(target[sym]); // undefined
// To copy all properties including non-enumerable
function assignAll(target, source) {
Object.getOwnPropertyNames(source).forEach((key) => {
const descriptor = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, descriptor);
});
Object.getOwnPropertySymbols(source).forEach((sym) => {
const descriptor = Object.getOwnPropertyDescriptor(source, sym);
Object.defineProperty(target, sym, descriptor);
});
return target;
}
Getters and Setters
// Source with getter/setter
const source = {
_value: 42,
get value() {
console.log('Getting value');
return this._value;
},
set value(val) {
console.log('Setting value');
this._value = val;
},
};
// Object.assign executes getters
const target = Object.assign({}, source);
// Console: "Getting value"
console.log(target.value); // 42 (but it's a regular property, not a getter)
console.log(Object.getOwnPropertyDescriptor(target, 'value'));
// { value: 42, writable: true, enumerable: true, configurable: true }
// To preserve getters/setters
function assignWithDescriptors(target, source) {
Object.keys(source).forEach((key) => {
const descriptor = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, descriptor);
});
return target;
}
const properCopy = assignWithDescriptors({}, source);
console.log(Object.getOwnPropertyDescriptor(properCopy, 'value'));
// { get: [Function: get value], set: [Function: set value], enumerable: true, configurable: true }
Error Handling
Handling Errors During Assignment
// Object.assign stops on first error
const target = { existing: 'value' };
const problematicSource = {
get errorProp() {
throw new Error('Getter error');
},
normalProp: 'This would be copied',
};
try {
Object.assign(target, { a: 1 }, problematicSource, { b: 2 });
} catch (error) {
console.error('Assignment failed:', error.message);
}
console.log(target); // { existing: 'value', a: 1 }
// Only properties before the error were copied
// Safe assignment function
function safeAssign(target, ...sources) {
sources.forEach((source) => {
if (source != null) {
Object.keys(source).forEach((key) => {
try {
target[key] = source[key];
} catch (error) {
console.warn(`Failed to copy property ${key}:`, error);
}
});
}
});
return target;
}
// Type checking before assignment
function typedAssign(target, source, typeMap) {
Object.keys(source).forEach((key) => {
if (key in typeMap) {
const expectedType = typeMap[key];
const actualType = typeof source[key];
if (actualType !== expectedType) {
throw new TypeError(
`Property ${key} must be ${expectedType}, got ${actualType}`
);
}
}
target[key] = source[key];
});
return target;
}
// Usage
const schema = {
name: 'string',
age: 'number',
active: 'boolean',
};
const validData = { name: 'John', age: 30, active: true };
const result = typedAssign({}, validData, schema); // Success
Advanced Patterns
Mixin Pattern
// Multiple mixins
const EventEmitter = {
_events: {},
on(event, handler) {
this._events[event] = this._events[event] || [];
this._events[event].push(handler);
},
emit(event, data) {
if (this._events[event]) {
this._events[event].forEach((handler) => handler(data));
}
},
};
const Timestamped = {
timestamp() {
return new Date().toISOString();
},
age() {
return Date.now() - this.createdAt;
},
};
const Serializable = {
toJSON() {
return JSON.stringify(this);
},
fromJSON(json) {
return Object.assign(this, JSON.parse(json));
},
};
// Create object with multiple mixins
function createModel(data) {
return Object.assign(
{ createdAt: Date.now() },
EventEmitter,
Timestamped,
Serializable,
data
);
}
const user = createModel({ name: 'John', email: 'john@example.com' });
user.on('change', (data) => console.log('Changed:', data));
user.emit('change', { field: 'email' });
console.log(user.timestamp());
Functional Object Updates
// Immutable update pattern
const state = {
user: { name: 'John', age: 30 },
settings: { theme: 'dark' },
posts: [],
};
// Update functions
const updateUser = (state, updates) =>
Object.assign({}, state, {
user: Object.assign({}, state.user, updates),
});
const updateSettings = (state, settings) =>
Object.assign({}, state, { settings });
const addPost = (state, post) =>
Object.assign({}, state, {
posts: [...state.posts, post],
});
// Usage
let newState = updateUser(state, { age: 31 });
newState = updateSettings(newState, { theme: 'light', fontSize: 16 });
newState = addPost(newState, { id: 1, title: 'Hello World' });
console.log(state.user.age); // 30 (unchanged)
console.log(newState.user.age); // 31
// Generic immutable update
function immutableUpdate(obj, path, value) {
const keys = path.split('.');
const lastKey = keys.pop();
const deepClone = (obj, keys) => {
if (keys.length === 0) {
return Object.assign({}, obj, { [lastKey]: value });
}
const [head, ...tail] = keys;
return Object.assign({}, obj, {
[head]: deepClone(obj[head] || {}, tail),
});
};
return deepClone(obj, keys);
}
// Usage
const updated = immutableUpdate(state, 'user.address.city', 'Boston');
Factory Functions with Defaults
// Component factory with defaults
function createComponent(type, props = {}) {
const defaults = {
button: {
type: 'button',
className: 'btn',
disabled: false,
},
input: {
type: 'text',
className: 'input',
required: false,
placeholder: '',
},
select: {
className: 'select',
multiple: false,
options: [],
},
};
const componentDefaults = defaults[type] || {};
return Object.assign({ component: type }, componentDefaults, props);
}
// Usage
const button = createComponent('button', {
text: 'Click me',
onClick: () => console.log('Clicked'),
});
const input = createComponent('input', {
placeholder: 'Enter your name',
required: true,
});
// Configuration builder
class ConfigBuilder {
constructor() {
this.config = {};
}
merge(partial) {
Object.assign(this.config, partial);
return this;
}
defaults(defaults) {
this.config = Object.assign({}, defaults, this.config);
return this;
}
build() {
return Object.assign({}, this.config);
}
}
// Usage
const config = new ConfigBuilder()
.merge({ host: 'localhost' })
.merge({ port: 8080 })
.defaults({ protocol: 'http', timeout: 5000 })
.build();
Performance Considerations
// Object.assign vs spread operator
const obj = { a: 1, b: 2 };
// Similar performance for simple cases
const copy1 = Object.assign({}, obj);
const copy2 = { ...obj };
// Object.assign is faster for multiple sources
const merged1 = Object.assign({}, obj1, obj2, obj3, obj4);
const merged2 = { ...obj1, ...obj2, ...obj3, ...obj4 };
// For large objects, consider alternatives
function shallowClone(obj) {
const clone = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = obj[key];
}
}
return clone;
}
// Avoid repeated Object.assign in loops
// Bad
let result = {};
for (const item of items) {
result = Object.assign(result, item);
}
// Better
const result = Object.assign({}, ...items);
Best Practices
- Always use an empty object as target for cloning
// Good - creates new object
const clone = Object.assign({}, original);
// Bad - modifies original
const clone = Object.assign(original, { newProp: 'value' });
- Remember it's a shallow copy
// Be explicit about deep copying needs
const shallowCopy = Object.assign({}, complex);
const deepCopy = JSON.parse(JSON.stringify(complex)); // Limited but works
- Use spread syntax for simple cases
// Modern and concise
const copy = { ...original };
const merged = { ...defaults, ...userConfig };
- Handle null/undefined sources
function safeMerge(target, ...sources) {
return Object.assign(target, ...sources.filter(Boolean));
}
- Document mutation behavior
// Clear function name indicates mutation
function mergeInto(target, source) {
return Object.assign(target, source);
}
// Clear function name indicates new object
function mergeCreate(...sources) {
return Object.assign({}, ...sources);
}
Conclusion
Object.assign() is a fundamental method for object manipulation in JavaScript. While it only performs shallow copying, it's perfect for many common use cases like cloning objects, merging configurations, and implementing mixins. Understanding its behavior with different data types, property descriptors, and edge cases helps you use it effectively. For modern JavaScript, the spread operator often provides a cleaner syntax for simple cases, but Object.assign() remains valuable for dynamic property copying and when working with multiple source objects.