JavaScript: Object
Keys, values, entries, copying and merging.
18 exercises
1 List the properties of an object
easyGiven { name: 'David Rayy', sclass: 'VI', rollno: 12 }, produce name,sclass,rollno.
Stuck? Three hints, smallest first
- 1.
Object.keysgives an array of the own, enumerable keys. - 2.
join(‘,’)turns that array into the string the question asks for. - 3.
for...inalso walks inherited keys, which is usually not what you want.
const student = { name: 'David Rayy', sclass: 'VI', rollno: 12 };
const listProperties = (obj) => Object.keys(obj).join(',');
// for...in walks the prototype chain too. Same answer here, different answer
// as soon as anything is inherited.
function listByForIn(obj) {
const keys = [];
for (const key in obj) keys.push(key);
return keys.join(',');
}
// Something with an inherited property, to show the difference.
const base = { fare: 4500 };
const trip = Object.create(base);
trip.plate = 'LAG-441-XA';
console.log('Object.keys :', listProperties(student));
console.log('for...in :', listByForIn(student));
console.log('--- with an inherited property ---');
console.log('Object.keys :', listProperties(trip), '<- own only');
console.log('for...in :', listByForIn(trip), '<- includes the inherited fare');
Output
10 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 10 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test("the source's expected output", listProperties(student), 'name,sclass,rollno');
test('the keys come out in insertion order', Object.keys(student), ['name', 'sclass', 'rollno']);
test('for...in agrees when nothing is inherited', listByForIn(student), listProperties(student));
test('an empty object gives an empty string', listProperties({}), '');
test('Object.keys sees only own properties', listProperties(trip), 'plate');
test('for...in also sees inherited ones', listByForIn(trip), 'plate,fare');
test('the inherited value is readable either way', trip.fare, 4500);
test('but it is not an own property', Object.hasOwn(trip, 'fare'), false);
test('while plate is', Object.hasOwn(trip, 'plate'), true);
test('number-like keys are sorted before string keys', Object.keys({ b: 1, 2: 2, a: 3, 1: 4 }), ['1', '2', 'b', 'a']);
Object.keys(obj).join(',') is the answer. The last assertion is the surprise worth carrying forward: object keys keep insertion order except for keys that look like array indexes, which are pulled to the front in numeric order. So { b: 1, 2: 2, a: 3, 1: 4 } enumerates as 1, 2, b, a. If order matters and your keys might be numeric, use a Map — it keeps strict insertion order for every key type.
Answers JS Object 1 in the original set.
2 Delete a property
easyRemove rollno from the student object, showing the object before and after. Then do it without modifying the original.
Stuck? Three hints, smallest first
- 1.
delete obj.keyremoves the property, and it returnstrueon success. - 2. Setting a property to
undefinedis not the same as deleting it: the key remains. - 3. Rest destructuring gives you a copy with the key left out and the original untouched.
const student = { name: 'David Rayy', sclass: 'VI', rollno: 12 };
console.log('before:', JSON.stringify(student));
// Non-destructive: pull the key out into its own variable and keep the rest.
const { rollno, ...withoutRollno } = student;
console.log('copy without rollno:', JSON.stringify(withoutRollno));
console.log('original still intact:', JSON.stringify(student));
// The difference between deleting and blanking.
const blanked = { ...student, rollno: undefined };
console.log('set to undefined:', JSON.stringify(blanked), '<- JSON drops it...');
console.log('...but the key is still there:', Object.keys(blanked));
// Destructive, as the question asks.
const removed = delete student.rollno;
console.log('after delete:', JSON.stringify(student), ' delete returned', removed);
Output
11 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 11 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('the copy has the key removed', Object.keys(withoutRollno), ['name', 'sclass']);
test('the extracted value is available', rollno, 12);
test('delete reports success', removed, true);
test('the key is gone from the original', Object.hasOwn(student, 'rollno'), false);
test('reading it now gives undefined', student.rollno, undefined);
test('the other properties survive', student, { name: 'David Rayy', sclass: 'VI' });
test('deleting something absent also returns true', delete student.nothing, true);
test('setting to undefined KEEPS the key', Object.hasOwn(blanked, 'rollno'), true);
test('so the key count differs', Object.keys(blanked).length - Object.keys(withoutRollno).length, 1);
test('though JSON hides the difference', JSON.stringify(blanked), JSON.stringify(withoutRollno));
test('"in" still finds a key set to undefined', 'rollno' in blanked, true);
Two ways to make a property go away, and they are not equivalent. delete removes the key; assigning undefined leaves the key present with an empty value. The assertions show the trap clearly: JSON.stringify produces identical output for both, so serialising the two objects makes them look the same while Object.keys and in still tell them apart. Prefer the rest-destructuring form when you can — it leaves the input alone, and code that does not mutate its inputs is easier to reason about.
Answers JS Object 2 in the original set.
3 How many properties does an object have?
easyObjects have no length. Count their properties, and be clear about which properties you are counting.
Stuck? Three hints, smallest first
- 1.
Object.keys(obj).lengthis the usual answer. - 2. It counts own enumerable string keys only — not inherited, not symbols, not hidden ones.
- 3.
Object.getOwnPropertyNamesincludes non-enumerable keys;Reflect.ownKeysincludes symbols too.
const student = { name: 'David Rayy', sclass: 'VI', rollno: 12 };
const size = (obj) => Object.keys(obj).length;
// An object with one of each kind of property, to show what each count includes.
const mixed = { visible: 1 };
Object.defineProperty(mixed, 'hidden', { value: 2, enumerable: false });
mixed[Symbol('tag')] = 3;
const inheriting = Object.create(mixed);
inheriting.own = 4;
console.log('size(student):', size(student), ' (objects have no .length:', student.length, ')');
console.log('--- the three levels of counting ---');
console.log('Object.keys :', Object.keys(mixed), '-> enumerable string keys');
console.log('Object.getOwnPropertyNames:', Object.getOwnPropertyNames(mixed), '-> plus non-enumerable');
console.log('Reflect.ownKeys :', Reflect.ownKeys(mixed).map(String), '-> plus symbols');
console.log('a Map does have .size :', new Map([['a', 1]]).size);
Output
13 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 13 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test("the source's object has 3 properties", size(student), 3);
test('an object has no length property', student.length, undefined);
test('an empty object has 0', size({}), 0);
test('keys counts only enumerable string keys', Object.keys(mixed).length, 1);
test('getOwnPropertyNames also counts the hidden one', Object.getOwnPropertyNames(mixed).length, 2);
test('ownKeys also counts the symbol', Reflect.ownKeys(mixed).length, 3);
test('the hidden property is still readable', mixed.hidden, 2);
test('it is just not enumerable', Object.keys(mixed).includes('hidden'), false);
test('inherited properties are not counted', size(inheriting), 1);
test('even though they are readable', inheriting.visible, 1);
test('JSON.stringify also skips the hidden one', JSON.stringify(mixed), '{"visible":1}');
test('a Map reports its size directly', new Map([['a', 1], ['b', 2]]).size, 2);
test('and counts every key type', new Map([[1, 'a'], ['1', 'b']]).size, 2);
Object.keys(obj).length is right, provided you know it counts own enumerable string keys and nothing else. The three-way comparison above is the point: a non-enumerable property is fully readable but invisible to Object.keys, to for...in and to JSON.stringify. That last assertion about Map is a reminder too — new Map([[1, 'a'], ['1', 'b']]) has size 2, because a Map keeps 1 and '1' apart, while a plain object would convert both keys to the string '1' and keep only one.
Answers JS Object 3 in the original set.
4 Report the reading status of each book
easyGiven a list of books with author, title and readingStatus, produce a readable line for each one.
Stuck? Three hints, smallest first
- 1.
mapturns each object into a string;joinputs the lines together. - 2. Do not print a raw boolean at a reader. Turn
trueinto words. - 3. Destructuring in the parameter list keeps the callback short.
const library = [
{ author: 'Bill Gates', title: 'The Road Ahead', readingStatus: true },
{ author: 'Steve Jobs', title: 'Walter Isaacson', readingStatus: true },
{ author: 'Suzanne Collins', title: 'Mockingjay: The Final Book of The Hunger Games', readingStatus: false },
];
const describe = ({ title, author, readingStatus }) =>
`${title} by ${author} — ${readingStatus ? 'already read' : 'not read yet'}`;
const report = (books) => books.map(describe);
// Split the list into the two groups.
const read = library.filter((b) => b.readingStatus);
const unread = library.filter((b) => !b.readingStatus);
for (const line of report(library)) console.log(line);
console.log('---');
console.log('read:', read.length, ' unread:', unread.length);
console.log('titles read:', read.map((b) => b.title));
Output
10 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 10 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('there is one line per book', report(library).length, 3);
test('the first line reads properly', report(library)[0], 'The Road Ahead by Bill Gates — already read');
test('an unread book says so', report(library)[2].endsWith('not read yet'), true);
test('no line contains a raw boolean', report(library).some((l) => l.includes('true') || l.includes('false')), false);
test('two books are read', read.length, 2);
test('one is not', unread.length, 1);
test('the groups account for every book', read.length + unread.length, library.length);
test('filtering does not change the source list', library.length, 3);
test('an empty library gives an empty report', report([]), []);
test('every line mentions its title', library.every((b, i) => report(library)[i].includes(b.title)), true);
Nothing subtle here, which makes it a good place to state a rule: never show a raw true or false to a reader. The assertion that no line contains the text "true" enforces it. The em dash in the template is a deliberate separator too — using a plain hyphen would be ambiguous in a title that already contains one.
Answers JS Object 4 in the original set.
5 Volume of a cylinder, using a class
easyBuild a cylinder class and report its volume, π r² h, to four decimal places.
Stuck? Three hints, smallest first
- 1.
toFixed(4)returns a string, not a number. - 2. Keep the exact value on the object and format only when displaying it.
- 3. A getter lets
volumebe read like a property while still being computed.
class Cylinder {
constructor(radius, height) {
if (radius <= 0 || height <= 0) throw new RangeError('radius and height must be positive');
this.radius = radius;
this.height = height;
}
// Computed on read, so it stays correct if radius or height changes.
get volume() {
return Math.PI * this.radius ** 2 * this.height;
}
get surfaceArea() {
return 2 * Math.PI * this.radius * (this.radius + this.height);
}
// Formatting is a separate job from calculating.
toFixed(places = 4) {
return this.volume.toFixed(places);
}
}
const tank = new Cylinder(4, 10);
const refused = (() => { try { new Cylinder(0, 5); return 'no error'; } catch (e) { return e.constructor.name; } })();
console.log('radius 4, height 10');
console.log('exact volume :', tank.volume);
console.log('to 4 decimals :', tank.toFixed());
console.log('surface area :', tank.surfaceArea.toFixed(4));
console.log('negative input:', refused);
console.log('--- a getter recomputes, so changing the radius updates the volume ---');
tank.radius = 2;
console.log('radius now 2 :', tank.toFixed());
Output
11 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 11 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('the volume is right to four places', new Cylinder(4, 10).toFixed(), '502.6548');
test('a smaller one', new Cylinder(2, 5).toFixed(), '62.8319');
test('toFixed returns a string, not a number', typeof new Cylinder(4, 10).toFixed(), 'string');
test('the underlying value is a number', typeof new Cylinder(4, 10).volume, 'number');
test('and it is not rounded', new Cylinder(4, 10).volume, Math.PI * 16 * 10);
test('doubling the radius quadruples the volume', new Cylinder(4, 10).volume / new Cylinder(2, 10).volume, 4);
test('doubling the height only doubles it', new Cylinder(2, 10).volume / new Cylinder(2, 5).volume, 2);
test('a zero dimension is refused', refused, 'RangeError');
test('the getter recomputes after a change', tank.toFixed(), '125.6637');
test('surface area of the changed one', tank.surfaceArea.toFixed(4), '150.7964');
test('volume is not stored as an own property', Object.hasOwn(new Cylinder(1, 1), 'volume'), false);
Two things worth keeping. toFixed returns a string — the assertion checks that, because value.toFixed(2) + 1 concatenating instead of adding is a common surprise. And using a get volume() rather than computing it once in the constructor means the value cannot go stale: the assertion after tank.radius = 2 proves the volume followed the change. A value stored in the constructor would still report the old figure.
Answers JS Object 5 in the original set.
6 Bubble sort
fairSort [6, 4, 0, 3, -2, 1] into [-2, 0, 1, 3, 4, 6] by repeatedly swapping neighbours that are in the wrong order.
Stuck? Three hints, smallest first
- 1. Each full pass moves the largest remaining value to the end, so the tail is already sorted.
- 2. Stop early when a pass makes no swaps: the list is sorted and further passes are wasted work.
- 3. Shrink the inner loop by one each pass, since the end is already correct.
function bubbleSort(input) {
const values = input.slice(); // do not modify the caller's array
let passes = 0;
for (let end = values.length - 1; end > 0; end--) {
let swapped = false;
for (let i = 0; i < end; i++) { // end shrinks: the tail is done
if (values[i] > values[i + 1]) {
[values[i], values[i + 1]] = [values[i + 1], values[i]];
swapped = true;
}
}
passes++;
if (!swapped) break; // already sorted, stop
}
return { values, passes };
}
const sort = (input) => bubbleSort(input).values;
const sample = [6, 4, 0, 3, -2, 1];
console.log('input :', sample);
console.log('sorted:', sort(sample));
console.log('input afterwards:', sample, '<- unchanged');
console.log('--- the early exit earns its place ---');
console.log('passes for already-sorted [1,2,3,4,5]:', bubbleSort([1, 2, 3, 4, 5]).passes);
console.log('passes for reversed [5,4,3,2,1]:', bubbleSort([5, 4, 3, 2, 1]).passes);
Output
11 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 11 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test("the source's sample", sort(sample), [-2, 0, 1, 3, 4, 6]);
test('the input is not modified', sample, [6, 4, 0, 3, -2, 1]);
test('an empty array', sort([]), []);
test('one element', sort([1]), [1]);
test('already sorted stays sorted', sort([1, 2, 3]), [1, 2, 3]);
test('reversed gets fixed', sort([3, 2, 1]), [1, 2, 3]);
test('duplicates are kept', sort([2, 1, 2, 1]), [1, 1, 2, 2]);
test('it agrees with a numeric sort()', sort([10, 9, 100, 1]), [10, 9, 100, 1].slice().sort((a, b) => a - b));
test('a sorted list needs only one pass', bubbleSort([1, 2, 3, 4, 5]).passes, 1);
test('a reversed list needs the full n-1', bubbleSort([5, 4, 3, 2, 1]).passes, 4);
test('so the early exit really does save work', bubbleSort([1, 2, 3, 4, 5]).passes < bubbleSort([5, 4, 3, 2, 1]).passes, true);
Bubble sort is O(n²) and you should not ship it, but the two optimisations are worth writing once. Shrinking the inner loop each pass is correct because a full pass guarantees the largest remaining value has reached the end. And the swapped flag turns an already-sorted input into a single pass instead of n−1 — the assertions compare 1 against 4 to prove it. That makes it O(n) on sorted data, which is the one thing bubble sort is genuinely good at.
Answers JS Object 6 in the original set.
7 Every substring of a string
fair'dog' should give ["d", "do", "dog", "o", "og", "g"]. Work out from that ordering exactly what is being generated.
Stuck? Three hints, smallest first
- 1. Read the expected output: it is every contiguous run, grouped by starting position.
- 2. Two nested loops — outer for the start, inner for the end.
- 3. This is not the same as a subset or a subsequence.
"dg"is absent for a reason.
// Every contiguous run, ordered by start then by length. That is exactly the
// ordering the source's expected output shows.
function substrings(text) {
const out = [];
for (let start = 0; start < text.length; start++) {
for (let end = start + 1; end <= text.length; end++) {
out.push(text.slice(start, end));
}
}
return out;
}
// A subsequence keeps order but allows gaps, so 'dg' IS included. Different
// question, and the source's expected output rules it out.
function subsequences(text) {
if (text.length === 0) return [''];
const [first, ...rest] = text;
const without = subsequences(rest.join(''));
return [...without, ...without.map((s) => first + s)].filter((s) => s !== '');
}
console.log("substrings('dog') :", substrings('dog'));
console.log("subsequences('dog'):", subsequences('dog').sort(), '<- note "dg"');
console.log('count for a 4-letter word:', substrings('bus').length, 'vs', substrings('trip').length);
console.log('n(n+1)/2 for n=4:', (4 * 5) / 2);
Output
12 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 12 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test("the source's expected output, exactly", substrings('dog'), ['d', 'do', 'dog', 'o', 'og', 'g']);
test('a single letter', substrings('a'), ['a']);
test('the empty string gives nothing', substrings(''), []);
test('two letters give three substrings', substrings('ab'), ['a', 'ab', 'b']);
test('the count is n(n+1)/2', substrings('trip').length, (4 * 5) / 2);
test('and for a longer word', substrings('sixgun').length, (6 * 7) / 2);
test('the whole string is always included', substrings('dog').includes('dog'), true);
test('every single letter is included', [...'dog'].every((c) => substrings('dog').includes(c)), true);
test('nothing has a gap in it', substrings('dog').includes('dg'), false);
test('whereas a subsequence does', subsequences('dog').includes('dg'), true);
test('every substring is a real slice of the input', substrings('dog').every((s) => 'dog'.includes(s)), true);
test('duplicated letters produce duplicated substrings', substrings('aa'), ['a', 'aa', 'a']);
The source calls these "subsets", which is the wrong word, and its own expected output proves it: a subset of the letters of 'dog' would include 'dg', and that is not in the list. What is being asked for is every contiguous substring, which is why two nested loops are enough — there are exactly n(n+1)/2 of them, and the assertions check that count. Both are implemented above so the difference is visible. The last assertion is worth noting too: 'aa' yields 'a' twice, because these are positions, not distinct values.
Answers JS Object 7 in the original set.
8 A clock that ticks every second
hardPrint the time as HH:MM:SS once a second. Build it so the ticking can be tested without waiting.
Stuck? Three hints, smallest first
- 1.
padStart(2, ‘0’)is what turns9into09. - 2. Take the timer and the clock as arguments instead of reaching for the globals.
- 3. A test can then drive time forward instantly, and the same code still works in a browser.
// ---- the stand-in ----------------------------------------------------------
// The build has no second to wait, so time is injected. makeClock is the fake;
// the real setInterval and Date are what you would pass in a browser.
function makeClock(startSeconds = 0) {
let now = startSeconds;
let jobs = [];
return {
now: () => now,
setInterval(fn, ms) {
const job = { fn, ms, next: now + ms / 1000 };
jobs.push(job);
return job;
},
clearInterval(job) { jobs = jobs.filter((j) => j !== job); },
// Drive time forward and fire whatever is due.
advance(seconds) {
const target = now + seconds;
while (true) {
const due = jobs.filter((j) => j.next <= target).sort((a, b) => a.next - b.next)[0];
if (!due) break;
now = due.next;
due.next += due.ms / 1000;
due.fn();
}
now = target;
},
};
}
// ---- the answer ------------------------------------------------------------
const two = (n) => String(n).padStart(2, '0');
const formatTime = (totalSeconds) => {
const s = ((totalSeconds % 86400) + 86400) % 86400; // wrap at midnight
return [Math.floor(s / 3600), Math.floor(s / 60) % 60, s % 60].map(two).join(':');
};
function startClock(clock, onTick) {
const handle = clock.setInterval(() => onTick(formatTime(clock.now())), 1000);
return () => clock.clearInterval(handle);
}
// In a browser this line is the whole thing:
// setInterval(() => console.log(new Date().toTimeString().slice(0, 8)), 1000);
// 14:37:42 is 52662 seconds past midnight, matching the source's sample output.
const clock = makeClock(14 * 3600 + 37 * 60 + 42);
const ticks = [];
const stop = startClock(clock, (t) => ticks.push(t));
clock.advance(6); // six seconds, instantly
stop();
clock.advance(10); // nothing more should arrive
for (const t of ticks) console.log(t);
Output
11 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 11 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test("it matches the source's sample output", ticks, ['14:37:43', '14:37:44', '14:37:45', '14:37:46', '14:37:47', '14:37:48']);
test('six seconds gave six ticks', ticks.length, 6);
test('stopping it really stops it', (() => { const before = ticks.length; clock.advance(30); return ticks.length - before; })(), 0);
test('single digits are padded', formatTime(9 * 3600 + 5 * 60 + 3), '09:05:03');
test('midnight', formatTime(0), '00:00:00');
test('one second before midnight', formatTime(86399), '23:59:59');
test('it wraps at midnight instead of reading 24:00:00', formatTime(86400), '00:00:00');
test('and keeps wrapping', formatTime(86400 + 61), '00:01:01');
test('every tick has the right shape', ticks.every((t) => /^\d{2}:\d{2}:\d{2}$/.test(t)), true);
test('the ticks are one second apart', ticks.every((t, i) => i === 0 || Number(t.slice(-2)) === Number(ticks[i - 1].slice(-2)) + 1), true);
test('no real time was spent', true, true);
Adapted. The original prints for as long as you leave it running, which nothing can verify. Here the timer and the current time are arguments, so the test substitutes a clock it can fast-forward: six simulated seconds pass instantly and the six expected lines are asserted against the source's own sample output. The real browser version is the commented line — one setInterval call.
Injecting the clock is not a testing trick, it is better design. The formatting is now a pure function of a number, so formatTime can be checked at midnight and at 23:59:59 without arranging for the test to run at those times. The wrap assertion matters: without % 86400 a clock counting past midnight reads 24:00:00.
Answers JS Object 8 in the original set.
9 Circle area and perimeter
easyMake a circle with two methods, one for area and one for perimeter, taking the radius from the caller.
Stuck? Three hints, smallest first
- 1. Area is
π r²and perimeter is2 π r. - 2. Validate the radius in the constructor, so a bad object cannot exist at all.
- 3. The ratio of area to perimeter is
r / 2, which is a free way to check both at once.
class Circle {
constructor(radius) {
if (!Number.isFinite(radius) || radius <= 0) {
throw new RangeError('radius must be a positive, finite number');
}
this.radius = radius;
}
area() { return Math.PI * this.radius ** 2; }
perimeter() { return 2 * Math.PI * this.radius; }
// Formatting kept apart from the maths.
report(places = 4) {
return `r=${this.radius} area=${this.area().toFixed(places)} perimeter=${this.perimeter().toFixed(places)}`;
}
}
const bad = (value) => {
try { new Circle(value); return 'accepted'; } catch (e) { return e.constructor.name; }
};
// Comparing computed floats needs a tolerance, not ===. See the note.
const close = (a, b, tolerance = 1e-12) => Math.abs(a - b) < tolerance;
const c = new Circle(3);
console.log(c.report());
console.log('area :', c.area());
console.log('perimeter :', c.perimeter());
console.log('--- area / perimeter should be exactly r / 2 ---');
for (const r of [3, 7]) {
const circle = new Circle(r);
const ratio = circle.area() / circle.perimeter();
console.log(`r=${r} ratio=${ratio} r/2=${r / 2} exactly equal? ${ratio === r / 2}`);
}
console.log('<- true for r=3, false for r=7, and the maths is identical');
console.log('--- rejected inputs ---');
for (const v of [0, -1, NaN, Infinity, '3']) console.log(String(v).padEnd(10), bad(v));
Output
17 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 17 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('area of radius 3, to four places', c.area().toFixed(4), '28.2743');
test('perimeter of radius 3', c.perimeter().toFixed(4), '18.8496');
test('radius 1 has area pi', new Circle(1).area(), Math.PI);
test('and perimeter 2 pi', new Circle(1).perimeter(), 2 * Math.PI);
test('area over perimeter is r/2', close(c.area() / c.perimeter(), c.radius / 2), true);
test('and for a different radius too', close(new Circle(7).area() / new Circle(7).perimeter(), 3.5), true);
test('but at r=7 it is not EXACTLY 3.5', new Circle(7).area() / new Circle(7).perimeter() === 3.5, false);
test('it is off by a fraction too small to matter', new Circle(7).area() / new Circle(7).perimeter(), 3.4999999999999996);
test('while at r=3 the same maths lands exactly', new Circle(3).area() / new Circle(3).perimeter() === 1.5, true);
test('doubling the radius quadruples the area', new Circle(6).area() / new Circle(3).area(), 4);
test('but only doubles the perimeter', new Circle(6).perimeter() / new Circle(3).perimeter(), 2);
test('a zero radius is refused', bad(0), 'RangeError');
test('a negative radius is refused', bad(-1), 'RangeError');
test('NaN is refused', bad(NaN), 'RangeError');
test('Infinity is refused', bad(Infinity), 'RangeError');
test('a numeric string is refused too', bad('3'), 'RangeError');
test('the report reads correctly', c.report(2), 'r=3 area=28.27 perimeter=18.85');
The constructor guard uses Number.isFinite rather than typeof === 'number', so NaN and Infinity are rejected. Both would otherwise produce a circle whose area is NaN, and NaN spreads silently through every later calculation. Refusing the string '3' is deliberate too: accepting it would mean new Circle('3') works while new Circle('3km') quietly produces NaN.
area / perimeter should be exactly r / 2, and it is a good check because it relates the two answers to each other without restating either formula. It is also where this exercise bites. At r = 3 the ratio is exactly 1.5. At r = 7 the identical arithmetic gives 3.4999999999999996, and the assertions above pin down both results.
Nothing is wrong with the code. π × 49 and 2 × π × 7 each round to the nearest representable double, and the two roundings do not cancel. This is why computed floating-point values are compared with a tolerance — Math.abs(a - b) < 1e-12 — and not with ===. The one place to avoid the problem entirely is money: count in kobo, as integers.
Answers JS Object 9 in the original set.
10 Sort an array of objects
fairSort a list of books. The source's own expected output is inconsistent, so part of this exercise is working out what it must have meant.
Stuck? Three hints, smallest first
- 1.
sortmodifies the array in place and also returns it. Copy first. - 2. Subtract for numbers; use
localeComparefor text. - 3. Look at the order of the IDs in the expected output: 4264, 3245, 1254.
const library = [
{ title: 'The Road Ahead', author: 'Bill Gates', libraryID: 1254 },
{ title: 'Walter Isaacson', author: 'Steve Jobs', libraryID: 4264 },
{ title: 'Mockingjay: The Final Book of The Hunger Games', author: 'Suzanne Collins', libraryID: 3245 },
];
// Copy first: sort() would reorder the caller's array.
const byNumber = (list, key) => list.slice().sort((a, b) => a[key] - b[key]);
const byNumberDesc = (list, key) => list.slice().sort((a, b) => b[key] - a[key]);
// Text needs localeCompare, and it needs an explicit locale or the result
// depends on the machine running it.
const byText = (list, key) => list.slice().sort((a, b) => a[key].localeCompare(b[key], 'en'));
console.log('by ID ascending :', byNumber(library, 'libraryID').map((b) => b.libraryID));
console.log('by ID descending:', byNumberDesc(library, 'libraryID').map((b) => b.libraryID));
console.log("<- descending is the order in the source's expected output");
console.log('by author :', byText(library, 'author').map((b) => b.author));
console.log('original untouched:', library.map((b) => b.libraryID));
console.log('--- and the reason to pass a comparator at all ---');
console.log('IDs sorted with no comparator:', [1254, 4264, 3245].slice().sort(), '<- text order');
Output
11 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 11 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('ascending by ID', byNumber(library, 'libraryID').map((b) => b.libraryID), [1254, 3245, 4264]);
test("descending, which matches the source's order", byNumberDesc(library, 'libraryID').map((b) => b.libraryID), [4264, 3245, 1254]);
test('the original list is not reordered', library.map((b) => b.libraryID), [1254, 4264, 3245]);
test('sorting by author', byText(library, 'author').map((b) => b.author), ['Bill Gates', 'Steve Jobs', 'Suzanne Collins']);
test('sorting by title', byText(library, 'title')[0].title, 'Mockingjay: The Final Book of The Hunger Games');
test('an empty list sorts to an empty list', byNumber([], 'libraryID'), []);
test('one item is already sorted', byNumber([library[0]], 'libraryID').length, 1);
test('ascending and descending are exact reverses', byNumber(library, 'libraryID').map((b) => b.libraryID), byNumberDesc(library, 'libraryID').map((b) => b.libraryID).reverse());
test('sort() without a comparator compares as text', [1254, 4264, 3245].slice().sort(), [1254, 3245, 4264]);
test('which goes badly wrong on mixed magnitudes', [10, 9, 100].slice().sort(), [10, 100, 9]);
test('the sorted list holds the same objects, not copies', byNumber(library, 'libraryID')[0] === library[0], true);
The source's expected output has author: "Walter Isaacson", title: "Steve Jobs" — the two fields swapped, and the same again in the third entry. So it cannot be taken literally. What it does show reliably is the ID order: 4264, 3245, 1254, which is descending by libraryID, and that is what the assertion checks.
Two habits to keep from this. sort() mutates, so list.slice().sort(...) unless you actually want the original reordered. And with no comparator, sort() converts everything to strings — the assertion showing [10, 9, 100] becoming [10, 100, 9] is not a contrived example, it is what happens to any list of numbers of differing lengths.
Answers JS Object 10 in the original set.
11 List the methods of an object
fairList the function-valued properties of an object. The source's expected list for Array includes names that no longer exist, so check rather than trust.
Stuck? Three hints, smallest first
- 1.
Object.getOwnPropertyNamesreaches non-enumerable properties;Object.keysdoes not, and built-ins are non-enumerable. - 2. Filter on
typeof obj[key] === ‘function’. - 3. Reading some inherited properties of a function throws in strict mode, so guard the read.
function methodsOf(obj) {
return Object.getOwnPropertyNames(obj).filter((key) => {
// Some properties are accessors that throw when read (fn.caller in strict
// mode). A plain typeof check would blow up on them.
try { return typeof obj[key] === 'function'; } catch { return false; }
});
}
// Own methods only misses inherited ones, which is usually what you want to see.
function allMethodsOf(obj) {
const found = new Set();
for (let o = obj; o && o !== Object.prototype; o = Object.getPrototypeOf(o)) {
for (const key of methodsOf(o)) found.add(key);
}
return [...found];
}
class Bus {
constructor(plate) { this.plate = plate; }
board() {}
depart() {}
}
console.log('own property names of Array:', Object.getOwnPropertyNames(Array));
console.log('methods of Array :', methodsOf(Array));
console.log("--- the source's list is from an older engine ---");
for (const name of ['isArray', 'from', 'of', 'observe', 'unobserve', 'arguments', 'caller']) {
console.log(String(name).padEnd(12), 'own property of Array?', Object.getOwnPropertyNames(Array).includes(name));
}
console.log('--- a class puts its methods on the prototype ---');
console.log('own methods of an instance :', methodsOf(new Bus('LAG-441-XA')));
console.log('methods on the prototype :', methodsOf(Bus.prototype));
console.log('walking the chain :', allMethodsOf(new Bus('LAG-441-XA')));
Output
15 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 15 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('isArray is a method of Array', methodsOf(Array).includes('isArray'), true);
test('so is from', methodsOf(Array).includes('from'), true);
test('and of', methodsOf(Array).includes('of'), true);
test('observe no longer exists', Object.getOwnPropertyNames(Array).includes('observe'), false);
test('nor does unobserve', Object.getOwnPropertyNames(Array).includes('unobserve'), false);
test('arguments is not an own property of Array', Object.getOwnPropertyNames(Array).includes('arguments'), false);
test('nor is caller', Object.getOwnPropertyNames(Array).includes('caller'), false);
test('length and name are properties but not methods', methodsOf(Array).includes('length') || methodsOf(Array).includes('name'), false);
test('prototype is not a function either', methodsOf(Array).includes('prototype'), false);
test('Object.keys finds nothing, because built-ins are non-enumerable', Object.keys(Array).length, 0);
test('an instance has no own methods', methodsOf(new Bus('X')), []);
test('they live on the prototype', methodsOf(Bus.prototype).sort(), ['board', 'constructor', 'depart']);
test('walking the chain finds them from the instance', allMethodsOf(new Bus('X')).includes('board'), true);
test('a plain object with a function property', methodsOf({ go() {}, plate: 'X' }), ['go']);
test('an empty object has none', methodsOf({}), []);
The source expects ["length", "name", "arguments", "caller", "prototype", "isArray", "observe", "unobserve"]. Today's answer is ["length", "name", "prototype", "isArray", "from", "of", ...]. Array.observe and Object.observe were an experiment that was withdrawn and never standardised; arguments and caller are no longer own properties of functions. The assertions check for their absence, so this exercise documents the change rather than repeating a stale list.
Two mechanics matter. Object.keys(Array) returns nothing at all, because every built-in method is non-enumerable — that is why getOwnPropertyNames is required here. And class methods are not own properties of instances; they sit on the prototype, which is why methodsOf(new Bus(...)) is empty and the chain has to be walked.
Answers JS Object 11 in the original set.
12 Parse a URL
fairBreak a URL into its parts. Use the built-in parser rather than a regular expression, and handle a bad URL without throwing at the caller.
Stuck? Three hints, smallest first
- 1.
new URL(str)does the whole job, in Node and in browsers. - 2. It throws on an invalid URL, so wrap it or use
URL.canParse. - 3.
searchParamshandles the query string, including repeated keys.
function parseUrl(input) {
let url;
try { url = new URL(input); } catch { return null; } // invalid: say so, do not throw
return {
protocol: url.protocol, // includes the ':'
hostname: url.hostname, // no port
host: url.host, // with port, if any
port: url.port, // '' when default for the protocol
pathname: url.pathname,
search: url.search,
hash: url.hash,
query: Object.fromEntries(url.searchParams),
};
}
const sample = 'https://www.example.com:8080/routes/lagos?from=Ojota&to=Abuja#seats';
const parsed = parseUrl(sample);
console.log(parsed);
console.log('--- defaults and edge cases ---');
console.log("port when default :", JSON.stringify(parseUrl('https://example.com/').port), '<- empty, not 443');
console.log('pathname when absent:', JSON.stringify(parseUrl('https://example.com').pathname), '<- always at least /');
console.log('invalid input :', parseUrl('not a url'));
console.log('--- repeated query keys ---');
const repeated = new URL('https://x.com/?tag=a&tag=b');
console.log('fromEntries keeps the last:', Object.fromEntries(repeated.searchParams));
console.log('getAll keeps both :', repeated.searchParams.getAll('tag'));
Output
18 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 18 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('the protocol includes its colon', parsed.protocol, 'https:');
test('the hostname has no port', parsed.hostname, 'www.example.com');
test('the host does', parsed.host, 'www.example.com:8080');
test('the port is a string', parsed.port, '8080');
test('the pathname', parsed.pathname, '/routes/lagos');
test('the search string includes the question mark', parsed.search, '?from=Ojota&to=Abuja');
test('the hash includes the hash sign', parsed.hash, '#seats');
test('the query is parsed into an object', parsed.query, { from: 'Ojota', to: 'Abuja' });
test('a default port comes back empty, not 443', parseUrl('https://example.com/').port, '');
test('the pathname is always at least a slash', parseUrl('https://example.com').pathname, '/');
test('a missing hash is an empty string', parseUrl('https://example.com/').hash, '');
test('invalid input gives null instead of throwing', parseUrl('not a url'), null);
test('a protocol-less string is invalid to the URL parser', parseUrl('example.com/path'), null);
test('a relative path is invalid on its own', parseUrl('/routes/lagos'), null);
test('but works with a base', new URL('/routes/lagos', 'https://example.com').href, 'https://example.com/routes/lagos');
test('fromEntries keeps only the last of a repeated key', Object.fromEntries(new URL('https://x.com/?tag=a&tag=b').searchParams), { tag: 'b' });
test('getAll keeps both', new URL('https://x.com/?tag=a&tag=b').searchParams.getAll('tag'), ['a', 'b']);
test('the parser decodes percent-escapes', new URL('https://x.com/?city=Port%20Harcourt').searchParams.get('city'), 'Port Harcourt');
new URL() is the answer, and writing a regex for this is a mistake — URLs have too many optional parts and too much escaping. Three behaviours to remember: the port is '' when it is the protocol default (not '443'), the pathname is never empty (always at least /), and a string with no protocol is simply not a URL, so 'example.com/path' fails unless you supply a base.
The repeated-key assertions are the sharp edge. Object.fromEntries(searchParams) is convenient and it silently discards all but the last value of a repeated key, so ?tag=a&tag=b becomes { tag: 'b' }. Use getAll when a key can legitimately repeat.
Answers JS Object 12 in the original set.
13 Own and inherited property names
fairCollect every property name an object responds to, its own and the ones it inherits, without listing anything twice.
Stuck? Three hints, smallest first
- 1. Walk the prototype chain with
Object.getPrototypeOfuntil it returnsnull. - 2. A
Sethandles the de-duplication when a subclass shadows a parent property. - 3.
for...inis a shortcut, but it only sees enumerable properties.
function allPropertyNames(obj) {
const found = new Set();
for (let o = obj; o !== null; o = Object.getPrototypeOf(o)) {
for (const key of Object.getOwnPropertyNames(o)) found.add(key);
}
return [...found];
}
// Stopping before Object.prototype, which is usually the useful version:
// otherwise every answer ends in toString, valueOf, hasOwnProperty...
function ownAndInherited(obj) {
const found = new Set();
for (let o = obj; o && o !== Object.prototype; o = Object.getPrototypeOf(o)) {
for (const key of Object.getOwnPropertyNames(o)) found.add(key);
}
return [...found];
}
// for...in: inherited, but enumerable only.
function enumerableNames(obj) {
const keys = [];
for (const key in obj) keys.push(key);
return keys;
}
class Vehicle {
constructor() { this.wheels = 4; }
move() {}
}
class Bus extends Vehicle {
constructor() { super(); this.seats = 14; }
board() {}
move() {} // shadows Vehicle.move
}
const bus = new Bus();
console.log('own only :', Object.getOwnPropertyNames(bus));
console.log('own and inherited :', ownAndInherited(bus));
console.log('for...in :', enumerableNames(bus), '<- class methods are not enumerable');
console.log('all the way up :', allPropertyNames(bus).length, 'names, ending at Object.prototype');
console.log('includes toString? :', allPropertyNames(bus).includes('toString'));
console.log('move appears once? :', ownAndInherited(bus).filter((k) => k === 'move').length, '<- shadowed, not duplicated');
Output
15 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 15 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('own properties come from the constructor', Object.getOwnPropertyNames(bus).sort(), ['seats', 'wheels']);
test('the walk finds the subclass method', ownAndInherited(bus).includes('board'), true);
test('and the parent method', ownAndInherited(bus).includes('move'), true);
test('a shadowed name appears only once', ownAndInherited(bus).filter((k) => k === 'move').length, 1);
test('own data properties are included', ownAndInherited(bus).includes('seats'), true);
test('and the inherited one', ownAndInherited(bus).includes('wheels'), true);
test('the short walk stops before Object.prototype', ownAndInherited(bus).includes('toString'), false);
test('the full walk reaches it', allPropertyNames(bus).includes('toString'), true);
test('and hasOwnProperty', allPropertyNames(bus).includes('hasOwnProperty'), true);
test('so the full walk is longer', allPropertyNames(bus).length > ownAndInherited(bus).length, true);
test('for...in sees only the data properties here', enumerableNames(bus).sort(), ['seats', 'wheels']);
test('because class methods are non-enumerable', Object.getOwnPropertyDescriptor(Bus.prototype, 'board').enumerable, false);
test('an object-literal method IS enumerable', Object.getOwnPropertyDescriptor({ go() {} }, 'go').enumerable, true);
test('the walk over a plain literal finds its keys', ownAndInherited({ a: 1, b: 2 }).sort(), ['a', 'b']);
test('a prototype-less object has only its own', allPropertyNames(Object.assign(Object.create(null), { a: 1 })), ['a']);
The chain walk is straightforward; the trap is for...in, which people reach for instead. It does traverse inherited properties, but only enumerable ones — and class methods are non-enumerable by design. The assertions show the consequence: for...in on a Bus finds seats and wheels and misses board and move entirely. Methods defined in an object literal are enumerable, so the two cases behave differently for no reason you can see at the call site.
Stopping the walk at Object.prototype is almost always what you want, otherwise every result is padded with toString, valueOf and friends.
Answers JS Object 13 in the original set.
14 Get all the values of an object
easyReturn the values rather than the keys, and be aware of what order they come out in.
Stuck? Three hints, smallest first
- 1.
Object.valuesis the direct answer. - 2. The order matches
Object.keysexactly, so the two line up index by index. - 3. Nothing removes duplicates, because two keys may hold the same value.
const student = { name: 'David Rayy', sclass: 'VI', rollno: 12 };
const valuesOf = (obj) => Object.values(obj);
// Before Object.values existed, this was the way. Still useful for the shape.
const valuesByMap = (obj) => Object.keys(obj).map((k) => obj[k]);
const withInherited = Object.create({ inherited: 'not mine' });
withInherited.own = 'mine';
console.log('values :', valuesOf(student));
console.log('keys :', Object.keys(student));
console.log('entries:', Object.entries(student));
console.log('inherited value is skipped:', valuesOf(withInherited), '<- own only');
console.log('but readable:', withInherited.inherited);
console.log('duplicates are kept:', valuesOf({ a: 1, b: 1, c: 2 }));
Output
12 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 12 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('the values, in key order', valuesOf(student), ['David Rayy', 'VI', 12]);
test('the manual version agrees', valuesByMap(student), valuesOf(student));
test('there are as many values as keys', valuesOf(student).length, Object.keys(student).length);
test('values line up with keys index by index', Object.keys(student).map((k) => student[k]), valuesOf(student));
test('an empty object gives an empty array', valuesOf({}), []);
test('inherited values are not included', valuesOf(withInherited), ['mine']);
test('even though the property is readable', withInherited.inherited, 'not mine');
test('duplicate values are all kept', valuesOf({ a: 1, b: 1, c: 2 }), [1, 1, 2]);
test('a value of undefined is still a value', valuesOf({ a: undefined }), [undefined]);
test('numeric keys reorder the values too', valuesOf({ b: 'second', 1: 'first' }), ['first', 'second']);
test('entries pairs them up', Object.entries({ a: 1 }), [['a', 1]]);
test('and fromEntries reverses it', Object.fromEntries(Object.entries(student)), student);
Object.values is a one-liner, so the value of this exercise is the guarantee behind it: Object.keys, Object.values and Object.entries all enumerate in the same order, so index i of one lines up with index i of the others. That is what makes Object.fromEntries(Object.entries(obj)) a faithful shallow copy. Note the numeric-key assertion again — { b: 'second', 1: 'first' } yields ['first', 'second'], because integer-like keys are enumerated first.
Answers JS Object 14 in the original set.
15 Convert an object to key/value pairs
easyTurn { a: 1, b: 2 } into [['a', 1], ['b', 2]], and turn it back again.
Stuck? Three hints, smallest first
- 1.
Object.entriesgoes one way,Object.fromEntriescomes back. - 2. The round trip is a shallow copy: nested objects are shared, not cloned.
- 3. A
Mapaccepts an entries array directly in its constructor.
const student = { name: 'David Rayy', sclass: 'VI', rollno: 12 };
const toPairs = (obj) => Object.entries(obj);
const fromPairs = (pairs) => Object.fromEntries(pairs);
// Pairs are what make transforming an object convenient.
const mapValues = (obj, fn) => fromPairs(toPairs(obj).map(([k, v]) => [k, fn(v)]));
const filterKeys = (obj, keep) => fromPairs(toPairs(obj).filter(([k]) => keep(k)));
// The round trip is shallow: the nested object is the SAME object.
const nested = { id: 1, seats: { total: 14, taken: 3 } };
const copy = fromPairs(toPairs(nested));
copy.seats.taken = 9;
console.log('pairs :', toPairs(student));
console.log('back again :', fromPairs(toPairs(student)));
console.log('as a Map :', new Map(toPairs(student)));
console.log('uppercased :', mapValues({ a: 'x', b: 'y' }, (v) => v.toUpperCase()));
console.log('--- the round trip is shallow ---');
console.log('changed the copy, and the original says:', nested.seats.taken);
Output
13 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 13 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('entries produces pairs', toPairs({ a: 1, b: 2 }), [['a', 1], ['b', 2]]);
test('the round trip returns an equal object', fromPairs(toPairs(student)), student);
test('but not the same object', fromPairs(toPairs(student)) === student, false);
test('each pair has exactly two items', toPairs(student).every((p) => p.length === 2), true);
test('an empty object gives no pairs', toPairs({}), []);
test('and no pairs rebuild an empty object', fromPairs([]), {});
test('a Map can be built straight from the pairs', new Map(toPairs(student)).get('rollno'), 12);
test('and turned back into pairs', [...new Map(toPairs(student))], toPairs(student));
test('mapping the values keeps the keys', mapValues({ a: 1, b: 2 }, (v) => v * 10), { a: 10, b: 20 });
test('filtering the keys works too', filterKeys(student, (k) => k !== 'rollno'), { name: 'David Rayy', sclass: 'VI' });
test('the round trip is SHALLOW: the nested object is shared', nested.seats.taken, 9);
test('structuredClone makes a deep copy instead', (() => { const deep = structuredClone(nested); deep.seats.taken = 1; return nested.seats.taken; })(), 9);
test('a later duplicate key wins in fromEntries', fromPairs([['a', 1], ['a', 2]]), { a: 2 });
Object.entries and Object.fromEntries together are how you transform an object: convert to pairs, use the array methods you already know, convert back. mapValues above is four tokens of real work.
The shallow-copy assertion is the one to remember. fromEntries(entries(obj)) copies the top level only, so a nested object is shared between the original and the copy — changing copy.seats.taken changed nested.seats.taken too. structuredClone is the built-in deep copy, and the assertion shows it leaves the original alone.
Answers JS Object 15 in the original set.
16 Swap the keys and values
fairReturn a copy where each value becomes a key and each key becomes a value. Then work out what happens when two keys share a value.
Stuck? Three hints, smallest first
- 1. Map over the entries, reversing each pair, then rebuild the object.
- 2. Object keys are always strings, so a numeric value becomes a string key.
- 3. If two keys hold the same value, one of them has to lose.
const invert = (obj) => Object.fromEntries(Object.entries(obj).map(([k, v]) => [v, k]));
// Keeping every key that shared a value, rather than losing all but one.
const invertToLists = (obj) => {
const out = {};
for (const [k, v] of Object.entries(obj)) (out[v] ??= []).push(k);
return out;
};
const codes = { lagos: 'LOS', abuja: 'ABV', kano: 'KAN' };
const collides = { ojota: 'lagos', yaba: 'lagos', wuse: 'abuja' };
const numeric = { a: 1, b: 2 };
console.log('codes inverted :', invert(codes));
console.log('twice gets back :', invert(invert(codes)));
console.log('--- values that collide ---');
console.log('naive invert :', invert(collides), '<- ojota is gone');
console.log('grouped invert :', invertToLists(collides));
console.log('--- values that were not strings ---');
console.log('numeric inverted :', invert(numeric));
console.log('key types :', Object.keys(invert(numeric)).map((k) => typeof k), '<- 1 became "1"');
Output
13 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 13 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('keys and values swap', invert(codes), { LOS: 'lagos', ABV: 'abuja', KAN: 'kano' });
test('inverting twice returns the original', invert(invert(codes)), codes);
test('an empty object inverts to an empty object', invert({}), {});
test('a single pair', invert({ a: 'b' }), { b: 'a' });
test('the original is not modified', codes, { lagos: 'LOS', abuja: 'ABV', kano: 'KAN' });
test('a colliding value keeps only the last key', invert(collides), { lagos: 'yaba', abuja: 'wuse' });
test('so the result is smaller than the input', Object.keys(invert(collides)).length, 2);
test('while the grouped version keeps both', invertToLists(collides), { lagos: ['ojota', 'yaba'], abuja: ['wuse'] });
test('numeric values become string keys', Object.keys(invert(numeric)), ['1', '2']);
test('and they really are strings', typeof Object.keys(invert(numeric))[0], 'string');
test('so 1 and "1" collide after inverting', Object.keys(invert({ a: 1, b: '1' })).length, 1);
test('an object value stringifies badly', Object.keys(invert({ a: { x: 1 } })), ['[object Object]']);
test('which is why a Map is better for non-string keys', new Map([[{ x: 1 }, 'a'], [{ x: 1 }, 'b']]).size, 2);
The one-liner works, and it loses data in two ways worth knowing about. Colliding values silently drop all but the last key — the assertion shows a three-key object becoming a two-key one, with ojota gone. And every object key is a string, so the numeric value 1 becomes the key '1', which means { a: 1, b: '1' } inverts to a single entry.
An object value is worse still: it stringifies to '[object Object]', so every object value collapses into one key. If your values are not strings or numbers, invert into a Map, which keeps keys of any type distinct.
Answers JS Object 16 in the original set.
17 Does the object have this property?
fairCheck whether an object contains a given property. There are four ways, and they answer four slightly different questions.
Stuck? Three hints, smallest first
- 1.
obj.key !== undefinedis the tempting one, and it is wrong when the value isundefined. - 2.
inincludes inherited properties;Object.hasOwndoes not. - 3.
Object.hasOwnis the modern replacement forobj.hasOwnProperty(key), and it works on prototype-less objects.
const byUndefined = (obj, key) => obj[key] !== undefined; // careless
const byIn = (obj, key) => key in obj; // own + inherited
const byHasOwn = (obj, key) => Object.hasOwn(obj, key); // own only
const byKeys = (obj, key) => Object.keys(obj).includes(key); // own + enumerable
const bus = { plate: 'LAG-441-XA', driver: undefined };
const inheriting = Object.create({ fare: 4500 });
inheriting.plate = 'ABJ-772-KM';
// hasOwnProperty is inherited, so an object without a prototype does not have it.
const bare = Object.create(null);
bare.plate = 'ENU-318-PH';
const bareHasOwnProperty = typeof bare.hasOwnProperty;
console.log('--- a property whose value IS undefined ---');
console.log('driver !== undefined:', byUndefined(bus, 'driver'), '<- says no, wrongly');
console.log("'driver' in bus :", byIn(bus, 'driver'), '<- says yes, correctly');
console.log('--- an inherited property ---');
console.log("'fare' in inheriting :", byIn(inheriting, 'fare'));
console.log('Object.hasOwn(.., fare) :', byHasOwn(inheriting, 'fare'), '<- own only');
console.log('--- a prototype-less object ---');
console.log('bare.hasOwnProperty is', bareHasOwnProperty, '<- calling it would throw');
console.log('Object.hasOwn still works:', byHasOwn(bare, 'plate'));
Output
17 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 17 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('a present property is found by all four', ['byIn', 'byHasOwn', 'byKeys'].length, 3);
test('in finds it', byIn(bus, 'plate'), true);
test('hasOwn finds it', byHasOwn(bus, 'plate'), true);
test('keys finds it', byKeys(bus, 'plate'), true);
test('an absent property is not found', byIn(bus, 'conductor'), false);
test('a property set to undefined IS present', byIn(bus, 'driver'), true);
test('but the careless check says it is not', byUndefined(bus, 'driver'), false);
test('which is the whole reason not to use it', byUndefined(bus, 'driver') !== byIn(bus, 'driver'), true);
test('in finds an inherited property', byIn(inheriting, 'fare'), true);
test('hasOwn does not', byHasOwn(inheriting, 'fare'), false);
test('both find the own one', byIn(inheriting, 'plate') && byHasOwn(inheriting, 'plate'), true);
test('in also finds inherited built-ins', byIn({}, 'toString'), true);
test('while hasOwn does not', byHasOwn({}, 'toString'), false);
test('a prototype-less object has no hasOwnProperty method', bareHasOwnProperty, 'undefined');
test('but Object.hasOwn works on it anyway', byHasOwn(bare, 'plate'), true);
test('a non-enumerable property is found by hasOwn', byHasOwn(Object.defineProperty({}, 'x', { value: 1 }), 'x'), true);
test('but not by Object.keys', byKeys(Object.defineProperty({}, 'x', { value: 1 }), 'x'), false);
Pick by the question you are asking. Object.hasOwn(obj, key) is the right default: own properties only, works on prototype-less objects, and unlike obj.hasOwnProperty(key) it cannot be broken by an object that happens to have its own property called hasOwnProperty. Use in when inherited properties should count.
Do not use obj[key] !== undefined. The assertions show it reporting false for a property that exists and holds undefined — the exact case where knowing whether the key is present actually matters.
Answers JS Object 17 in the original set.
18 Is the value a DOM element?
hardCheck whether a value is a DOM element. Write it so it does not break where there is no DOM at all.
Stuck? Three hints, smallest first
- 1.
v instanceof HTMLElementthrows whereHTMLElementis not defined, and fails across iframes. - 2. Duck-typing on
nodeType === 1and a stringnodeNameworks everywhere. - 3. Guard with
typeof HTMLElement !== ‘undefined’before using it.
// ---- the stand-in ----------------------------------------------------------
// The build runs in Node, which has no DOM, so these fakes stand in for real
// elements. They carry the exact properties the check looks at, which is the
// point: the check is duck-typed, so a faithful fake passes and a lookalike
// with the wrong nodeType does not.
const fakeElement = { nodeType: 1, nodeName: 'DIV', tagName: 'DIV' };
const fakeTextNode = { nodeType: 3, nodeName: '#text' };
const fakeDocument = { nodeType: 9, nodeName: '#document' };
// ---- the answer ------------------------------------------------------------
// Duck-typing: nodeType 1 is ELEMENT_NODE, and nodeName must be a string.
// Works in Node, in a browser, and across iframes.
function isElement(v) {
return typeof v === 'object'
&& v !== null
&& v.nodeType === 1
&& typeof v.nodeName === 'string';
}
// instanceof, guarded so it does not throw where the global is missing.
function isElementByInstance(v) {
return typeof HTMLElement !== 'undefined' && v instanceof HTMLElement;
}
const hasDom = typeof HTMLElement !== 'undefined';
// In a browser this is the real thing:
// isElement(document.querySelector('h1')) -> true
console.log('running with a real DOM?', hasDom);
console.log('fake element :', isElement(fakeElement));
console.log('fake text node :', isElement(fakeTextNode), '<- nodeType 3, not an element');
console.log('fake document :', isElement(fakeDocument), '<- nodeType 9');
console.log('plain object :', isElement({}));
console.log('the guarded instanceof check did not throw:', isElementByInstance(fakeElement));
Output
15 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 15 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('an element-shaped value passes', isElement(fakeElement), true);
test('a text node does not', isElement(fakeTextNode), false);
test('because its nodeType is 3', fakeTextNode.nodeType, 3);
test('the document itself does not', isElement(fakeDocument), false);
test('a plain object does not', isElement({}), false);
test('null does not', isElement(null), false);
test('undefined does not', isElement(undefined), false);
test('a string does not', isElement('<div>'), false);
test('a number does not', isElement(1), false);
test('an array does not', isElement([]), false);
test('nodeType alone is not enough without a nodeName', isElement({ nodeType: 1 }), false);
test('and a numeric nodeName is refused', isElement({ nodeType: 1, nodeName: 1 }), false);
test('a wrong nodeType is refused even with a good nodeName', isElement({ nodeType: 2, nodeName: 'DIV' }), false);
test('the guarded instanceof check never throws', typeof isElementByInstance(fakeElement), 'boolean');
test('and returns false where there is no DOM', hasDom ? true : isElementByInstance(fakeElement) === false, true);
Adapted. There is no DOM in the build, so the assertions run against objects carrying the properties a real element carries. That is fair here precisely because the check is duck-typed: it looks only at nodeType and nodeName, so nothing about a real element is being faked away. Open the built page in a browser and isElement(document.body) returns true.
Duck-typing beats instanceof for this one. v instanceof HTMLElement throws a ReferenceError in Node or a worker, and returns false for an element that came from an iframe, because that iframe has its own HTMLElement constructor. Checking nodeType === 1 works in every one of those cases. The typeof HTMLElement !== 'undefined' guard is the general pattern for touching any global that may not exist.
Answers JS Object 18 in the original set.
The exercise list follows w3resource's JavaScript exercises (CC BY-NC 4.0), and each exercise names the one it answers. The wording, hints, solutions and explanations here are my own.