1. Interaction
  2. JavaScript
  3. Objects and arrays

Interaction · JavaScript 06

Objects and arrays

Most of the data in a JavaScript program lives in arrays and objects, and most of the surprises come from how the language handles them. A second name for an array turns out to be the same array. Two identical objects compare as different. A function's this changes depending on the line that calls it. This page covers how these composite types are built, reached, copied and compared, and what happens to them when you are done.

JavaScript supports more advanced types made up of a collection of basic types. They are also called reference types, and that second name explains most of this page. The array and the object are the two you will write constantly. Functions are the third, and Functions and closures covers them. The methods that arrays and strings come with, such as map and slice, are on the Built-ins page. As before, read each playground, predict the output, then change something and run it again.

1. Arrays

An array is an ordered set of values grouped together under a single identifier. There are two ways to make one, a literal in square brackets and the Array constructor. Use the literal. The constructor has a trap: one number argument sets the length and does not become an element, so new Array(10) is ten empty slots and new Array(10, 20) is two elements.

let emptyArray = []; let myArray = [1, 15, 3, 33, 68]; let myArray2 = ['Thomas', true, 3, -47]; let myArray3 = new Array(); let myArray4 = new Array(10); console.log(myArray2.length); // logs: 4 console.log(myArray3.length); // logs: 0 console.log(myArray4.length); // logs: 10 console.log(myArray4[0]); // logs: undefined

The third line of that sample shows an oddity if you come from C or Java. An array has no element type, so strings, booleans and numbers can sit side by side.

Reading and writing elements

Access by index looks as usual, and indexes start at 0. The odd part is the last line of the next sample. An array can also be accessed by a string key, and myArray['3'] reaches the same element as myArray[3].

let myArray = [1, 15, 3, 33, 68]; let myArray2 = ['Thomas', true, 3, -47]; myArray[3] = 'Hello'; console.log(myArray[3]); // logs: Hello console.log(myArray2.length); // logs: 4 myArray['3'] = 'Goodbye'; console.log(myArray[3]); // logs: Goodbye

Arrays can store arbitrary content, and that includes functions. Since an element can be a function, following it with parentheses calls it. The typeof line explains the string key from the previous sample: typeof says an array is an object. Its indexes are property names, and property names are strings, so the number 3 and the string '3' name the same property. Because typeof cannot tell you whether something is an array, use Array.isArray().

let myArray = [1, 15, 3, 33, 68]; myArray[3] = function () { console.log('called from an array'); }; myArray[3](); // logs: called from an array console.log(typeof myArray); // logs: object console.log(Array.isArray(myArray)); // logs: true

Length is not a count

In most languages, writing past the end of an array is an error. In JavaScript it makes the array longer and leaves a gap. The length property is one more than the highest index, so it can exceed the number of values you have stored. It can also be written to, and setting it lower cuts off the elements past that point.

let sparse = ['a']; sparse[5] = 'f'; console.log(sparse.length); // logs: 6 console.log(sparse[3]); // logs: undefined console.log(sparse.join()); // logs: a,,,,,f sparse.length = 1; console.log(sparse.join()); // logs: a

An array assigned to a second name also behaves differently from a number assigned to a second name. Section 5 takes that up, with objects, since an array is a kind of object.

2. Objects

An object is a less ordered collection of data as well as functions in one package. Objects have two notations to access their properties. Period notation is similar to Java and Python, as in object.property and object.function(). Bracket notation is similar to arrays, as in someObject['property'].

"Less ordered" is the accurate phrase. You should not treat an object as a sequence, but its keys do have a defined order when you list them: keys that look like array indexes come first in ascending order, and other string keys follow in the order they were added.

let mixed = { b: 1, 2: 1, a: 1, 1: 1 }; console.log(Object.keys(mixed).join()); // logs: 1,2,b,a

The next sample has two small objects. The first holds only data. The second also holds a function, and the function reaches the data it travels with through the keyword this. Section 9 explains this in full.

