objects-classes: more work on ch2

This commit is contained in:
Kyle Simpson 2022-01-26 12:53:48 -06:00
parent 5c9d088676
commit f44ffc57f9
No known key found for this signature in database
GPG Key ID: F5D84852C80B2DF8
2 changed files with 203 additions and 16 deletions

View File

@ -5,15 +5,15 @@
| :--- |
| Work in progress |
Objects are not just containers for multiple values, though that's the context for most interactions with objects.
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 the behavior when interacting with them.
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 "meta-object protocol" (MOP)[^mop]. The MOP is useful not only for predicting 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.
These characteristics that define the underlying behavior of objects are collectively referred to in formal terms as the "metaobject protocol" (MOP)[^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 (a "metaobject") with several properties (aka "attributes") on it, dictating how the target property behaves.
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 using `Object.getOwnPropertyDescriptor(..)` (ES5):
@ -54,6 +54,23 @@ If an existing property has not already been marked as non-configurable (with `c
| :--- |
| A number of earlier sections in this chapter refer to "copying" or "duplicating" properties. One might assume such copying/duplication would be at the property descriptor level. However, none of those operations actually work that way; they all do simple `=` style access and assignment, which has the effect of ignoring any nuances in how the underlying descriptor for a property is defined. |
Though it seems far less common out in the wild, we can even define multiple properties at once, each with their own descriptor:
```js
anotherObj = {};
Object.defineProperties(anotherObj,{
"fave": {
// a property descriptor
},
"superFave": {
// another property descriptor
}
});
```
It's not very common to see this usage, because it's rarer that you need to specifically control the definition of multiple properties. But it may be useful in some cases.
### Accessor Properties
A property descriptor usually defines a `value` property, as shown above. However, a special kind of property, known as an "accessor property" (aka, a getter/setter), can be defined. For these a property like this, its descriptor does not define a fixed `value` property, but would instead look something like this:
@ -66,7 +83,7 @@ A property descriptor usually defines a `value` property, as shown above. Howeve
}
```
A getter looks like a property access (`obj.prop`), but under the covers it invokes the `get()` method as defined; it's sort of like if you had done `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 done `obj.prop(value)`.
A getter looks like a property access (`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:
@ -92,7 +109,7 @@ anotherObj.fave;
### Enumerable, Writable, Configurable
Besides `value` or `get()` / `set(..)`, the other 3 attributes of a property are, as shown above:
Besides `value` or `get()` / `set(..)`, the other 3 attributes of a property descriptor are (as shown above):
* `enumerable`
* `writable`
@ -104,15 +121,9 @@ The `writable` attribute controls whether a `value` assignment (via `=`) is allo
The `configurable` attribute controls whether a property's **descriptor** can be re-defined/overwritten. A property that's `configurable: false` is locked to its definition, and any further attempts to change it with `Object.defineProperty(..)` will fail. A non-configurable property can still be assigned new values (via `=`), as long as `writable: true` is still set on the property's descriptor.
## `[[Prototype]]` Chain
One of the most important, but least obvious, characteristics of an object is referred to as the "prototype chain".
// TODO
## Object Sub-Types
There are a variety of specialized sub-types of objects. But by far, the two most common ones you'll interact with are arrays and `function`s.
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 and `function`s.
| NOTE: |
| :--- |
@ -212,10 +223,184 @@ extraInfo.set(help,"this is some important information");
extraInfo.get(help); // "this is some important information"
```
## Behavior Overview
## Object Characteristics
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 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.
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:
```js
myObj = {
favoriteNumber: 42
};
myObj.firstName = "Kyle"; // works fine
Object.preventExtensions(myObj);
myObj.nicknames = [ "getify", "ydkjs" ]; // fails
myObj.favoriteNumber = 123; // works fine
```
In non-strict-mode, an assignment that creates a new property will silently fail, whereas in strict mode an exception will be thrown.
### Sealed
// TODO
### Frozen
// TODO
## Extending The MOP
As mentioned at the start of this chapter, objects in JS behave according to a set of rules referred to as the Metaobject Protocol (MOP)[^mop]. Now that we understand more fully how objects work by default, we want to turn our attention to how we can hook into some of these default behaviors and override/customize them.
// TODO
## `[[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. More on that shortly.
We already saw several implications of `[[Prototype]]` linkage in Chapter 1. For example, by default, all objects are `[[Prototype]]`-linked to the built-in object named `Object.prototype`.
| WARNING: |
| :--- |
| That `Object.prototype` name itself can be confusing, since it uses a property called `prototype`. How are `[[Prototype]]` and `prototype` related!? Put such questions/confusion on pause for a bit, as we'll come back an explain the differences between `[[Prototype]]` and `prototype` soon. For the moment, just assume the presence of this important but weirdly named built-in object, `Object.prototype`. |
Let's consider some code:
```js
myObj = {
favoriteNumber: 42
};
```
That should look familiar from Chapter 1. But what you *don't see* in this code is that the object there was automatically linked to (via its internal `[[Prototype]]`) to that automatically built-in, but weirdly named, `Object.prototype` object.
When we do things like:
```js
myObj.toString(); // "[object Object]"
myObj.hasOwnPropertyName("favoriteNumber"); // true
```
We're taking advantage of this internal `[[Prototype]]` linkage, without really realizing it. Since `myObj` does not have `toString` or `hasOwnPropertyName` 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 `hasOwnPropertyName` 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 `hasOwnPropertyName` properties, along with many others, are said to be "inherited properties" on `myObj`.
| NOTE: |
| :--- |
| 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 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:
```js
myObj = Object.create(differentObj);
```
The `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 `=`.
| NOTE: |
| :--- |
| The second, optional argument to `Object.create(..)` is -- like the second argument to `Object.defineProperties(..)` as discussed earlier -- an object with properties that hold descriptors to initially define the new object with. In practice out in the wild, this form is rarely used, likely because it's more awkward to specify full descriptors instead of just name/value pairs. But it may come in handy in some limited cases. |
Alternately, but less preferably, you can use the `{ .. }` literal syntax along with a special (and strange looking!) property:
```js
myObj = {
__proto__: differentObj
};
```
| WARNING: |
| :--- |
| The strange looking `__proto__` property existed in some JS engines for more than 20 years, but was only standardized in JS as of ES6. Even still, it was added in Appendix B of the specification[^specApB], which lists features that TC39 begrudgingly includes because they exist popularly in various browser-based JS engines and therefore are a de-facto reality even if they didn't originate with TC39. This feature is thus "guaranteed" by the spec to exist in all conforming browser-based JS engines, but is not necessarily guaranteed to work in other independent JS engines. Node.js uses the JS engine (v8) from the Chrome browser, so Node.js gets `__proto__` by default/accident. Be careful when using `__proto__` to be aware of all the JS engine environments your code will run in. |
Whether you use `Object.create(..)` or `__proto__`, the object in question will be `[[Prototype]]`-linked to a different object than the default `Object.prototype`.
#### 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:
```js
emptyObj = Object.create(null);
// or: emptyObj = { __proto__: null }
empty.toString; // undefined
```
It can be quite useful to create an object with no `[[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 objects!) have such a `prototype` property on them, pointing at an object.
Any 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 -- more on that in a second.
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:
```js
myObj = {};
// is basically the same as:
myObj = new Object();
```
Since the `{ .. }` 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]]` linkage for the new object we create and name `myObj`.
Phew! Talk about a topic made significantly more confusing just because of the name overlap between `[[Prototype]]` and `prototype`!
----
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 could 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 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 as `value` (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)[^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.
// TODO: move to a later chapter
Prior to ES6, the prototype system was how developers expressed (i.e., emulated) the class design pattern in JS -- so-called "prototypal inheritance". ES6 introduced the `class` keyword as a syntactic affordance to embrace and centralize the prevalence of varied class design approaches. Ostensibly, `class` was introduced as "sugar" built on top of manual/explicit prototypal classes.
[^mop]: "Metaobject", Wikipedia, https://en.wikipedia.org/wiki/Metaobject
[^specApB]: ECMAScript 2021 Language Specification, Appendix B: Additional ECMAScript Features for Web Browsers, https://262.ecma-international.org/12.0/#sec-additional-ecmascript-features-for-web-browsers (latest as of time of this writing in January 2022)

View File

@ -20,7 +20,9 @@
* Containers Are Collections Of Properties
* Chapter 2: How Objects Work
* Property Descriptors
* `[[Prototype]]` Chain
* Object Sub-Types
* Object Characteristics
* Extending the MOP
* `[[Prototype]]` Chain
* Objects Behavior
* Appendix A: TODO