4. Arrays and Objects
Work with lists and records, the shape of every piece of test data.
Chapter film (about 7m) is coming. The written chapter below is complete.
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
JavaScript arrays are dynamic and heterogeneous. They have no fixed size and no element type: they grow or shrink automatically and can hold a mix of different data types. 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. 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. 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);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 sit directly on the Array prototype, 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 is powerful but can be 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);
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));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.
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);Sign 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. Because objects and arrays are reference types, assigning them to new variables only copies the memory reference, not the underlying data. Spread operators (...) and Object.assign() 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 comes back as a string, Map, Set, RegExp and class instances 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');
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:
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);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. -
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(). Use RegEx/\[(.*?)\]/to extract the tag. -
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.
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. Arrow functions also cannot be used as constructors. -
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 immutable. To make it immutable, you would needObject.freeze().
// 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' ]Sign in to track your progress
Practice
3 exercises attached to this chapter, about 12m.