javascript
exercises
exercises.js⚡javascript
/**
* =====================================================
* 3.10 OPTIONAL CHAINING OPERATOR - EXERCISES
* =====================================================
* Practice safe property access with ?.
*/
/**
* Exercise 1: Basic Property Access
*
* Use optional chaining to safely access the user's city.
* Return "Unknown" if any part of the chain is missing.
*/
function getUserCity(user) {
// TODO: Return user.address.city safely, or "Unknown" if missing
// Hint: Use ?. and ??
}
// Test cases:
console.log('Exercise 1:');
console.log(getUserCity({ name: 'Alice', address: { city: 'NYC' } })); // "NYC"
console.log(getUserCity({ name: 'Bob', address: {} })); // "Unknown"
console.log(getUserCity({ name: 'Charlie' })); // "Unknown"
console.log(getUserCity(null)); // "Unknown"
/**
* Exercise 2: Bracket Notation Access
*
* Access a property dynamically using bracket notation with optional chaining.
* Return undefined if the path doesn't exist.
*/
function getPropertyByName(obj, propName) {
// TODO: Safely access obj[propName] and return its value
// Then access the 'data' property of that value if it exists
}
// Test cases:
console.log('\nExercise 2:');
const testObj = {
users: { data: [1, 2, 3] },
posts: { data: ['a', 'b'] },
};
console.log(getPropertyByName(testObj, 'users')); // { data: [1, 2, 3] }
console.log(getPropertyByName(testObj, 'comments')); // undefined
console.log(getPropertyByName(null, 'users')); // undefined
/**
* Exercise 3: Safe Method Calls
*
* Safely call the format method on the formatter object.
* If the method doesn't exist, return the original value.
*/
function formatValue(formatter, value) {
// TODO: Call formatter.format(value) if it exists
// Otherwise return the original value
}
// Test cases:
console.log('\nExercise 3:');
const upperFormatter = { format: (s) => s.toUpperCase() };
console.log(formatValue(upperFormatter, 'hello')); // "HELLO"
console.log(formatValue({}, 'hello')); // "hello"
console.log(formatValue(null, 'hello')); // "hello"
/**
* Exercise 4: Deep Object Access
*
* Extract specific data from a deeply nested API response.
* Use optional chaining throughout.
*/
function extractApiData(response) {
// TODO: Return an object with:
// - userId: from response.data.user.id (default: null)
// - userName: from response.data.user.profile.name (default: "Anonymous")
// - email: from response.data.user.contact.email (default: "No email")
// - firstRole: from response.data.user.roles[0] (default: "guest")
}
// Test cases:
console.log('\nExercise 4:');
const fullResponse = {
data: {
user: {
id: 123,
profile: { name: 'John Doe' },
contact: { email: 'john@example.com' },
roles: ['admin', 'editor'],
},
},
};
console.log(extractApiData(fullResponse));
// { userId: 123, userName: "John Doe", email: "john@example.com", firstRole: "admin" }
const partialResponse = {
data: {
user: {
id: 456,
},
},
};
console.log(extractApiData(partialResponse));
// { userId: 456, userName: "Anonymous", email: "No email", firstRole: "guest" }
console.log(extractApiData(null));
// { userId: null, userName: "Anonymous", email: "No email", firstRole: "guest" }
/**
* Exercise 5: Array Element Access
*
* Safely get items from potentially missing arrays.
*/
function getFirstAndLast(data) {
// TODO: Return an object with:
// - first: first element of data.items array (or null)
// - last: last element of data.items array (or null)
// Hint: For last element, you'll need to calculate the index
}
// Test cases:
console.log('\nExercise 5:');
console.log(getFirstAndLast({ items: ['a', 'b', 'c'] })); // { first: "a", last: "c" }
console.log(getFirstAndLast({ items: ['only'] })); // { first: "only", last: "only" }
console.log(getFirstAndLast({ items: [] })); // { first: null, last: null }
console.log(getFirstAndLast({})); // { first: null, last: null }
console.log(getFirstAndLast(null)); // { first: null, last: null }
/**
* Exercise 6: Callback Execution
*
* Create a function that safely executes optional callbacks.
*/
function processWithCallbacks(data, options) {
// TODO:
// 1. Call options.onStart() if it exists
// 2. Process data by converting to uppercase (if string)
// 3. Call options.onProcess(result) if it exists
// 4. Call options.onComplete(result) if it exists
// 5. Return the processed result
}
// Test cases:
console.log('\nExercise 6:');
const result1 = processWithCallbacks('hello', {
onStart: () => console.log('Starting...'),
onComplete: (r) => console.log('Done:', r),
});
console.log('Result:', result1); // Starting... Done: HELLO Result: HELLO
const result2 = processWithCallbacks('world', null);
console.log('Result:', result2); // Result: WORLD (no callbacks called)
/**
* Exercise 7: Configuration Merger
*
* Merge user config with defaults, using optional chaining
* to safely access nested properties.
*/
function mergeConfig(userConfig) {
const defaults = {
theme: 'light',
fontSize: 14,
sidebar: { visible: true, width: 250 },
notifications: { email: true, push: false },
};
// TODO: Return a merged config object
// User config values should override defaults when present
// Use optional chaining to safely access userConfig properties
}
// Test cases:
console.log('\nExercise 7:');
console.log(mergeConfig({ theme: 'dark' }));
// { theme: "dark", fontSize: 14, sidebar: { visible: true, width: 250 }, ... }
console.log(mergeConfig({ sidebar: { width: 300 } }));
// { theme: "light", fontSize: 14, sidebar: { visible: true, width: 300 }, ... }
console.log(mergeConfig(null));
// defaults object
console.log(mergeConfig(undefined));
// defaults object
/**
* Exercise 8: Safe JSON Path Access
*
* Create a function that accesses nested properties using a path string.
* Example: "user.profile.name" should access obj.user.profile.name
*/
function getByPath(obj, path) {
// TODO: Split path by '.' and access each property safely
// Return undefined if any part of the path is missing
// Hint: Use reduce with optional chaining
}
// Test cases:
console.log('\nExercise 8:');
const testData = {
user: {
profile: {
name: 'Alice',
settings: {
theme: 'dark',
},
},
},
};
console.log(getByPath(testData, 'user.profile.name')); // "Alice"
console.log(getByPath(testData, 'user.profile.settings.theme')); // "dark"
console.log(getByPath(testData, 'user.address.city')); // undefined
console.log(getByPath(null, 'user.name')); // undefined
/**
* Exercise 9: Event Handler System
*
* Create a mini event system with safe handler invocation.
*/
function createEventSystem() {
const handlers = {};
return {
// TODO: Implement on(event, handler) - registers a handler
on(event, handler) {},
// TODO: Implement off(event) - removes handler (safe if doesn't exist)
off(event) {},
// TODO: Implement emit(event, data) - calls handler safely
emit(event, data) {},
// TODO: Implement emitAll(data) - calls all handlers safely
emitAll(data) {},
};
}
// Test cases:
console.log('\nExercise 9:');
const events = createEventSystem();
events.on('click', (data) => console.log('Clicked:', data));
events.emit('click', { x: 100, y: 200 }); // Clicked: { x: 100, y: 200 }
events.emit('hover', { x: 50 }); // Nothing (no handler)
events.off('click');
events.emit('click', { x: 0 }); // Nothing (handler removed)
/**
* Exercise 10: Safe Array Operations
*
* Perform safe operations on arrays that might not exist.
*/
function safeArrayOps(data) {
// TODO: Return an object with:
// - length: length of data.items array (or 0)
// - sum: sum of data.items if they're numbers (or 0)
// - first: first item (or null)
// - mapped: items mapped to uppercase if strings (or empty array)
// Use optional chaining for all access
}
// Test cases:
console.log('\nExercise 10:');
console.log(safeArrayOps({ items: [1, 2, 3, 4, 5] }));
// { length: 5, sum: 15, first: 1, mapped: [1, 2, 3, 4, 5] }
console.log(safeArrayOps({ items: ['a', 'b', 'c'] }));
// { length: 3, sum: 0, first: "a", mapped: ["A", "B", "C"] }
console.log(safeArrayOps({ items: [] }));
// { length: 0, sum: 0, first: null, mapped: [] }
console.log(safeArrayOps(null));
// { length: 0, sum: 0, first: null, mapped: [] }
// =====================================================
// BONUS CHALLENGES
// =====================================================
/**
* Bonus 1: Optional Chaining vs Logical AND
*
* Demonstrate the difference between ?. and && with falsy values.
*/
function compareApproaches(obj) {
// TODO: Return an object showing the difference between
// && approach and ?. approach for accessing obj.value.toString()
// when obj.value is 0
}
console.log('\nBonus 1:');
console.log(compareApproaches({ value: 0 }));
// { andResult: 0, optionalResult: "0" }
/**
* Bonus 2: Chain Builder
*
* Create a utility that allows building safe property access chains.
*/
function createChain(obj) {
// TODO: Return an object that allows method chaining for safe access
// Example: createChain(obj).get('a').get('b').value()
// Should return obj?.a?.b or undefined
}
console.log('\nBonus 2:');
const chainData = { a: { b: { c: 42 } } };
// console.log(createChain(chainData).get('a').get('b').get('c').value()); // 42
// console.log(createChain(chainData).get('x').get('y').value()); // undefined
// =====================================================
// SOLUTIONS (Uncomment to check your answers)
// =====================================================
/*
// Exercise 1 Solution:
function getUserCity(user) {
return user?.address?.city ?? "Unknown";
}
// Exercise 2 Solution:
function getPropertyByName(obj, propName) {
return obj?.[propName];
}
// Exercise 3 Solution:
function formatValue(formatter, value) {
return formatter?.format?.(value) ?? value;
}
// Exercise 4 Solution:
function extractApiData(response) {
return {
userId: response?.data?.user?.id ?? null,
userName: response?.data?.user?.profile?.name ?? "Anonymous",
email: response?.data?.user?.contact?.email ?? "No email",
firstRole: response?.data?.user?.roles?.[0] ?? "guest"
};
}
// Exercise 5 Solution:
function getFirstAndLast(data) {
const items = data?.items;
const length = items?.length ?? 0;
return {
first: items?.[0] ?? null,
last: length > 0 ? items[length - 1] : null
};
}
// Exercise 6 Solution:
function processWithCallbacks(data, options) {
options?.onStart?.();
const result = typeof data === 'string' ? data.toUpperCase() : data;
options?.onProcess?.(result);
options?.onComplete?.(result);
return result;
}
// Exercise 7 Solution:
function mergeConfig(userConfig) {
const defaults = {
theme: "light",
fontSize: 14,
sidebar: { visible: true, width: 250 },
notifications: { email: true, push: false }
};
return {
theme: userConfig?.theme ?? defaults.theme,
fontSize: userConfig?.fontSize ?? defaults.fontSize,
sidebar: {
visible: userConfig?.sidebar?.visible ?? defaults.sidebar.visible,
width: userConfig?.sidebar?.width ?? defaults.sidebar.width
},
notifications: {
email: userConfig?.notifications?.email ?? defaults.notifications.email,
push: userConfig?.notifications?.push ?? defaults.notifications.push
}
};
}
// Exercise 8 Solution:
function getByPath(obj, path) {
return path.split('.').reduce((acc, key) => acc?.[key], obj);
}
// Exercise 9 Solution:
function createEventSystem() {
const handlers = {};
return {
on(event, handler) {
handlers[event] = handler;
},
off(event) {
delete handlers[event];
},
emit(event, data) {
handlers[event]?.(data);
},
emitAll(data) {
Object.values(handlers).forEach(handler => handler?.(data));
}
};
}
// Exercise 10 Solution:
function safeArrayOps(data) {
const items = data?.items ?? [];
return {
length: items?.length ?? 0,
sum: items?.reduce?.((a, b) => typeof b === 'number' ? a + b : a, 0) ?? 0,
first: items?.[0] ?? null,
mapped: items?.map?.(item => typeof item === 'string' ? item.toUpperCase() : item) ?? []
};
}
// Bonus 1 Solution:
function compareApproaches(obj) {
return {
andResult: obj && obj.value && obj.value.toString(),
optionalResult: obj?.value?.toString?.()
};
}
// Bonus 2 Solution:
function createChain(obj) {
let current = obj;
return {
get(prop) {
current = current?.[prop];
return this;
},
value() {
return current;
}
};
}
*/