let stooge1 = { names: ['Larry', 'Curly', 'Moe'], number: 3 }; let stooge2 = { names: ['Larry', 'Curly', 'Moe'], showNumber: function () { console.log(this.names.length); } }; console.log(stooge1.names.join()); // logs: Larry,Curly,Moe stooge2.showNumber(); // logs: 3

A missing property gives no error. A misspelled stooge1.name in place of stooge1.names quietly evaluates to undefined, and the failure appears later, when something tries to use it. The same mistake inside showNumber would throw a TypeError, since it would ask for the length of undefined.

Objects underneath, and primitives on top

You have been using these ideas everywhere. console.log('hello') says: using the console object, invoke the log() method, and give it the string 'hello'. So much of the language is reached this way that it can look as if everything underneath is an object. That is a common claim, and two pieces of evidence are often offered for it. Neither holds up.

The first is that typeof null returns 'object'. That result is a historical quirk. In the first implementation of JavaScript, a value carried a type tag, the tag for objects was 0, and null was represented as the null pointer, which also had 0 in that position. MDN records that a fix was proposed for ECMAScript and rejected. So null is a primitive, and typeof misreports it.

The second is that a literal can have methods, as in 'tom'.toUpperCase(). A primitive string is not an object, though. When you ask it for a method, JavaScript wraps it in a temporary String object for that one operation and then discards the wrapper. You can see the wrapper disappear. A property assigned to a primitive is set on the temporary object and gone by the next line, and the assignment gives no error in an ordinary script.

console.log(typeof null); // logs: object console.log('tom'.toUpperCase()); // logs: TOM let name1 = 'tom'; name1.nickname = 'tommy'; console.log(name1.nickname); // logs: undefined console.log(typeof name1); // logs: string

Global variables have a related story. A var at the top level of a classic script becomes a property of the window object, so the two lines below refer to one thing. It does not hold for everything global. A top-level let is not a property of window, as Statements, variables and flow showed.

var x = 5; console.log(window.x); // 5 let y = 6; console.log(window.y); // undefined

Where objects come from, and how far to take them

Objects in client-side JavaScript fall into four groups: built-in objects (String, Date, Array), browser and API objects (Window, Navigator), document objects (Document, document.forms), and user-defined objects. This page is mostly about the fourth group. Built-ins covers the first, and the DOM series covers the third.

Since objects are everywhere, you may ask whether to always use object-oriented programming. This course's position is yes-ish, but make sure it makes sense. The value of objects at the very small, without reuse, is suspect: a short script gains little from wrapping three variables and a function in a class. There are true trade-offs at scale. With deep inheritance performance suffers, since looking up a property high on the prototype chain takes time, and some problems are awkward to express this way. Those speed and awkwardness problems keep leading back to what the course calls "the one true way to write code" fallacy. Use objects because a problem has things in it that hold state and behavior together, and use something else when it does not.

The traditional way to create an object

The traditional way to create an object is the new operator along with the appropriate constructor function. You will see it in older code, and some samples on this page use it to get a string object to experiment on. The new String form is one to recognize and avoid: it makes an object where a literal makes a primitive, and the two behave differently, as the typeof lines show and as comparison will show again. Of course we will often be making our own objects, and new Object() starts one with no properties of its own.

let city = new String('San Diego'); let city2 = 'San Diego'; console.log(typeof city); // logs: object console.log(typeof city2); // logs: string let place = new Object(); place.name = 'San Diego'; place.sunny = true; console.log(place.name); // logs: San Diego

Current code rarely writes new Object(), because the literal in section 6 does the same job in fewer characters. new with your own constructors, and the class syntax over them, belongs to Prototypes and classes.

3. Properties and methods

Properties

A property is named data contained in an object. A string object has a length. Unlike in Java, you are not limited to the properties an object started with. Instance properties can be added to a particular object at any time and deleted as well. Properties that are not set return undefined, with no error.

let myString = new String('Hello there'); console.log(myString.length); // logs: 11 let another = new String('General Kenobi'); another.simpleExample = true; console.log(another.simpleExample); // logs: true delete another.simpleExample; // Properties that are not set return undefined console.log(another.simpleExample); // logs: undefined

