Skip to content

Latest commit

 

History

History
709 lines (476 loc) · 38.7 KB

File metadata and controls

709 lines (476 loc) · 38.7 KB

You Don't Know JS Yet: Objects & Classes - 2nd Edition

Chapter 1: Object Foundations

NOTE:
Work in progress

Everything in JS is an object.

This is one of the most pervasive, but most incorrect, "facts" that perpetually circulates about JS. Let the myth busting commence.

JS definitely has objects, but that doesn't mean that all values are objects. Nevertheless, objects are arguably the most important (and varied!) value type in the language, so mastering them is critical to your JS journey.

The object mechanism is certainly the most flexible and powerful container type -- something you put other values into; every JS program you write will use them in one way or another. But that's not why objects deserve top billing for this book. Objects are the foundation for the second of JS's three pillars: the prototype.

Why are prototypes (along with the this keyword, covered later in the book) so core to JS as to be one of its three pillars? Among other things, prototypes are how JS's object system can express the class design pattern, one of the most widely relied on design patterns in all of programming.

So our journey here will start with objects, build up a compelete understanding of prototypes, de-mystify the this keyword, and explore the class system.

About This Book

Welcome to book 3 in the You Don't Know JS Yet series! If you already finished Get Started (the first book) and Scope & Closures (the second book), you're in the right spot! If not, before you proceed I encourage you to read those two as foundations before diving into this book.

The first edition of this book is titled, "this & Object Prototypes". In that book, our focus started with the this keyword, as it's arguably one of the most confused topics in all of JS. The book then spent the majority of its time focused on expositing the prototype system and advocating for embrace of the lesser-known "delegation" pattern instead of class designs. At the time of that book's writing (2014), ES6 would still be almost 2 years to its completion, so I felt the early sketches of the class keyword only merited a brief addendum of coverage.

It's quite an understatement to say a lot has changed in the JS landscape in the almost 8 years since that book. ES6 is old news now; at the time of this book's writing, JS has seen 7 yearly updates after ES6 (ES2016 through ES2022).

Now, we still need to talk about how this works, and how that relates to methods invoked against various objects. And class actually operates (mostly!) via the prototype chain deep under the covers. But JS developers in 2022 are almost never writing code to explicitly wire up prototypal inheritance anymore. And as much as I personally wish differently, class design patterns -- not "behavior delegation" -- are how the majority of data and behavior organization (data structures) in JS are expressed.

This book reflects JS's current reality: thus the new sub-title, new organization and focus of topics, and complete re-write of the previous edition's text.

Objects As Containers

One common way of gathering up multiple values in a single container is with an object. Objects are collections of key/value pairs. There are also sub-types of object in JS with specialized behaviors, such as arrays (numerically indexed) and even functions (callable); more on these sub-types later.

NOTE:
Keys are often referred to as "property names", with the pairing of a property name and a value often called a "property". This book will use those terms distinctly in that manner.

Regular JS objects are typically declared with literal syntax, like this:

myObj = {
    // ..
};

Note: There's an alternate way to create an object (using myObj = new Object()), but this is not common or preferred, and is almost never the appropriate way to go about it. Stick with object literal syntax.

It's easy to get confused what pairs of { .. } mean, since JS overloads the curly brackets to mean any of the following, depending on the context used:

  • delimit values, like object literals
  • define object destructuring patterns (more on this later)
  • delimit interpolated string expressions, like `some ${ getNumber() } thing`
  • define blocks, like on if and for loops
  • define function bodies

Though it can sometimes be challenging as you read code, look for whether a { .. } curly brace pair is used in the program where a value/expression is valid to appear; if so, it's an object literal, otherwise it's one of the other overloaded uses.

Defining Properties

Inside the object literal curly braces, you define properties (name and value) with propertyName: propertyValue pairs, like this:

myObj = {
    favoriteNumber: 42,
    isDeveloper: true,
    firstName: "Kyle"
};

