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.
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].
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().
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
| Concept | Key points |
|---|---|
| Arrays | Ordered, 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. |
| Objects | A less ordered collection of data and functions. Reached with obj.prop or obj['prop']. A missing property is undefined, with no error. |
| Primitives | Not objects. They are wrapped temporarily for a method call. typeof null is a historical quirk. |
| How much OOP | Yes-ish, if it makes sense. Suspect at the very small, with trade-offs at scale. Watch for the "one true way" fallacy. |
| Properties and methods | Added and deleted per object. Use brackets for names in variables, names with special characters, and symbols. A method is a property holding a function. |
| Enumeration | for/in visits enumerable string keys, inherited ones too. Object.keys(), values() and entries() stay with the object's own. |
| References | Assignment 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. |
| Spread | Expands 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. |
| this | Set 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 collection | Unreachable 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. |