undefined therefore has two meanings: the property is absent, or it is present and holds undefined. To tell those apart, use the in operator, which also counts inherited properties, or Object.hasOwn(), which does not. Note also what delete does and does not do. It removes a property from an object. It does not free memory directly and it does not work on a variable, and on an array it leaves a gap without changing length.

Array-style access

You can also use [ ] to access a property of an object by name. These lines do the same work as the sample above. Parentheses after delete are allowed, but it is an operator, and they only group the expression after it.

let myString = new String('hi'); console.log(myString['length']); // logs: 2 myString['simpleExample'] = true; console.log(myString['simpleExample']); // logs: true delete myString['simpleExample']; console.log(myString['simpleExample']); // logs: undefined

The main uses of this style are for when the property name is held in a variable, or when the name contains spaces or special characters. The hyphen is the case you will meet most, since user-name after a dot means subtraction, as Where three languages meet explained. Properties made with the Symbol type also need brackets. A symbol is a value and not a name you can type, so tagged.id looks for a property called 'id' and does not find the symbol. Types and coercion introduces symbols.

This playground uses all three. Each read goes through brackets for a different reason.

Methods

Object properties that are functions are called methods. You can use the standard . operator as well as the array syntax to access a method, and you add the ( ) to invoke it. Without the parentheses you get the function itself, as a value.

let myString = new String('Hi there'); console.log(myString.toUpperCase()); // logs: HI THERE console.log(myString['toLowerCase']()); // logs: hi there console.log(typeof myString.toUpperCase); // logs: function

Like properties, instance methods can be set on a particular object. No other object gets the method, and it can be replaced or deleted the way any other property can.

let myString = new String('Hi there'); myString.sayWhat = function () { console.log('What?'); }; myString.sayWhat(); // logs: What?

4. Enumerating objects

The for/in loop, introduced in Walking a collection, can be used to enumerate object properties and sometimes methods. It is a quick way to see what an unfamiliar object offers. Run this one in the browser's console.

for (let prop in window.navigator) { console.log(`window.navigator["${prop}"] = ${window.navigator[prop]}`); }

The "sometimes" comes from a flag on each property, called enumerable, and for/in visits only properties that have it set. Properties you create by assignment or in a literal are enumerable, and that includes methods you assign. Many properties of built-in objects are not. That is why, in the next sample, the loop over a string object finds its characters and your added foo, but no toUpperCase. The loop over String, the constructor, finds nothing. String has plenty of properties, and String.prototype is one, but none of them is enumerable.