The values you assign to the properties can be literals, as shown, or can be computed by expression:

function twenty() { return 20; }

myObj = {
    favoriteNumber: (twenty() + 1) * 2,
};

The expression (twenty() + 1) * 2 is evaluated immediately, with the result (42) assigned as the property value.

Developers sometimes wonder if there's a way to define an expression for a property value where the expression is "lazy", meaning it's not computed at the time of assignment, but defined later. JS does not have lazy expressions, so the only way to do so is for the expression to be wrapped in a function:

function twenty() { return 20; }
function myNumber() { return (twenty() + 1) * 2; }

myObj = {
    favoriteNumber: myNumber   // notice, NOT `myNumber()` as a function call
};

In this case, favoriteNumber is not holding a numeric value, but rather a function reference. To compute the result, that function reference must be explicitly executed.

Looks Like JSON?

You may notice that this object-literal syntax we've seen thus far resembles a related syntax, "JSON" (JavaScript Object Notation):

{
    "favoriteNumber": 42,
    "isDeveloper": true,
    "firstName": "Kyle"
}

The biggest differences between JS's object literals and JSON are, for objects defined as JSON:

  1. property names must be quoted with " double-quote characters

  2. property values must be literals (either primitives, objects, or arrays), not arbitrary JS expressions

In JS programs, an object literal does not require quoted property names -- you can quote them (' or " allowed), but it's usually optional. There are however characters that are valid in a property name, but which cannot be included without surrounding quotes; for example, leading numbers or whitespace:

myObj = {
    favoriteNumber: 42,
    isDeveloper: true,
    firstName: "Kyle",
    "2 nicknames": [ "getify", "ydkjs" ]
};

One other minor difference is, JSON syntax -- that is, text that will be parsed as JSON, such as from a .json file -- is stricter than general JS. For example, JS allows comments (// .. and /* .. */), and trailing , commas in object and array expressions; JSON does not allow any of these. Thankfully, JSON does still allow arbitrary whitespace.

Property Names

Property names in object literals are almost always treated/coeced as string values. One exception to this is for integer (or "integer looking") property "names":

anotherObj = {
    42:       "<-- this property name will be treated as an integer",
    "41":     "<-- ...and so will this one",

    true:     "<-- this property name will be treated as a string",
    [myObj]:  "<-- ...and so will this one"
};

The 42 property name will be treated as an integer property name (aka, index); the "41" string value will also be treated as such since it looks like an integer. By contrast, the true value will become the string property name "true", and the myObj identifier reference, computed via the surrounding [ .. ], will coerce the object's value to a string (generally the default "[object Object]").

WARNING:
If you need to actually use an object as a key/property name, never rely on this computed string coercion; its behavior is surprising and almost certainly not what's expected, so program bugs are likely to occur. Instead, use a more specialized data structure, called a Map (added in ES6), where objects used as property "names" are left as-is instead of being coerced to a string value.

As with [myObj] above, you can compute any property name (distinct from computing the property value) at the time of object literal definition:

anotherObj = {
    ["x" + (21 * 2)]: true
};

The expression "x" + (21 * 2), which must appear inside of [ .. ] brackets, is computed immediately, and the result ("x42") is used as the property name.

Symbols As Property Names

ES6 added a new primitive value type of Symbol, which is often used as a special property name for storing and retrieving property values. They're created via the Symbol(..) function call (without the new keyword), which accepts an optional description string used only for friendlier debugging purposes; if specified, the description is inaccessible to the JS program and thus not used for any other purpose than debug output.

myPropSymbol = Symbol("optional, developer-friendly description");
NOTE:
Symbols are sort of like numbers or strings, except that their value is opaque to, and globally unique within, the JS program. In other words, you can create and use symbols, but JS doesn't let you know anything about, or do anything with, the underlying value; that's kept as a hidden implementation detail by the JS engine.

Computed property names, as previously described, are how to define a symbol property name on an object literal:

myPropSymbol = Symbol("optional, developer-friendly description");

anotherObj = {
    [myPropSymbol]: "Hello, symbol!"
};

The computed property name used to define the property on anotherObj will be the actual primitive symbol value (whatever it is), not the optional description string ("optional, developer-friendly description").

Because symbols are globally unique in your program, there's no chance of accidental collision where one part of the program might accidentally define a property name the same as another part of the program tried defined/assigned.

Symbols are also useful to hook into special default behaviors of objects, and we'll cover that in more detail in "Extending the MOP" in the next chapter.

Concise Properties

When defining an object literal, it's common to use a property name that's the same as an existing in-scope identifier that holds the value you want to assign.

coolFact = "the first person convicted of speeding was going 8 mph";

anotherObj = {
    coolFact: coolFact
};
NOTE:
That would have been the same thing as the quoted property name definition "coolFact": coolFact, but JS developers rarely quote property names unless strictly necessary. Indeed, it's idiomatic to avoid the quotes unless required, so it's discouraged to include them unnecessarily.

In this situation, where the property name and value expression identifier are identical, you can omit the property-name portion of the property definition, as a so-called "concise property" definition:

coolFact = "the first person convicted of speeding was going 8 mph";

anotherObj = {
    coolFact   // <-- concise property short-hand
};

The property name is "coolFact" (string), and the value assigned to the property is what's in the coolFact variable at that moment: "the first person convicted of speeding was going 8 mph".

At first, this shorthand convenience may seem confusing. But as you get more familiar with seeing this very common and popular feature being used, you'll likely favor it for typing (and reading!) less.

Concise Methods

Another similar shorthand is defining functions/methods in an object literal using a more concise form:

anotherObj = {
    // standard function property
    greet: function() { console.log("Hello!"); },

    // concise function/method property
    greet2() { console.log("Hello, friend!"); }
};

While we're on the topic of concise method properties, we can also define generator functions (another ES6 feature):

anotherObj = {
    // instead of:
    //   greet3: function*() { yield "Hello, everyone!"; }

    // concise generator method
    *greet3() { yield "Hello, everyone!"; }
};

And though it's not particularly common, concise methods/generators can even have quoted or computed names:

anotherObj = {
    "greet-4"() { console.log("Hello, audience!"); },

    // concise computed name
    [ "gr" + "eet 5" ]() { console.log("Hello, audience!"); },

    // concise computed generator name
    *[ "ok, greet 6".toUpperCase() ]() { yield "Hello, audience!"; }
};

Object Spread

Another way to define properties at object literal creation time is with a form of the ... syntax -- it's not technically an operator, but it certainly seems like one -- often referred to as "object spread".

The ... when used inside an object literal will "spread" out the contents (properties, aka key/value pairs) of another object value into the object being defined:

anotherObj = {
    favoriteNumber: 12,

    ...myObj,   // object spread, shallow copies `myObj`

    greeting: "Hello!"
}

The spreading of myObj's properties is shallow, in that it only copies the top-level properties from myObj; any values those properties hold are simply assigned over. If any of those values are references to other objects, the references themselves are assigned (by copy), but the underlying object values are not duplicated -- so you end up with multiple shared references to the same object(s).

You can think of object spreading like a for loop that runs through the properties one at a time and does an = style assignment from the source object (myObj) to the target object (anotherObj).

Also, consider these property definition operations to happen "in order", from top to bottom of the object literal. In the above snippet, since myObj has a favoriteNumber property, the object spread will end up overwriting the favoriteNumber: 12 property assignment from the previous line. Moreover, if myObj had contained a greeting property that was copied over, the next line (greeting: "Hello!") would override that property definition.

NOTE:
Object spread also only copies owned properties (those directly on the object) that are enumerable (allowed to be enumerated/listed). It does not duplicate the property -- as in, actually mimic the property's exact characteristics -- but rather do a simple assignment style copy. We'll cover more such details in the "Property Descriptors" section of the next chapter.

A common way ... object spread is used is for performing shallow object duplication:

myObjShallowCopy = { ...myObj };

Keep in mind you cannot ... spread into an existing object value; the ... object spread syntax can only appear inside the { .. } object literal, which is creating a new object value. To perform a similar shallow object copy but with APIs instead of syntax, see the "Object Entries" section later in this chapter (with coverage of Object.entries(..) and Object.fromEntries(..)).

But if you instead want to copy object properties (shallowly) into an existing object, see the "Assigning Properties" section later in this chapter (with coverage of Object.assign(..)).

Deep Object Copy

Also, since ... doesn't do full, deep object duplication, the object spread is generally only suitable for duplicating objects that hold simple, primitive values only, not references to other objects.

Deep object duplication is an incredibly complex and nuanced operation. Duplicating a value like 42 is obvious and straightforward, but what does it mean to copy a function (which is a special kind of object, also held by reference), or to copy an external (not entirely in JS) object reference, such as a DOM element? And what happens if an object has circular references (like where a nested descendant object holds a reference back up to an outer ancestor object)? There's a variety of opinions in the wild about how all these corner cases should be handled, and thus no single standard exists for deep object duplication.

For deep object duplication, the standard approaches have been:

  1. Use a library utility that declares a specific opinion on how the duplication behaviors/nuances should be handled.

  2. Use the JSON.parse(JSON.stringify(..)) round-trip trick -- this only "works" correctly if there are no circular references, and if there are no values in the object that cannot be properly serialized with JSON (such as functions).

Recently, though, a third option has landed. This is not a JS feature, but rather a companion API provided to JS by environments like the web platform. Objects can be deep copied now using structuredClone(..)1.

myObjCopy = structuredClone(myObj);

The underlying algorithm behind this built-in utility supports duplicating circular references, as well as many more types of values than the JSON round-trip trick. However, this algorithm still has its limits, including no support for cloning functions or DOM elements.

Accessing Properties

Property access of an existing object is preferably done with the . operator:

myObj.favoriteNumber;    // 42
myObj.isDeveloper;       // true

If it's possible to access a property this way, it's strongly suggested to do so.

If the property name contains characters that cannot appear in identifiers, such as leading numbers or whitespace, [ .. ] brackets can be used instead of the .:

myObj["2 nicknames"];    // [ "getify", "ydkjs" ]
anotherObj[42];          // "<-- this property name will..."
anotherObj["41"];        // "<-- this property name will..."

Even though numeric property "names" remain as numbers, property access via the [ .. ] brackets will coerce a string representation to a number (e.g., "42" as the 42 numeric equivalent), and then access the associated numeric property accordingly.

Similar to the object literal, the property name to access can be computed via the [ .. ] brackets. The expression can be a simple identifier:

propName = "41";
anotherObj[propName];

Actually, what you put between the [ .. ] brackets can be any arbitrary JS expression, not just identifiers or literal values like 42 or "isDeveloper". JS will first evaluate the expression, and the resulting value will then be used as the property name to look up on the object:

function howMany(x) {
    return x + 1;
}

myObj[`${ howMany(1) } nicknames`];   // [ "getify", "ydkjs" ]

In this snippet, the expression is a back-tick delimited `template string literal` with an interpolated expression of the function call howMany(1). The overall result of that expression is the string value "2 nicknames", which is then used as the property name to access.

Object Entries

You can get a listing of the properties in an object, as an array of tuples (two-element sub-arrays) holding the property name and value:

myObj = {
    favoriteNumber: 42,
    isDeveloper: true,
    firstName: "Kyle"
};

Object.entries(myObj);
// [ ["favoriteNumber",42], ["isDeveloper",true], ["firstName","Kyle"] ]

Added in ES6, Object.entries(..) retrieves this list of entries -- containing only owned an enumerable properties; see the "Property Descriptors" section in the next chapter -- from a source object.

Such a list can be looped/iterated over, potentially assigning properties to another existing object. However, it's also possible to create a new object from a list of entries, using Object.fromEntries(..) (added in ES2019):

myObjShallowCopy = Object.fromEntries( Object.entries(myObj) );

// alternate approach to the earlier discussed:
// myObjShallowCopy = { ...myObj };

Destructuring

Another approach to accessing properties is through object destructuring (added in ES6). Think of destructuring as defining a "pattern" that describes what an object value is supposed to "look like" (structurally), and then asking JS to follow that "pattern" to systematically access the contents of an object value.

The end result of object destructuring is not another object, but rather one or more assignments to other targets (variables, etc) of the values from the source object.

Imagine this sort of pre-ES6 code:

myObj = {
    favoriteNumber: 42,
    isDeveloper: true,
    firstName: "Kyle"
};

const favoriteNumber = (
    myObj.favoriteNumber !== undefined ? myObj.favoriteNumber : 42
);
const isDev = myObj.isDeveloper;
const firstName = myObj.firstName;
const lname = (
    myObj.lastName !== undefined ? myObj.lastName : "--missing--"
);

Those accesses of the property values, and assignments to other identifiers, is generally called "manual destructuring". To use the declarative object destructuring syntax, it might look like this:

myObj = {
    favoriteNumber: 42,
    isDeveloper: true,
    firstName: "Kyle"
};

const { favoriteNumber = 12 } = myObj;
const {
    isDeveloper: isDev,
    firstName: firstName,
    lastName: lname = "--missing--"
} = myObj;

favoriteNumber;   // 42
isDev;            // true
firstName;        // "Kyle"
lname;            // "--missing--"

As shown, the { .. } object destucturing resembles an object literal value definition, but it appears on the left-hand side of the = operator rather than on the right-hand side where an object value expression would appear. That makes the { .. } on the left-hand side a destructuring pattern rather than another object definition.

The { favoriteNumber } = myObj destructuring tells JS to find a property named favoriteNumber on the object, and to assign its value to an identifier of the same name. The single instance of the favoriteNumber identifier in the pattern is similar to "concise properties" as discussed earlier in this chapter: if the source (property name) and target (identifier) are the same, you can omit one of them and only list it once.

The = 12 part tells JS to provide 12 as a default value for the assignment to favoriteNumber, if the source object either doesn't have a favoriteNumber property, or if the property holds an undefined value.

In the second destructuring pattern, the isDeveloper: isDev pattern is instructing JS to find a property named isDeveloper on the source object, and assign its value to an identifier named isDev. It's sort of a "renaming" of the source to the target. By contrast, firstName: firstName is providing the source and target for an assignment, but is redundant since they're identical; a single firstName would have sufficed here, and is generally more preferred.

The lastName: lname = "--missing--" combines both source-target renaming and a default value (if the lastName source property is missing or undefined).

The above snippet combines object destructuring with variable declarations -- in this example, const is used, but var and let work as well -- but it's not inherently a declaration mechanism. Destructuring is about access and assignment (source to target), so it can operate against existing targets rather than declaring new ones:

let fave;

// surrounding ( ) are required syntax here,
// when a declarator is not used
({ favoriteNumber: fave } = myObj);

fave;  // 42

Object destructuring syntax is generally preferred for its declarative and more readable style, over the heavily imperative pre-ES6 equivalents. But don't go overboard with destructuring. Sometimes just doing x = someObj.x is perfectly fine!

Conditional Property Access

Recently (in ES2020), a feature known as "optional chaining" was added to JS, which augments property access capabilities (especially nested property access). The primary form is the two-character compound operator ?., like A?.B.

This operator will check the left-hand side reference (A) to see if it's null'ish (null or undefined). If so, the rest of the property access expression is short-circuited (skipped), and undefined is returned as the result (even if it was null that was actually encountered!). Otherwise, ?. will access the property just as a normal . operator would.

For example:

myObj?.favoriteNumber

Here, the null'ish check is performed against the myObj, meaning that the favoriteNumber property access is only performed if the value in myObj is non-null'ish. Note that it doesn't verify that myObj is actually holding a real object, only that it's non-nullish. However, all non-nullish values can "safely" (no JS exception) be "accessed" via the . operator, even if there's no matching property to retrieve.

It's easy to get confused into thinking that the null'ish check is against the favoriteNumber property. But one way to keep it straight is to remember that the ? is on the side where the safety check is performed, while the . is on the side that is only conditionally evaluated if the non-null'ish check passes.

Typically, the ?. operator is used in nested property accesses that may be 3 or more levels deep, such as:

myObj?.address?.city

The equivalent operation with the ?. operator would look like this:

(myObj != null && myObj.address != null) ? myObj.address.city : undefined

Again, remember that no check has been performed against the right-most property (city) here.

Also, the ?. should not universally be used in place of every single . operator in your programs. You should endeavor to know if a . property access will succeed or not before making the access, whenever possible. Use ?. only when the nature of the values being accessed is subject to conditions that cannot be predicted/controlled.

For example, in the previous snippet, the myObj?. usage is probably mis-guided, because it really shouldn't be the case that you start a chain of property access against a variable that might not even hold a top-level object (aside from its contents potentially missing certain properties in certain conditions).

Instead, I would recommend usage more like this:

myObj.address?.city

And that expression should only be used in part of your program where you're sure that myObj is at least holding a valid object (whether or not it has an address property with a sub-object in it).

Another form of the "optional chaining" operator is ?.[, which is used when the property access you want to make conditional/safe requires a [ .. ] bracket.

myObj["2 nicknames"]?.[0];   // "getify"

Everything asserted about how ?. behaves goes the same for ?.[.

WARNING:
There's a third form of this feature, named "optional call", which uses ?.( as the operator. It's used for performing a non-null'ish check on a property before executing the function value in the property. For example, instead of myObj.someFunc(42), you can do myObj.someFunc?.(42). The ?.( checks to make sure myObj.someFunc is non-null'ish before invoking it (with the (42) part). While that may sound like a useful feature, I think this is dangerous enough to warrant complete avoidance of this form/construct.

My concern is that ?.( makes it seem as if we're ensuring that the function is "callable" before calling it, when in fact we're only checking if it's non-null'ish. Unlike ?. which can allow a "safe" . access against a non-null'ish value that's also not an object, the ?.( non-null'ish check isn't similarly "safe". If the property in question has any non-null'ish, non-function value in it, like true or "Hello", the (42) call part will be invoked and yet throw a JS exception. So in other words, this form is unfortunately masquerading as more "safe" than it actually is, and should thus be avoided in essentially all circumstances. If a property value can ever not be a function, do a more fullsome check for its function'ness before trying to invoke it. Don't pretend that ?.( is doing that for you, or future readers/maintainers of your code (including your future self!) will likely regret it.

Accessing Properties On Non-Objects

This may sound counter-intuitive, but you can generally access properties/methods from values that aren't themselves objects:

fave = 42;

fave;              // 42
fave.toString();   // "42"

Here, fave holds a primitive 42 number value. So how can we do .toString to access a property from it, and then () to invoke the function held in that property?

This is a tremendously more indepth topic than we'll get into in this book; see book 4, "Types & Grammar", of this series for more. However, as a quick glimpse: if you perform a property access (. or [ .. ]) against a non-object, non-null'ish value, JS will by default (temporarily!) coerce the value into an object-wrapped representation, allowing the property access against that implicitly instantiated object.

This process is typically called "boxing", as in putting a value inside a "box" (object container).

So in the above snippet, just for the moment that .toString is being accessed on the 42 value, JS will box this value into a Number object, and then perform the property access.

Note that null and undefined can be object-ified, by calling Object(null) / Object(undefined). However, JS does not automatically box these null'ish values, so property access against them will fail (as discussed earlier in the "Conditional Property Access" section).

NOTE:
Boxing has a counterpart: unboxing. For example, the JS engine will take an object wrapper -- like a Number object wrapped around 42 -- created with Number(42) or Object(42) -- and unwrap it to retrieve the underlying primitive 42, whenever a mathematical operation (like * or -) encounters such an object. Unboxing behavior is way out of scope for our discussion, but is covered fully in the aforementioned "Types & Grammar" title.

Assigning Properties

Whether a property is defined at the time of object literal definition, or added later, the assignment of a property value is done with the = operator, as any other normal assignment would be:

myObj.favoriteNumber = 123;

If the favoriteNumber property doesn't already exist, that statement will create a new property of that name and assign its value. But if it already exists, that statement will re-assign its value.

WARNING:
An = assignment to a property may fail (silently or throwing an exception), or it may not directly assign the value but instead invoke a setter function that performs some operation(s). More details on these behaviors in the next chapter.

It's also possible to assign one or more properties at once -- assuming the source properties (name and value pairs) are in another object -- using the Object.assign(..) (added in ES6) method:

// shallow copy all (owned and enumerable) properties
// from `myObj` into `anotherObj`
Object.assign(anotherObj,myObj);

Object.assign(
    /*target=*/anotherObj,
    /*source1=*/{
        someProp: "some value",
        anotherProp: 1001,
    },
    /*source2=*/{
        yetAnotherProp: false
    }
);

Object.assign(..) takes the first object as target, and the second (and optionally subsequent) object(s) as source(s). Copying is done in the same manner as described earlier in the "Object Spread" section.

Deleting Properties

Once a property is defined on an object, the only way to remove it is with the delete operator:

anotherObj = {
    counter: 123
};

anotherObj.counter;   // 123

delete anotherObj.counter;

anotherObj.counter;   // undefined

Contrary to common misconception, the JS delete operator does not directly do any deallocation/freeing up of memory, through garbage collection (GC). The only thing it does is remove a property from an object. If the value in the property was a reference (to another object/etc), and there are no other surviving references to that value once the property is removed, that value would likely then be eligible for removal in a future sweep of the GC.

Calling delete on anything other than an object property is a misuse of the delete operator, and will either fail silently (in non-strict mode) or throw an exception (in strict mode).

Deleting a property from an object is distinct from assigning it a value like undefined or null. A property assigned undefined, either initially or later, is still present on the object, and might still be revealed when enumerating the contents

Determining Container Contents

You can determine an object's contents in a variety of ways. To ask an object if it has a specific property:

myObj = {
    favoriteNumber: 42,
    coolFact: "the first person convicted of speeding was going 8 mph",
    beardLength: undefined,
    nicknames: [ "getify", "ydkjs" ]
};

"favoriteNumber" in myObj;            // true

myObj.hasOwnProperty("coolFact");     // true
myObj.hasOwnProperty("beardLength");  // true

myObj.nicknames = undefined;
myObj.hasOwnProperty("nicknames");    // true

delete myObj.nicknames;
myObj.hasOwnProperty("nicknames");    // false

There is an important difference between how the in operator and the hasOwnProperty(..) method behave. The in operator will check not only the target object specified, but if not found there, it will also consult the object's [[Prototype]] chain (covered in the next chapter). By contrast, hasOwnProperty(..) only consults the target object.

If you're paying close attention, you may have noticed that myObj appears to have a method property called hasOwnProperty(..) on it, even though we didn't define such. That's because hasOwnProperty(..) is defined as a built-in on Object.prototype, which by default is "inherited by" all normal objects. There is risk inherent to accessing such an "inherited" method, though. Again, more on prototypes in the next chapter.

Better Existence Check

ES2022 (almost official at time of writing) has already settled on a new feature, Object.hasOwn(..). It does essentially the same thing as hasOwnProperty(..), but it's invoked as a static helper external to the object value instead of via the object's [[Prototype]], making it safer and more consistent in usage:

// instead of:
myObj.hasOwnProperty("favoriteNumber")

// we should now prefer:
Object.hasOwn(myObj,"favoriteNumber")

Even though (at time of writing) this feature is just now emerging in JS, there are polyfills that make this API available in your programs even when running in a previous JS environment that doesn't yet have the feature defined. For example, a quick stand-in polyfill sketch:

// simple polyfill sketch for `Object.hasOwn(..)`
if (!Object.hasOwn) {
    Object.hasOwn = function hasOwn(obj,propName) {
        return Object.prototype.hasOwnProperty.call(obj,propName);
    };
}

Including a polyfill patch such as that in your program means you can safely start using Object.hasOwn(..) for property existence checks no matter whether a JS environment has Object.hasOwn(..) built in yet or not.

Listing All Container Contents

We already discussed the Object.entries(..) API earlier, which tells us what properties an object has (as long as they're enumerable -- more in the next chapter).

There's a variety of other mechanisms available, as well. Object.keys(..) gives us list of the enumerable property names (aka, keys) in an object -- names only, no values; Object.values(..) instead gives us list of all values held in enumerable properties.

But what if we wanted to get all the keys in an object (enumerable or not)? Object.getOwnPropertyNames(..) seems to do what we want, in that it's like Object.keys(..) but also returns non-enumerable property names. However, this list will not include any Symbol property names, as those are treated as special locations on the object. Object.getOwnPropertySymbols(..) returns all of an object's Symbol properties. So if you concatenate both of those lists together, you'd have all the direct (owned) contents of an object.

Yet as we've implied several times already, and will cover in full detail in the next chapter, an object can also "inherit" contents from its [[Prototype]] chain. These are not considered owned contents, so they won't show up in any of these lists.

Recall that the in operator will potentially traverse the entire chain looking for the existence of a property. Similarly, a for..in loop will traverse the chain and list any enumerable (owned or inherited) properties. But there's no built-in API that will traverse the whole chain and return a list of the combined set of both owned and inherited contents.

Temporary Containers

Using a container to hold multiple values is sometimes just a temporary transport mechanism, such as when you want to pass multiple values to a function via a single argument, or when you want a function to return multiple values:

function formatValues({ one, two, three }) {
    // the actual object passed in as an
    // argument is not accessible, since
    // we destructured it into three
    // separate variables

    one = one.toUpperCase();
    two = `--${two}--`;
    three = three.substring(0,5);

    // this object is only to transport
    // all three values in a single
    // return statement
    return { one, two, three };
}

// destructuring the return value from
// the function, because that returned
// object is just a temporary container
// to transport us multiple values
const { one, two, three } =

    // this object argument is a temporary
    // transport for multiple input values
    formatValues({
       one: "Kyle",
       two: "Simpson",
       three: "getify"
    });

one;     // "KYLE"
two;     // "--Simpson--"
three;   // "getif"

The object literal passed into formatValues(..) is immediately parameter destructured, so inside the function we only deal with three separate variables (one, two, and three). The object literal returned from the function is also immediately destructured, so again we only deal with three separate variables (one, two, three).

This snippet illustrates the idiom/pattern that an object is sometimes just a temporary transport container rather than a meaningful value in and of itself.

Containers Are Collections Of Properties

The most common usage of objects is as containers for multiple values. We create and manage property container objects by:

  • defining properties (named locations), either at object creation time or later
  • assigning values, either at object creation time or later
  • accessing values later, using the location names (property names)
  • deleting properties via delete
  • determining container contents with in, hasOwnProperty(..) / hasOwn(..), Object.entries(..) / Object.keys(..), etc

But there's a lot more to objects than just static collections of property names and values. In the next chapter, we'll dive under the hood to look at how they actually work.

Footnotes

  1. "Structured Clone Algorithm", HTML Specification; https://html.spec.whatwg.org/multipage/structured-data.html#structured-cloning ; Accessed July 2022