4. Arrays and Objects
Work with lists and records, the shape of every piece of test data.
Chapter film (about 7m) is coming.
Arrays: The Complete Guide
In plain English. An array is an ordered list of values, like the rows of a table or the items in a shopping cart. Positions are counted from zero, so the first item sits at position 0. Tests use arrays everywhere: the names of every product on a page, a set of test users to log in with, the error messages a form shows. This lesson covers adding, removing and cutting out items. Some methods mutate the array, meaning they change the original list in place; others leave it alone and give you a new list. Knowing which is which stops your test data changing under you. The spread operator, three dots (...), unpacks a list's items so you can copy or combine lists.
Dynamic Collections
A JavaScript array is a list with no fixed size and no fixed type: it grows or shrinks as you add and remove items, and one list can hold text, numbers and objects together. You can define them simply using array literal syntax [], which is preferred over the new Array() constructor.
Mutating Methods
JavaScript provides several built-in methods for basic array mutation. You can use push() to add elements to the end and pop() to remove them, turning the array into a stack. Similarly, unshift() adds to the beginning and shift() removes from the beginning. Pair them correctly: push() + pop() (both at the end) is a stack; push() + shift() (add at the end, take from the front) is a queue. unshift() + shift() is just a stack again, built at the front. reverse() flips the order and sort() puts the items in order, and both do it in place too. sort() accepts a comparator, a function you write that is given two items a and b and returns a negative number when a should come first, a positive one when b should, and zero for a tie. These operations are executed in-place, modifying the original array, which is an important consideration when trying to write immutable code.
Slicing and Splicing
For extracting or modifying sequences of elements, you have slice() and splice(). slice(start, end) returns a shallow copy of a portion of an array into a new array object, leaving the original array untouched. (Shallow means that items which are themselves lists or objects are shared with the original, not duplicated; the JSON & Data Manipulation lesson explains.) In contrast, splice(start, deleteCount, ...items) changes the contents of an array by removing or replacing existing elements and/or adding new elements in place. This distinction between mutating and non-mutating methods is what makes state predictable.
Combining Arrays
When it comes to combining arrays, you can use the older concat() method, but modern JavaScript heavily favors the spread operator ([...arr]). The spread operator expands an iterable into individual elements. It provides a readable and declarative way to merge arrays, insert arrays into other arrays, or create shallow copies.
const arr = [1, 2, 3];
arr.push(4);
arr.unshift(0);
console.log('After push/unshift:', arr);
const last = arr.pop(); // stack: push()/pop() both work at the end
const first = arr.shift(); // queue: push()/shift() - add at the end, take from the front
console.log('After pop/shift:', arr, '| popped:', last, '| shifted:', first);
const spliced = arr.splice(1, 1, 99);
console.log('After splice:', arr, 'Removed:', spliced);
const original = [10, 20];
const copy = [...original];
const combined = [0, ...original, 30];
console.log('Combined array:', combined);- fixSorted, but not numerically
- mcqJavaScript — quiz bank (1 question)🔒 standard
Sign in to track your progress
Array Iteration Methods
In plain English. Rather than writing a loop, you can call a method on an array and hand it a callback saying what to do with each item. map turns every item into something new, such as trimming every product name. filter keeps only the items that pass a check, such as rows marked "Failed". find picks out the first match. reduce boils the whole list down to one value, such as a cart total. Testers use these on lists read off a page or returned by a server. Chaining means calling one method straight after another, each working on the last one's result. A side effect is something a function does beyond handing back a value, such as printing a line.
Functional Paradigms
Modern JavaScript favors functional approaches for array iteration, heavily utilizing methods that accept callback functions. These methods belong to every array, so you call them on the array itself with no wrapper or conversion step. forEach is the most basic, simply executing a provided function once for each array element without returning a new array, ideal for side effects like logging or interacting with a page element in Playwright.
Transformation and Filtering
For transformation and filtering, map and filter are indispensable. map creates a new array populated with the results of calling a provided function on every element. filter creates a new array with all elements that pass the test implemented by the provided function. Because they return new arrays, they can be chained directly — .map().filter(). This chainability is perfect for processing lists of web elements or API response data cleanly.
Reducing and Searching
When you need to derive a single value from an array, reduce is the tool of choice. It executes a reducer callback function on each element, passing in the return value from the calculation on the preceding element. It does a lot in one line, which can make it harder to read than a standard loop. For searching, find returns the first element that satisfies the provided testing function, while findIndex returns its index.
Advanced Methods
Advanced querying methods include some (checks if at least one element passes a test) and every (checks if all elements pass), returning booleans. Additionally, includes checks if an array contains a certain value, much cleaner than indexOf(val) !== -1. For nested arrays, flat creates a new array with all sub-array elements concatenated into it recursively up to the specified depth, and flatMap combines mapping and flattening into a single pass. Note the asymmetry: flat() takes a depth argument and defaults to 1, while flatMap() always flattens exactly one level and has no depth argument — returning a nested array from a flatMap callback leaves that nesting in place.
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0); // % is the remainder after dividing, so this keeps the even numbers
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log('Doubled:', doubled);
console.log('Evens:', evens);
console.log('Sum:', sum);
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
const foundUser = users.find(u => u.name === 'Bob');
console.log('Found User:', foundUser);
const hasLargeNumber = numbers.some(n => n > 4);
console.log('Has large number?', hasLargeNumber);
const nested = [1, [2, 3], [[4, 5]]];
console.log('Flattened 1 level:', nested.flat());
console.log('Flattened deep:', nested.flat(Infinity));- fixThree timers, one variable
- mcqJavaScript — quiz bank (1 question)🔒 standard
Sign in to track your progress
Objects & Destructuring
In plain English. You met objects briefly with the for...in loop: a bundle of named values, each found by its key, like one row of a test-data sheet with a name, an email and a role. Here they get a lesson of their own, because test settings and server responses usually arrive in this shape. The named values are also called properties. This lesson shows how to read a property by name, list every key or value, and copy an object while changing one field. It ends with destructuring: unpacking several properties from an object into their own variables in a single line, so const { host } = config gives you a variable called host holding config.host.
Lightweight Structures
In JavaScript, objects are dynamic collections of key-value pairs, written with the literal syntax {}. No class definition is required, and properties can be added, changed or removed at any time. You can access properties using dot notation (obj.key), which is standard and readable, or bracket notation (obj['key']), which allows for dynamic key access using variables.
Iterating Objects
JavaScript provides utility methods on the global Object constructor to introspect and iterate over objects. Object.keys(obj) returns an array of the object's property names (keys), Object.values(obj) returns an array of the values, and Object.entries(obj) returns an array of [key, value] pairs. These methods bridge the gap between objects and the rich ecosystem of array iteration methods, allowing you to map or filter over an object's properties easily.
Object Spread
The object spread operator ({...obj}) is a modern addition that allows you to easily copy properties from one object to another. It's heavily used for creating shallow copies, merging objects, or updating specific properties while keeping the rest intact (a pattern common in state management like Redux or React). If two objects have the same key, the one spread last overwrites the previous value.
Destructuring
Destructuring assignment is a special syntax that allows you to unpack values from arrays, or properties from objects, into distinct variables. It significantly reduces boilerplate when extracting data from complex objects, such as API responses or Playwright configuration objects. You can even perform nested destructuring to extract deeply nested properties in a single line, and use computed property names [dynamicKey]: value within object literals to dynamically define keys during object creation. The rest syntax you met for parameters works here too: const { status, ...restOfConfig } = config; takes status out into its own variable and collects every other property into a new object called restOfConfig.
const dynamicKey = 'status';
const config = {
host: 'localhost',
port: 8080,
[dynamicKey]: 'active',
nested: {
retries: 3
}
};
console.log(config.host, config['port']);
console.log('Keys:', Object.keys(config));
console.log('Entries:', Object.entries(config));
const updatedConfig = { ...config, port: 9090, newFlag: true };
console.log('Updated Port:', updatedConfig.port);
const { host, nested: { retries } } = config;
console.log(`Host: ${host}, Retries: ${retries}`);
const { status, ...restOfConfig } = config;
console.log('Without status:', restOfConfig);- mcqJavaScript — quiz bank (2 questions)🔒 standard
Sign in to track your progress
Map and Set
In plain English. A Set is a collection that keeps each value only once: add the same thing twice and it is stored once. A Map is a lookup table: give it a key and it gives back the value filed under that key. Testers use a Set to remove duplicates from a list read off a page, such as product names on a results page that repeats some, and a Map to remember something by name, such as how many of each status a report shows. Both come ready-made in JavaScript. You make one with new, which means "make me a fresh one of these"; the JavaScript Classes & OOP lesson explains new in full, and for now the pattern in the code box is all you need.
Set: each value once. new Set(list) builds a Set from a list and drops the repeats. set.add(value) puts a value in, and adding one that is already there changes nothing. set.has(value) answers true or false, set.delete(value) removes one, and set.size is how many values it holds. A Set remembers the order the values first went in. To turn a Set back into a plain list, spread it: [...set], or use Array.from(set), which does the same.
Map: look up by key. map.set(key, value) files a value, map.get(key) reads it back, and a key that was never set gives undefined. map.has(key), map.delete(key) and map.size work as they do on a Set. An ordinary object can act as a lookup table too, but its keys are turned into text. A Map's keys can be anything, including a number or an object, and a Map keeps the order you added them in. Looping over a Map with for...of hands you one [key, value] pair per round; the square brackets in const [name, count] unpack the pair into two variables, and the Objects & Destructuring lesson covers unpacking.
Which one? Reach for a Set when the question is "have I seen this before?" or "which values are there, without repeats?". Reach for a Map when the question is "what goes with this key?". If your keys are plain words and you only read them, an ordinary object is fine.
Practise this. Predict the output of each console.log in the code box before you press Run, then add a duplicate and a missing key of your own.
// Set: each value once
const names = ['Latte', 'Mocha', 'Latte', 'Espresso', 'Mocha'];
const unique = new Set(names);
console.log('Unique count:', unique.size); // 3
console.log('Has Mocha?', unique.has('Mocha')); // true
unique.add('Latte'); // already there: nothing changes
console.log('Back to a list:', [...unique]); // [ 'Latte', 'Mocha', 'Espresso' ]
console.log('Same, with Array.from:', Array.from(unique));
// Map: look up by key
const stock = new Map();
stock.set('Latte', 12);
stock.set('Mocha', 0);
console.log('Latte in stock:', stock.get('Latte')); // 12
console.log('Tea in stock:', stock.get('Tea')); // undefined: no such key
console.log('Has Mocha?', stock.has('Mocha')); // true, even though its value is 0
// Looping over a Map gives one [key, value] pair per round
for (const [name, count] of stock) {
console.log(`${name}: ${count}`);
}
stock.delete('Mocha');
console.log('Entries left:', stock.size); // 1Sign in to track your progress
JSON & Data Manipulation
In plain English. JSON is the text format most APIs use to send data: an object or a list written out as plain text so it can travel between a server and your test. A response arrives as JSON text, and it has to become a JavaScript object before you can check its fields. Turning an object into text is called serializing; turning the text back into an object is parsing. This lesson also covers copying test data safely. A deep copy duplicates everything, nested parts included, so changing the copy never touches the original. Finally, it shows how to read a value buried deep in a response without crashing when part of it is missing. Nullish means null or undefined, JavaScript's two ways of saying "nothing here".
Native JSON Support
JSON (JavaScript Object Notation) is natively understood by JavaScript, with no third-party library needed to serialize or deserialize data. The built-in JSON.stringify(obj) converts a JavaScript object into a JSON string, which is necessary for sending data over HTTP. Conversely, JSON.parse(string) converts a JSON string back into a usable JavaScript object.
Shallow vs Deep Copies
A critical concept when manipulating objects and arrays is the difference between shallow and deep copies. It starts with how assignment works. Text, numbers and booleans are copied when you assign them, so changing the copy leaves the original alone. Objects and arrays are different: a variable holding one holds a reference, a pointer to the one object in memory, so const b = a makes two names for the same object, and a change made through b shows up through a. That is why they are called reference types: assigning them to new variables only copies the memory reference, not the underlying data. Spread operators (...) and Object.assign() (which copies one object's properties onto another) create shallow copies, meaning nested objects still share references. For a true deep copy, reach for the structuredClone() global function, which is available in Node 17+ and in every current browser. You will still see the older JSON.parse(JSON.stringify(obj)) trick in existing test code, and you need to know what it silently costs you, because it does not round-trip a JavaScript object faithfully: functions and undefined values are dropped from objects entirely (and become null inside arrays), a Date (the built-in type for a moment in time) comes back as a string, Map, Set, RegExp and class instances (objects made from a class, which you meet in JavaScript Classes & OOP) collapse to plain objects ({}), NaN and Infinity become null, BigInt throws a TypeError, and any circular reference throws. For an API response that is pure JSON to begin with it is harmless; for a fixture object holding dates or a Map it quietly corrupts your test data. structuredClone() handles all of those, including cycles — its own limitation is that it cannot clone functions, and it throws a DataCloneError if you hand it one.
Safe Navigation
When working with API response data, responses are rarely flat and often contain deeply nested structures that might occasionally be null or undefined. Reading a property off null or undefined throws a TypeError. To safely navigate these structures, JavaScript introduced Optional Chaining (?.). It permits reading the value of a property located deep within a chain without having to expressly validate that each reference in the chain is valid. If a reference is nullish, the expression short-circuits and returns undefined.
Nullish Coalescing
Complementing Optional Chaining is the Nullish Coalescing Operator (??). While the logical OR (||) operator falls back on any 'falsy' value (including 0, '', or false), ?? strictly checks for null or undefined. This allows you to safely provide default values for API responses or configuration options without accidentally overriding perfectly valid zero or empty string values. Together, ?. and ?? make data manipulation drastically safer and cleaner.
const apiResponse = {
user: {
id: 101,
profile: {
name: 'TestUser',
settings: null
}
}
};
const jsonString = JSON.stringify(apiResponse);
const parsedData = JSON.parse(jsonString);
console.log('Is object?', typeof parsedData === 'object');
// Two names, one object: a change through one shows through the other
const sameObject = apiResponse;
sameObject.user.id = 102;
console.log('apiResponse.user.id is now:', apiResponse.user.id); // 102
sameObject.user.id = 101;
const deepCopy = structuredClone(apiResponse);
deepCopy.user.id = 999;
console.log('Original ID unchanged:', apiResponse.user.id); // 101
// Why not JSON.parse(JSON.stringify(...))? It is lossy:
// new Date(...) makes a date value; new Set(...) is from the Map and Set lesson
const fixture = { createdAt: new Date('2026-01-01'), tags: new Set(['smoke']), retry: undefined };
const viaJson = JSON.parse(JSON.stringify(fixture));
console.log('via JSON :', viaJson); // { createdAt: '2026-01-01T00:00:00.000Z', tags: {} }
console.log('via clone :', structuredClone(fixture)); // Date stays a Date, Set stays a Set
const theme = apiResponse.user?.profile?.settings?.theme;
console.log('Theme (undefined, no crash):', theme);
const defaultTheme = theme ?? 'dark_mode';
console.log('Assigned Theme:', defaultTheme);
const count = 0;
console.log('Using || :', count || 10);
console.log('Using ?? :', count ?? 10);- fixOne fixture, two tests, one surprise
- mcqJavaScript — quiz bank (2 questions)🔒 standard
Sign in to track your progress
Practice and interview questions: Functions, Arrays & Objects
Now we move onto the functional and data manipulation aspects of JavaScript. Arrays and Objects are your primary data structures, and the built-in Array methods (map, filter, reduce) do most of the work you would otherwise write loops for.
Functions in JavaScript are first-class citizens. This means you can pass them around as variables, return them from other functions, and use them to create Closures. A closure is simply a function that remembers the variables from the place where it was defined, regardless of where it is executed later. This is crucial for managing state in test execution without using global variables.
Destructuring and JSON handling are also covered here. You will frequently interact with JSON APIs or parse test data files in Playwright. Mastering JSON.parse and object destructuring will make your test code much cleaner and more readable.
Coding Exercises
-
Extract Unique CSS Classes (Easy) Problem: Given an array of element objects
[{ id: 1, classes: ["btn", "primary"] }, { id: 2, classes: ["btn", "active"] }], writegetUniqueClasses(elements)to return a flat array of unique classes. Hint: Usereduceto flatten, and aSetto enforce uniqueness. -
Filter Visible Elements (Easy) Problem: Write
getVisibleText(elements)that takes an array of objects{ text: string, isVisible: boolean }. Return an array of the text of ONLY the visible elements, converted to uppercase. Hint: Chain.filter()and.map()together. A string'stoUpperCase()method returns an uppercase copy:'ok'.toUpperCase()is'OK'. -
Test Tag Counter (Medium) Problem: Given an array of test names like
["[bug] Login fails", "[feature] Add to cart", "[bug] Cart empty"], writecountTags(tests)that returns an object mapping tags to their counts:{ bug: 2, feature: 1 }. Hint: Use.reduce(). To pull the tag out of a name, find the position of]withindexOfandslicethe text between the brackets. (A regular expression also works, if you know one.) -
Deep Clone Config (Medium) Problem: Write
cloneConfig(config)that takes a nested configuration object and returns a deep copy, preventing mutations to the original. Hint: The quickest trick for JSON-safe objects isJSON.parse(JSON.stringify(config)). -
Test ID Generator (Medium) Problem: Write a function
createIdGenerator(prefix)that uses a closure to return a function. Each time the returned function is called, it yields a new ID:"test-1","test-2", etc. Hint: Declare acountvariable inside the parent function. The inner function will maintain access to it. -
First Plain Pattern (Medium) Problem: Write
firstPlainPattern(patterns)that takes an array of text patterns. Return the first one that does NOT start with"//". If every one starts with"//", return the first one. Hint: Afor...ofloop withstartsWith, returning as soon as you find a match.patterns[0]is the fallback. -
Safe Element Text (Medium) Problem: Write
getTextSafe(element)that takes an object. If the object has atextproperty, return it trimmed. If the object isnullorundefined, or lacks the property, return"". Hint: Optional chaining and??from the JSON & Data Manipulation lesson:element?.text?.trim() ?? "".
Interview Questions
-
What is a Closure in JavaScript? Answer: A closure is the combination of a function bundled together with references to its surrounding state (the lexical environment). It gives you access to an outer function's scope from an inner function, even after the outer function has returned.
-
What is the difference between an Arrow Function and a regular
function? Answer: Arrow functions do not have their ownthisbinding; they inheritthisfrom the parent scope. Regular functions bindthisbased on how they are called. (You will meetthisproperly in the classes lesson.) -
How does Array
mapdiffer fromforEach? Answer:mapreturns a new array containing the transformed elements, leaving the original array unchanged.forEachjust executes a callback for every element and returnsundefined. -
What is Object Destructuring? Give an example. Answer: It's a syntax to unpack properties from objects into distinct variables. Example:
const { name, age } = user; -
What is the Spread operator (
...) used for? Answer: It allows an iterable (like an array or string) to be expanded in places where zero or more arguments are expected, or an object to be expanded into another object. Useful for shallow copying or merging. -
Explain the difference between JSON and a JavaScript Object. Answer: JSON is a string format used for data serialization. A JS Object is an active data structure in memory. You convert between them using
JSON.stringify()andJSON.parse(). -
How do you add a new key-value pair to an existing object? Answer: Using dot notation (
obj.key = value) or bracket notation (obj["key"] = value). -
What happens if you use
conston an Array, and then push an item to it? Answer: It succeeds.constprevents reassignment of the variable identifier itself, but does not make the array read-only. JavaScript has a built-inObject.freeze()for that, which is beyond this course. -
How do you check if a variable is an array? Answer: Use
Array.isArray(variable).typeofwill just return"object". -
Write a quick script to reverse a string. Answer:
const reverseStr = str => str.split('').reverse().join('');
// Exercise 1: Extract Unique CSS Classes
function getUniqueClasses(elements) {
const allClasses = elements.reduce((acc, curr) => {
return acc.concat(curr.classes);
}, []);
return Array.from(new Set(allClasses));
}
const mockElements = [
{ id: 1, classes: ["btn", "primary"] },
{ id: 2, classes: ["btn", "active"] }
];
console.log(getUniqueClasses(mockElements)); // Expected: [ 'btn', 'primary', 'active' ]
// Exercise 3: Test Tag Counter (using indexOf and slice, no regex)
function countTags(tests) {
return tests.reduce((acc, name) => {
const tag = name.slice(1, name.indexOf(']'));
acc[tag] = (acc[tag] ?? 0) + 1;
return acc;
}, {});
}
console.log(countTags(["[bug] Login fails", "[feature] Add to cart", "[bug] Cart empty"])); // Expected: { bug: 2, feature: 1 }
// Exercise 6: First Plain Pattern
function firstPlainPattern(patterns) {
for (const pattern of patterns) {
if (!pattern.startsWith('//')) return pattern;
}
return patterns[0];
}
console.log(firstPlainPattern(['//button', '#save', '//a'])); // Expected: #save
console.log(firstPlainPattern(['//button', '//a'])); // Expected: //button
// Exercise 7: Safe Element Text
function getTextSafe(element) {
return element?.text?.trim() ?? '';
}
console.log(JSON.stringify(getTextSafe({ text: ' Save ' }))); // Expected: "Save"
console.log(JSON.stringify(getTextSafe(null))); // Expected: ""
console.log(JSON.stringify(getTextSafe({}))); // Expected: ""- mcqJavaScript — quiz bank (2 questions)🔒 standard
Sign in to track your progress
Common questions
Practice
11 exercises attached to this chapter, about 12m.
- fixThree timers, one variable
- fixSorted, but not numerically
- fixOne fixture, two tests, one surprise
- mcqJavaScript — quiz bank (8 questions)🔒 standard