let myString = new String('hi'); myString.foo = true; for (let prop in myString) { console.log(`myString["${prop}"] = ${myString[prop]}`); // logs: myString["0"] = h // logs: myString["1"] = i // logs: myString["foo"] = true } for (let prop in String) { console.log(prop); // nothing is printed }

Older material says this varies by browser. The flag is defined by the language, so a difference between browsers comes from the objects each one offers. Host objects like navigator do differ, which is the exercise at the end of the for/of section on the syntax page.

for/in also walks inherited enumerable properties, which is usually not what you want from a data object. Object.keys(), Object.values() and Object.entries() give arrays of an object's own enumerable string-keyed properties, and you can loop over those with for/of. None of these lists symbol keys.

let backpack = { color: 'blue', pockets: 3 }; console.log(Object.keys(backpack).join()); // logs: color,pockets console.log(Object.values(backpack).join()); // logs: blue,3 for (let [key, value] of Object.entries(backpack)) { console.log(key + ' is ' + value); // logs: color is blue // logs: pockets is 3 }

5. Reference types and comparison

All JavaScript types can be categorized as primitive or reference types. Reference types include objects, arrays and functions. Since they can be large, a variable does not contain the actual data. It holds a reference to the place in memory that holds the data. Assignment copies what the variable holds, so assigning a primitive copies the value and assigning an object copies the reference. The result is two names for one object.

Assigning a primitive against assigning an arrayOn the left, a primitive: after let b = a and b += 2, variable a holds 5 and variable b holds 7, in two separate boxes. On the right, a reference: arr1 and arr2 are two boxes whose arrows point at one array holding Larry, Shemp and Moe, so a change through either name shows through both.Primitive: the value is copiedlet a = 5;let b = a;b += 2;a: 5b: 7two values, changed apartReference: the reference is copiedarr1arr2Larry,Shemp,Moeone array, seen through two names
Assigning a primitive makes a second value. Assigning an array or object makes a second name for the first one.

In the playground, arr1[1] = 'Shemp' changes one element through one name, and both arrays appear changed, since there is only one array. The city object shows the same behavior with a property.

References and functions

The same rule applies when you pass an argument. Functions receive a copy of what the variable holds. For a primitive, that is a copy of the value, so the caller's variable cannot change. For an array or object, it is a copy of the reference, so the function can change the contents and the caller will see them. Given the way reference types work, you will find them useful with functions.

function fiddle(x) { x[0] = 'hello'; } let x = ['test']; fiddle(x); console.log(x[0]); // logs: hello function fiddle(x) { x += 2; } let x = 5; fiddle(x); console.log(x); // logs: 5

This is sometimes called pass by reference, but the reference itself was copied. Assigning a new array to the parameter inside fiddle would change only the local name and leave the caller's array alone. Functions and closures goes through this with parameters in full.

Comparing objects

When comparing primitive types, things work as expected. With reference types, you are comparing a reference to data and not the data itself, so the comparison may fail. Two string objects with the same characters are two objects, so they are not equal. Getting their primitive values out with valueOf() compares what you meant.

let str1 = 'abc'; let str2 = 'abc'; console.log(str1 == str2); // logs: true str1 = new String('abc'); str2 = new String('abc'); console.log(str1 == str2); // logs: false console.log(str1.valueOf() == str2.valueOf()); // logs: true

The same holds for arrays and plain objects, with === as well as ==. Two literals make two objects even when they read the same, and a comparison is true only when both sides are the same object.

let a = [1, 2]; let b = [1, 2]; let c = a; console.log(a === b); // logs: false console.log(a === c); // logs: true console.log(JSON.stringify(a) === JSON.stringify(b)); // logs: true console.log(JSON.stringify({ x: 1, y: 2 }) === JSON.stringify({ y: 2, x: 1 })); // logs: false

JavaScript has no built-in operator that compares objects by content. Comparing the JSON.stringify output, as above, works for simple data. It fails when the same keys were added in a different order, as the last line shows. For anything larger, write a function that compares the fields you care about.

6. User-defined objects and literals

Generic objects are used to create user-defined data types. Start with an empty object, then add what it needs. A method can be added in the same way, and here it is an arrow function, a form covered in Functions and closures.

let robot = new Object(); robot.name = 'Zephyr'; robot.model = 'Guard'; robot.hasJetPack = true; console.log(`${robot.name} ${robot.model}`); // logs: Zephyr Guard robot.attack = () => { console.log('Zap!'); }; robot.attack(); // logs: Zap!

Object literals

Object literals have been supported since JavaScript 1.2 and are written as { name1: value1, name2: value2 }. A literal gathers the whole object in one expression and is the usual way to make one today. A key that is not a valid identifier needs quotes, so write 'user-name': 1 and not user-name: 1.

let robot2 = { name: 'Arnold', model: 'Terminator', hasJetPack: false, attack: function () { console.log('Hasta la vista, baby'); } }; robot2.attack(); // logs: Hasta la vista, baby

You can build quite large objects this way if you like. A value can be a variable's value, a function, null, or another object, and nesting gives you a chain of dots to walk.

let jetpack = true; let robot3 = { name: null, hasJetPack: jetpack, model: 'Protocol Droid', attack: function () { console.log('Ahhhh!'); }, sidekick: { name: 'Spot', model: 'Dog', hasJetPack: false, attack: function () { console.log('CHOMP!'); } } }; robot3.attack(); // logs: Ahhhh! robot3.sidekick.attack(); // logs: CHOMP! console.log(robot3.sidekick.name); // logs: Spot

Newer code has three shorthands, and you will read them everywhere. A method can drop the colon and the function keyword. When a variable has the same name as the key, you can write the name once. A key in square brackets is computed when the literal runs.

let jetpack = true; let keyName = 'model'; let robot4 = { jetpack, // the same as jetpack: jetpack [keyName]: 'Guard', // a key chosen at run time attack() { // the same as attack: function () { ... } console.log('Zap!'); } }; console.log(robot4.jetpack); // logs: true console.log(robot4.model); // logs: Guard robot4.attack(); // logs: Zap!

Objects as associative arrays

You can also use an associative array style for object access, or you could say associative arrays are just objects. Other languages give this structure its own type, under names like dictionary, hash or map. In JavaScript a plain object has long filled the role, and the key can contain anything, as this customer list shows.

let customers = new Object(); customers['John Doe'] = '123 Main St, San Diego'; console.log(customers['John Doe']); // logs: 123 Main St, San Diego

The advantage is that you can use property names not known until run time. Below, the two values stand in for what someone typed into a form, and the program does not know the name when it is written.

let customers = {}; let customerName = 'Ada Lovelace'; let customerAddress = '12 St James Square, London'; customers[customerName] = customerAddress; console.log(customers[customerName]); // logs: 12 St James Square, London

There is a catch once the keys come from outside your program. A plain object inherits properties, so an empty one already answers to some names. A customer named toString appears to exist before anyone adds them. Object.hasOwn() checks the object's own properties only. For a collection that has arbitrary keys, or that changes often, Map avoids the problem, and Built-ins covers it.

let customers = {}; console.log(typeof customers['toString']); // logs: function console.log('toString' in customers); // logs: true console.log(Object.hasOwn(customers, 'toString')); // logs: false

7. Optional chaining

Often you want to read a nested property that may not be there. Say you want user.address.street. If user has no address property, user.address is undefined, and asking undefined for street throws a TypeError, because you are trying to read a property of something that does not exist. Firefox reports "can't access property "street", user.address is undefined", and Chrome reports "Cannot read properties of undefined (reading 'street')". The wording differs by browser.

To do this safely, use the ?. operator: user.address?.street. If the value on its left is null or undefined, the whole expression stops there and evaluates to undefined. Otherwise it behaves as a plain dot. Optional chaining has worked across browsers since July 2020.

That playground shows several details. The result is undefined even when the value that stopped the chain was null, and you get null only when the property itself holds null. The chain stops as a whole, so nobody?.address.street does not throw even though it has a plain dot after the ?.. The forms ?.() and ?.[ ] do the same for a method call and for bracket access.

It has limits. user.address?.street guards only address, so if user itself might be missing you need user?.address?.street. A variable that was never declared is still a ReferenceError. You also cannot assign through it, so user.address?.street = 'Main' is a SyntaxError.

Use it where a value can legitimately be missing, such as an optional field in a response. Put on every dot, it hides bugs by turning a broken object into a quiet undefined that fails somewhere else. It pairs with ?? from Types and coercion to supply a default.

8. Spread syntax

A function can accept an unknown quantity of arguments through a rest parameter, three dots and a name, which collects the remaining arguments into an array. Functions and closures covers it. This is the function the next samples call, with explicitly defined parameters and then a rest parameter, which must be last.

function foo(firstName, lastName, ...clubs) { console.log(`${firstName} ${lastName} is in the following clubs:`); for (let i = 0; i < clubs.length; i++) { console.log(clubs[i]); } }

Sometimes the situation is the reverse. You have an array, of a length you may or may not know, and you want each element to be its own argument in a function call, without having to write them out. Spread syntax gives you this, with the same ... as the rest parameter, as part of the function call and not the definition. Rest collects values into an array, and spread expands an array into values.

function foo(firstName, lastName, ...clubs) { console.log(`${firstName} ${lastName} is in ${clubs.length} clubs`); } let clubs = ['Red Cross', 'Ultimate Frisbee', 'Knitting']; foo('Camdyn', 'Rasque', ...clubs); // logs: Camdyn Rasque is in 3 clubs

You can even use spread inside an array, to combine arrays. Both forms appear together in the second sample below, where the function collects with a rest parameter and the call expands two arrays.

let arr1 = ['apples', 'oranges']; let arr2 = ['peaches', 'coconuts']; let fruits = [...arr1, ...arr2]; console.log(fruits.join()); // logs: apples,oranges,peaches,coconuts // Rest parameter function listColors(...colors) { colors.forEach(color => console.log(color)); } let warm = ['red', 'orange', 'yellow']; let cool = ['blue', 'green']; // Spread syntax listColors(...warm, ...cool); // logs: red // logs: orange // logs: yellow // logs: blue // logs: green

Spread in calls and array literals works on anything iterable, the same set for/of accepts, so [...'abc'] gives three strings and spreading a plain object there throws a TypeError.

Spreading objects and shallow copies

Spread also works inside an object literal, where it copies an object's own enumerable properties. If a name appears twice, the later value wins, which makes this a common way to apply settings over defaults.

let defaults = { color: 'blue', size: 'M' }; let order = { ...defaults, size: 'L' }; console.log(order.color); // logs: blue console.log(order.size); // logs: L console.log(defaults.size); // logs: M

This connects to references. [...arr1] and { ...student } are the usual ways to get a separate copy, and they fix the Larry, Curly and Moe problem. The copy is shallow, though. It goes one level deep, so a nested object is copied by its reference and stays shared between the original and the copy.

A shallow copy shares its nested objectsTwo separate objects, student and copy, made with spread. Each has its own name property. Each has an address property with an arrow pointing to one shared object holding the city, so a change to the address through copy also shows through student.studentname: 'Camdyn'address:copy = { ...student }name: 'Thomas'address:city: 'Los Angeles'
Spread copies one level. The two objects have separate names but share one address.

Changing copy.name left the original alone, and changing copy.address.city did not. For a deep copy, the platform has structuredClone(), available across browsers since March 2022. It throws a DataCloneError on values it cannot clone, and functions are among those, so it suits data and not objects with methods.

9. The this keyword

Sometimes when creating functions inside objects, referred to as methods, you need to reference the object you are in so you can access its other properties or methods. The this keyword does that.

let backpack = { color: 'blue', printColor() { console.log(this.color); } }; backpack.printColor(); // logs: blue

If you know Java or C++, that looks familiar, but the rule differs. this is evaluated at run time, based on how the function was called. Where the function was defined does not decide it. So you can use it outside of objects, and the same function can see a different this on each call.

Called as a method

When a function is called with a dot or brackets in front of it, this is the object immediately to the left. With nesting, that is the nearest object and not the outermost one. Inside student.name.print(), this is student.name, so it has first and last, and this.favoriteColor would be undefined.

let student = { favoriteColor: 'blue', name: { first: 'Camdyn', last: 'Rasque', print() { console.log(`${this.first} ${this.last}`); } } }; student.name.print(); // logs: Camdyn Rasque

A function does not need to be written inside the object. It can be defined on its own and attached as a property, and this works just the same, because what counts is the call.

function printName() { console.log(this.name); } let student = { name: 'Camdyn Rasque', print: printName }; student.print(); // logs: Camdyn Rasque

The most common use is a method that works on data held in the same object. Here shop keeps a queue of orders, and each method reaches the queue through this.

let shop = { orders: [], addOrder(order) { this.orders.push(order); }, getNextOrder() { return this.orders.shift(); } }; shop.addOrder({ item: 'pencil', cost: 0.50 }); shop.addOrder({ item: 'eraser', cost: 1.50 }); shop.addOrder({ item: 'notebook', cost: 1.00 }); let nextOrder = shop.getNextOrder(); console.log(nextOrder.item); // logs: pencil console.log(shop.orders.length); // logs: 2

Called on its own

With nothing to the left of the call, the answer depends on strict mode. In strict mode, which Built-ins covers, this is undefined. In sloppy mode, the default for a classic script, this is replaced with the global object, which in a page is window. Module code and class bodies are always strict. So the plain call printName() behaves in two ways. In a module it throws a TypeError, because it reads name from undefined. In a classic script it reads window.name, a built-in property of the window, and prints that with no error at all, which makes for a confusing bug.

function sloppy() { return this === globalThis; } function strict() { 'use strict'; return this; } console.log(sloppy()); // logs: true console.log(strict()); // logs: undefined

The trap most people hit comes from this rule. Taking a method off its object gives you only the function, and the object does not come with it. Calling that function on its own is a plain call. The playground also shows the fix for each case: attach it to something, or pass this explicitly with call, apply or bind. call takes the this value and then the arguments one by one, apply takes them as an array, and bind makes a new function with this fixed for good, so even call cannot change it. If printColor were strict, the detached call would throw a TypeError where this one prints undefined.

What this is, by how the function was calledFive forms of call and the value of this in each. backpack.printColor() gives backpack. printColor() gives the global object, or undefined in strict mode. printColor.call(bag) gives bag. new Thing() gives the new object. An arrow function ignores the call and uses the this of the code around it.The callWhat this isbackpack.printColor()backpackprintColor()global, or undefined if strictprintColor.call(bag)bagnew Thing()the new objectan arrow function, any callthis of the code around it
To find this, look at the call. The exception is an arrow function, where you look at the surrounding code.

Callbacks and arrow functions

A callback is a function you hand to something else to call later, and that something else usually makes a plain call. Passing backpack.printColor to setTimeout detaches it, the same as the playground above. Wrapping it in an arrow function keeps the dot in the call.

let backpack = { color: 'blue', printColor() { console.log(this.color); } }; setTimeout(backpack.printColor, 0); setTimeout(() => backpack.printColor(), 0); // logs: undefined // logs: blue

An arrow function has no this of its own. It uses the this of the code surrounding it, whatever the call looks like, and call and bind cannot change that. That makes it the right choice for a callback inside a method, where you want the method's this, and the wrong choice for a method itself. In the sample, the regular function passed to forEach gets a plain call and loses label, and the arrow function sees the method's this.

let shop = { label: 'Order', orders: ['pencil'], printRegular() { this.orders.forEach(function (order) { console.log(this.label + ': ' + order); }); }, printArrow() { this.orders.forEach((order) => { console.log(this.label + ': ' + order); }); } }; shop.printRegular(); // logs: undefined: pencil shop.printArrow(); // logs: Order: pencil let robot = { model: 'Guard', describe: () => console.log(this.model) }; robot.describe(); // logs: undefined

The last call shows the wrong choice. describe is an arrow function written at the top level of the script, so its this is the top-level this, the global object in a classic script. The robot's literal does not create a scope for this.

The browser adds one case of its own. A regular function registered with addEventListener is called with this set to the element the listener is on, the same value as event.currentTarget. An arrow function used as a handler keeps the surrounding this and does not get the element, so use event.currentTarget there. Hello, events introduces handlers. The new row of the diagram belongs to Prototypes and classes. MDN's reference on this lists the remaining cases.

10. Garbage collection and leaks

Objects get made constantly, and you never destroy one directly. You do not have to do much, as JavaScript has a garbage collector that cleans up objects your program can no longer reach. In the first sample below, once myString points at the new object, nothing refers to the Larry Fine string, and it will eventually be collected.

let myString = new String('Larry Fine'); myString = new String('Moe Howard'); // The Larry Fine string is eventually garbage collected

The test is reachability. All modern engines use a mark-and-sweep collector, which starts from a set of roots, such as the global object, follows every reference it can, and collects what it did not reach. This also handles cycles. Two objects that refer only to each other are unreachable and get collected.

Reachable objects stay, unreachable objects goFrom the global object an arrow reaches a cache array, and from a running timer an arrow reaches a callback; both stay in memory. Below them, a Larry Fine string and two objects that refer only to each other are not reached from any root, so they can be collected.RootsObjectsglobal objectcache arraystaysrunning timertimer callbackstays'Larry Fine'collectedabcollected
What counts is whether a path leads to the object from a root. A pair that refers only to each other has no such path.

You can hint to the browser that you are done with memory by using null. More exactly, assigning null removes one reference. It frees nothing by itself, and if another name still refers to the object, the object stays.

let city = new Object(); let city2 = city; city.name = 'San Diego'; city = null; // say you are done with this name console.log(city2.name); // logs: San Diego

There is no obvious way to force collection. It is not possible to trigger it from standard JavaScript, and when it runs is up to the host environment. Engines may expose a way behind a flag, such as Node's --expose-gc, but that is a debugging aid and not something a page can use.

Leaks

Garbage collection does not rule out memory leaks. A leak in JavaScript is memory you no longer need that is still reachable, so the collector correctly leaves it alone. Leaks happen especially in long-lived apps, since a page reloaded every few minutes clears its memory, while a mail client left open all week does not. Dangling references and timers are the usual causes, and event handlers in web apps are particularly prone to misuse.

// A dangling reference: this array holds every report for as long as the page lives let cache = []; function remember(report) { cache.push(report); } // A timer: the callback and all it can reach stay until the timer is cleared let timerId = setInterval(checkForMail, 1000); // ...and when it is no longer needed: clearInterval(timerId); // A handler: stays on a long-lived target until it is removed window.addEventListener('scroll', onScroll); // ...and when it is no longer needed: window.removeEventListener('scroll', onScroll);

The handler case is the one that catches people. Each time a view opens, it adds a listener to window or document. If the view closes without removing it, the listener stays on the long-lived object, and it keeps alive everything its function can reach, which through a closure can include the view's elements after they have left the document. Opening and closing that view a hundred times leaves a hundred listeners. removeEventListener needs the same function reference you added, so an anonymous function cannot be removed that way. A listener's signal option offers another way out, removing every listener tied to a controller with one call.

let controller = new AbortController(); window.addEventListener('scroll', onScroll, { signal: controller.signal }); window.addEventListener('resize', onResize, { signal: controller.signal }); // When the view closes, both listeners are removed controller.abort();

For a cache that should not hold its entries alive, WeakMap holds its object keys weakly, so an entry can go once nothing else refers to the key. To find a leak, use the memory tools in your browser's DevTools, and compare heap snapshots taken before and after repeating the action you suspect.

11. Summary

ConceptKey points
ArraysOrdered, with mixed content, including functions. Use the literal, because new Array(10) is ten empty slots. typeof says object, so use Array.isArray(). length is one more than the highest index.
ObjectsA less ordered collection of data and functions. Reached with obj.prop or obj['prop']. A missing property is undefined, with no error.
PrimitivesNot objects. They are wrapped temporarily for a method call. typeof null is a historical quirk.
How much OOPYes-ish, if it makes sense. Suspect at the very small, with trade-offs at scale. Watch for the "one true way" fallacy.
Properties and methodsAdded and deleted per object. Use brackets for names in variables, names with special characters, and symbols. A method is a property holding a function.
Enumerationfor/in visits enumerable string keys, inherited ones too. Object.keys(), values() and entries() stay with the object's own.
ReferencesAssignment and argument passing copy the reference, giving two names for one object. == and === compare identity and not contents.
Literals{ name: value } is the usual way to make an object. Shorthands cover methods, same-named variables and computed keys. A plain object works as an associative array, with inherited names as the catch.
Optional chaining?. evaluates to undefined when the left side is null or undefined. Use it where absence is legitimate.
SpreadExpands an array into arguments or an array, and copies properties into an object. Rest collects, spread expands. Copies are shallow, and structuredClone() goes deep for data.
thisSet by the call. A method call gives the object to the left. A plain call gives the global object, or undefined in strict mode. Detaching a method loses its object. Arrow functions use the surrounding this.
Garbage collectionUnreachable objects get collected, on a schedule you cannot control. null removes one reference. Leaks are memory still reachable, usually through dangling references, timers and event handlers.