This chapter is marked as “work in progress” by the author.
Chapter 2: How Objects Work
Objects are not just containers for multiple values, though clearly that’s the context for most interactions with objects. To fully understand the object mechanism in JS, and get the most out of using objects in our programs, we need to look more closely at a number of characteristics of objects (and their properties) which can affect their behavior when interacting with them. These characteristics that define the underlying behavior of objects are collectively referred to in formal terms as the “metaobject protocol” (MOP). The MOP is useful not only for understanding how objects will behave, but also for overriding the default behaviors of objects to bend the language to fit our program’s needs more fully.Property Descriptors
Each property on an object is internally described by what’s known as a “property descriptor”. This is, itself, an object (aka, “metaobject”) with several properties (aka “attributes”) on it, dictating how the target property behaves. We can retrieve a property descriptor for any existing property usingObject.getOwnPropertyDescriptor(..) (ES5):
Object.defineProperty(..) (ES5):
configurable: false in its descriptor), it can always be re-defined/overwritten using Object.defineProperty(..).
Though it seems far less common out in the wild, we can even define multiple properties at once, each with their own descriptor:
Accessor Properties
A property descriptor usually defines avalue property, as shown above. However, a special kind of property, known as an “accessor property” (aka, a getter/setter), can be defined. For a property like this, its descriptor does not define a fixed value property, but would instead look something like this:
obj.prop), but under the covers it invokes the get() method as defined; it’s sort of like if you had called obj.prop(). A setter looks like a property assignment (obj.prop = value), but it invokes the set(..) method as defined; it’s sort of like if you had called obj.prop(value).
Let’s illustrate a getter/setter accessor property:
Enumerable, Writable, Configurable
Besidesvalue or get() / set(..), the other 3 attributes of a property descriptor are:
-
enumerable- Controls whether the property will appear in various enumerations of object properties, such asObject.keys(..),Object.entries(..),for..inloops, and the copying that occurs with the...object spread andObject.assign(..). Most properties should be left enumerable, but you can mark certain special properties on an object as non-enumerable if they shouldn’t be iterated/copied. -
writable- Controls whether avalueassignment (via=) is allowed. To make a property “read only”, define it withwritable: false. However, as long as the property is still configurable,Object.defineProperty(..)can still change the value by settingvaluedifferently. -
configurable- Controls whether a property’s descriptor can be re-defined/overwritten. A property that’sconfigurable: falseis locked to its definition, and any further attempts to change it withObject.defineProperty(..)will fail. A non-configurable property can still be assigned new values (via=), as long aswritable: trueis still set on the property’s descriptor.
Object Sub-Types
There are a variety of specialized sub-types of objects in JS. But by far, the two most common ones you’ll interact with are arrays andfunctions.
By “sub-type”, we mean the notion of a derived type that has inherited the behaviors from a parent type but then specialized or extended those behaviors. In other words, values of these sub-types are fully objects, but are also more than just objects.
Arrays
Arrays are objects that are specifically intended to be numerically indexed, rather than using string named property locations. They are still objects, so a named property likefavoriteNumber is legal. But it’s greatly frowned upon to mix named properties into numerically indexed arrays.
Arrays are preferably defined with literal syntax (similar to objects), but with the [ .. ] square brackets rather than { .. } curly brackets:
0, not 1:
length property, which is automatically kept updated with the “length” of the array.
Empty Slots
JS arrays also have a really unfortunate “flaw” in their design, referred to as “empty slots”. If you assign an index of an array more than one position beyond the current end of the array, JS will leave the in between slots “empty” rather than auto-assigning them toundefined as you might expect:
Functions
I don’t have much specifically to say about functions here, other than to point out that they are also sub-object-types. This means that in addition to being executable, they can also have named properties added to or accessed from them. Functions have two pre-defined properties you may find yourself interacting with, specifically for meta-programming purposes:length of a function is the count of its explicitly defined parameters, up to but not including a parameter that either has a default value defined (e.g., param = 42) or a “rest parameter” (e.g., ...remainingOpts).
Avoid Setting Function-Object Properties
You should avoid assigning properties on function objects. If you’re looking to store extra information associated with a function, use a separateMap(..) (or WeakMap(..)) with the function object as the key, and the extra information as the value.
Object Characteristics
In addition to defining behaviors for specific properties, certain behaviors are configurable across the whole object:- extensible
- sealed
- frozen
Extensible
Extensibility refers to whether an object can have new properties defined/added to it. By default, all objects are extensible, but you can change shut off extensibility for an object:[[Prototype]] Chain
One of the most important, but least obvious, characteristics of an object (part of the MOP) is referred to as its “prototype chain”; the official JS specification notation is [[Prototype]]. Make sure not to confuse this [[Prototype]] with a public property named prototype. Despite the naming, these are distinct concepts.
The [[Prototype]] is an internal linkage that an object gets by default when its created, pointing to another object. This linkage is a hidden, often subtle characteristic of an object, but it has profound impacts on how interactions with the object will play out. It’s referred to as a “chain” because one object links to another, which in turn links to another, … and so on. There is an end or top to this chain, where the linkage stops and there’s no further to go.
By default, all objects are [[Prototype]]-linked to the built-in object named Object.prototype.
Let’s consider some code:
[[Prototype]]) to that automatically built-in, but weirdly named, Object.prototype object.
When we do things like:
[[Prototype]] linkage, without really realizing it. Since myObj does not have toString or hasOwnProperty properties defined on it, those property accesses actually end up DELEGATING the access to continue its lookup along the [[Prototype]] chain.
Since myObj is [[Prototype]]-linked to the object named Object.prototype, the lookup for toString and hasOwnProperty properties continues on that object; and indeed, these methods are found there!
The ability for myObj.toString to access the toString property even though it doesn’t actually have it, is commonly referred to as “inheritance”, or more specifically, “prototypal inheritance”. The toString and hasOwnProperty properties, along with many others, are said to be “inherited properties” on myObj.
I have a lot of frustrations with the usage of the word “inheritance” here — it should be called “delegation”! — but that’s what most people refer to it as, so we’ll begrudgingly comply and use that same terminology for now (albeit under protest, with ” quotes). I’ll save my objections for an appendix of this book.
Object.prototype has several built-in properties and methods, all of which are “inherited” by any object that is [[Prototype]]-linked, either directly or indirectly through another object’s linkage, to Object.prototype.
Some common “inherited” properties from Object.prototype include:
constructor__proto__toString()valueOf()hasOwnProperty(..)isPrototypeOf(..)
Creating An Object With A Different [[Prototype]]
By default, any object you create in your programs will be [[Prototype]]-linked to that Object.prototype object. However, you can create an object with a different linkage like this:
Object.create(..) method takes its first argument as the value to set for the newly created object’s [[Prototype]].
One downside to this approach is that you aren’t using the { .. } literal syntax, so you don’t initially define any contents for myObj. You typically then have to define properties one-by-one, using =.
Alternately, but less preferably, you can use the { .. } literal syntax along with a special (and strange looking!) property:
Empty [[Prototype]] Linkage
We mentioned above that the [[Prototype]] chain has to stop somewhere, so as to have lookups not continue forever. Object.prototype is typically the top/end of every [[Prototype]] chain, as its own [[Prototype]] is null, and therefore there’s nowhere else to continue looking.
However, you can also define objects with their own null value for [[Prototype]], such as:
[[Prototype]] linkage to Object.prototype. For example, as mentioned in Chapter 1, the in and for..in constructs will consult the [[Prototype]] chain for inherited properties. But this may be undesirable, as you may not want something like "toString" in myObj to resolve successfully.
Moreover, an object with an empty [[Prototype]] is safe from any accidental “inheritance” collision between its own property names and the ones it “inherits” from elsewhere. These types of (useful!) objects are sometimes referred to in popular parlance as “dictionary objects”.
[[Prototype]] vs prototype
Notice that public property name prototype in the name/location of this special object, Object.prototype? What’s that all about?
Object is the Object(..) function; by default, all functions (which are themselves objects!) have such a prototype property on them, pointing at an object.
And here’s where the name conflict between [[Prototype]] and prototype really bites us. The prototype property on a function doesn’t define any linkage that the function itself experiences. Indeed, functions (as objects) have their own internal [[Prototype]] linkage somewhere else.
Rather, the prototype property on a function refers to an object that should be linked TO by any other object that is created when calling that function with the new keyword:
{ .. } object literal syntax is essentially the same as a new Object() call, the built-in object named/located at Object.prototype is used as the internal [[Prototype]] value for the new object we create and name myObj.
But where do functions themselves (as objects!) link to, [[Prototype]] wise? They link to Function.prototype, yet another built-in object, located at the prototype property on the Function(..) function.
In other words, you can think of functions themselves as having been “created” by a new Function(..) call, and then [[Prototype]]-linked to the Function.prototype object. This object contains properties/methods all functions “inherit” by default, such as toString() (to string serialize the source code of a function) and call(..) / apply(..) / bind(..) (we’ll explain these later in this book).
Objects Behavior
Properties on objects are internally defined and controlled by a “descriptor” metaobject, which includes attributes such asvalue (the property’s present value) and enumerable (a boolean controlling whether the property is included in enumerable-only listings of properties/property names).
The way object and their properties work in JS is referred to as the “metaobject protocol” (MOP). We can control the precise behavior of properties via Object.defineProperty(..), as well as object-wide behaviors with Object.freeze(..). But even more powerfully, we can hook into and override certain default behaviors on objects using special pre-defined Symbols.
Prototypes are internal linkages between objects that allow property or method access against one object — if the property/method requested is absent — to be handled by “delegating” that access lookup to another object. When the delegation involves a method, the context for the method to run in is shared from the initial object to the target object via the this keyword.
Continue to Chapter 3
Learn about class-oriented design with the
class keyword, inheritance, and modern class features
