Skip to content

Commit 251cc3e

Browse files
committed
objects-classes: almost finished with ch1 draft
1 parent 5f1f428 commit 251cc3e

File tree

2 files changed

+281
-7
lines changed

2 files changed

+281
-7
lines changed

objects-classes/ch1.md

Lines changed: 280 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,14 @@ Also, consider these property definition operations to happen "in order", from t
277277
A common way `...` object spread is used is for performing *shallow* object duplication:
278278

279279
```js
280-
myObjCopy = { ...myObj };
280+
myObjShallowCopy = { ...myObj };
281281
```
282282

283-
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.
283+
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(..)`).
284284

285-
##### Deep Object Copy
285+
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(..)`).
286+
287+
#### Deep Object Copy
286288

287289
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.
288290

@@ -345,6 +347,32 @@ myObj[`${ howMany(1) } nicknames`]; // [ "getify", "ydkjs" ]
345347

346348
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.
347349

350+
#### Object Entries
351+
352+
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:
353+
354+
```js
355+
myObj = {
356+
favoriteNumber: 42,
357+
isDeveloper: true,
358+
firstName: "Kyle"
359+
};
360+
361+
Object.entries(myObj);
362+
// [ ["favoriteNumber",42], ["isDeveloper",true], ["firstName":"Kyle"] ]
363+
```
364+
365+
Added in ES6, `Object.entries(..)` retieves this list of entries -- containing only owned an enumerable properties; see "Object Characteristics" later in this chapter -- from a source object.
366+
367+
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):
368+
369+
```js
370+
myObjShallowCopy = Object.fromEntries( Object.entries(myObj) );
371+
372+
// alternate approach to the earlier discussed:
373+
// myObjShallowCopy = { ...myObj };
374+
```
375+
348376
#### Destructuring
349377

350378
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.
@@ -418,7 +446,7 @@ Object destructuring syntax is generally preferred for its declarative and more
418446

419447
#### Conditional Property Access
420448

421-
Recently (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`.
449+
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`.
422450

423451
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.
424452

@@ -470,19 +498,264 @@ Everything asserted about how `?.` behaves goes the same for `?.[`.
470498
| :--- |
471499
| 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.<br><br>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. |
472500
501+
#### Accessing Properties On Non-Objects
502+
503+
This may sound counter-intuitive, but you can generally access properties/methods from values that aren't themselves objects:
504+
505+
```js
506+
fave = 42;
507+
508+
fave; // 42
509+
fave.toString(); // "42"
510+
```
511+
512+
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?
513+
514+
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.
515+
516+
This process is typically called "boxing", as in putting a value inside a "box" (object container).
517+
518+
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.
519+
520+
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).
521+
522+
| NOTE: |
523+
| :--- |
524+
| 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. |
525+
473526
### Assiging Properties
474527
475-
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
528+
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:
476529
477-
// TODO
530+
```js
531+
myObj.favoriteNumber = 123;
532+
```
533+
534+
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.
535+
536+
It's also possible to assign one or more properties at once -- assuming the source properties (name and value) are in another object -- using the `Object.assign(..)` (added in ES6) method:
537+
538+
```js
539+
// shallow copy all (owned and enumerable) properties
540+
// from `myObj` into `anotherObj`
541+
Object.assign(anotherObj,myObj);
542+
543+
Object.assign(
544+
/*target=*/anotherObj,
545+
/*source1=*/{
546+
someProp: "some value",
547+
anotherProp: 1001,
548+
},
549+
/*source2=*/{
550+
yetAnotherProp: false
551+
}
552+
);
553+
```
554+
555+
`Object.assign(..)` takes the first object as target, and the second (and optionally subsequent) objects as source(s). Copying is done in the same manner as described earlier in the "Object Spread" section.
478556
479557
## Object Characteristics
480558
559+
To understand the object mechanism in JS, 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.
560+
561+
### Property Descriptors
562+
563+
Each property on an object is internally described by what's known as a "property descriptor". This is, itself, an object with several properties on it, dictating how the outer property behaves on the object in question.
564+
565+
We can retrieve a property descriptor for any existing property using `Object.getOwnPropertyDescriptor(..)` (ES5):
566+
567+
```js
568+
myObj = {
569+
favoriteNumber: 42,
570+
isDeveloper: true,
571+
firstName: "Kyle"
572+
};
573+
574+
Object.getOwnPropertyDescriptor(myObj,"favoriteNumber");
575+
// {
576+
// value: 42,
577+
// enumerable: true,
578+
// writable: true,
579+
// configurable: true
580+
// }
581+
```
582+
583+
We can even use such a descriptor to define a new property on an object, using `Object.defineProperty(..)` (ES5):
584+
585+
```js
586+
anotherObj = {};
587+
588+
Object.defineProperty(anotherObj,"fave",{
589+
value: 42,
590+
enumerable: true, // default if omitted
591+
writable: true, // default if omitted
592+
configurable: true // default if omitted
593+
});
594+
595+
anotherObj.fave; // 42
596+
```
597+
598+
If an existing property has not already been marked as non-configurable (with `configurable: false` in its descriptor), it can always be re-defined/overwritten using `Object.defineProperty(..)`.
599+
600+
| WARNING: |
601+
| :--- |
602+
| 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. |
603+
604+
#### Accessor Properties
605+
606+
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:
607+
608+
```js
609+
{
610+
get() { .. }, // function to invoke when retrieving the value
611+
set(v) { .. }, // function to invoke when assigning the value
612+
// .. enumerable, etc
613+
}
614+
```
615+
616+
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)`.
617+
618+
Let's illustrate a getter/setter accessor property:
619+
620+
```js
621+
anotherObj = {};
622+
623+
Object.defineProperty(anotherObj,"fave",{
624+
get() { console.log("Getting 'fave' value!"); return 123; },
625+
set(v) { console.log(`Ignoring ${v} assignment.`); }
626+
});
627+
628+
anotherObj.fave;
629+
// Getting 'fave' value!
630+
// 123
631+
632+
anotherObj.fave = 42;
633+
// Ignoring 42 assignment.
634+
635+
anotherObj.fave;
636+
// Getting 'fave' value!
637+
// 123
638+
```
639+
640+
#### Enumerable, Writable, Configurable
641+
642+
Besides `value` or `get()` / `set(..)`, the other 3 attributes of a property are, as shown above:
643+
644+
* `enumerable`
645+
* `writable`
646+
* `configurable`
647+
648+
The `enumerable` attribute controls whether the property will appear in various enumerations of object properties, such as `Object.keys(..)`, `Object.entries(..)`, `for..in` loops, and the copying that occurs with the `...` object spread and `Object.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.
649+
650+
The `writable` attribute controls whether a `value` assignment (via `=`) is allowed. To make a property "read only", define it with `writable: false`. However, as long as the property is still configurable, `Object.defineProperty(..)` can still change the value by setting `value` differently.
651+
652+
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.
653+
654+
### `[[Prototype]]`
655+
481656
// TODO
482657
658+
## Sub-Object Types
659+
660+
There are a variety of specialized sub-types of objects. By far, the two most common ones you'll interact with are arrays and `function`s.
661+
662+
### Arrays
663+
664+
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 like `favoriteNumber` is legal. But it's greatly frowned upon to mix named properties into numerically indexed arrays.
665+
666+
Arrays are preferably defined with literal syntax (similar to objects), but with the `[ .. ]` square brackets rather than `{ .. }` curly brackets:
667+
668+
```js
669+
myList = [ 23, 42, 109 ];
670+
```
671+
672+
JS allows any mixture of value types in arrays, including objects, other arrays, functions, etc. As you're likely already aware, arrays are "zero-indexed", meaning the first element in the array is at the index `0`, not `1`:
673+
674+
```js
675+
myList = [ 23, 42, 109 ];
676+
677+
myList[0]; // 23
678+
myList[1]; // 42
679+
```
680+
681+
Recall that any string property name on an object that "looks like" a number -- is able to be validly coerced to a string -- will actually be treated like a number. The same goes for arrays. You should always use `42` as a numeric index (aka, property name), but if you use the string `"42"`, JS will coerce that to a number for you.
682+
683+
```js
684+
// "2" works as an index here, but it's not advised
685+
myList["2"]; // 109
686+
```
687+
688+
One exception to the "no named properties on arrays" rule is that all arrays automatically expose a `length` property, which is automatically kept updated with the "length" of the array.
689+
690+
```js
691+
myList = [ 23, 42, 109 ];
692+
693+
myList.length; // 3
694+
695+
// "push" another value onto the end of the list
696+
myList.push("Hello");
697+
698+
myList.length; // 4
699+
```
700+
701+
| WARNING: |
702+
| :--- |
703+
| Many JS developers incorrectly believe that array `length` is basically a *getter* (see "Accessor Properties" earlier), but it's not. The offshoot is that these developers feel like it's "expensive" to access this property -- as if JS has to on-the-fly recompute the length -- and will thus do things like capture/store the length of an array before doing a non-mutating loop over it. This used to be "best practice" from a performance perspective. But for at least 10 years now, that's actually been an anti-pattern, because the JS engine is more efficient at managing the `length` property than our JS code is at trying to "outsmart" the engine to avoid invoking something we think is a *getter*. It's more efficient to let the JS engine do its job, and just access the property whenever and however often it's needed. |
704+
705+
#### Empty Slots
706+
707+
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 to `undefined` as you might expect:
708+
709+
```js
710+
myList = [ 23, 42, 109 ];
711+
myList.length; // 3
712+
713+
myList[14] = "Hello";
714+
myList.length; // 15
715+
716+
myList; // [ 23, 42, 109, empty x 11, "Hello" ]
717+
718+
// looks like a real slot with a
719+
// real `undefined` value in it,
720+
// but beware, it's a trick!
721+
myList[9]; // undefined
722+
```
723+
724+
You might wonder why empty slots are so bad? One reason: there are APIs in JS, like array's `map(..)`, where empty slots are suprisingly skipped over! Never, ever intentionally create empty slots in your arrays. This in undebateably one of JS's "bad parts".
725+
726+
### Functions
727+
728+
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.
729+
730+
Functions have two pre-defined properties you may find yourself interacting with, specifially for meta-programming purposes:
731+
732+
```js
733+
function help(opt1,opt2,...remainingOpts) {
734+
// ..
735+
}
736+
737+
help.name; // "help"
738+
help.length; // 2
739+
```
740+
741+
The `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`).
742+
743+
#### Avoid Setting Function Properties
744+
745+
You should avoid assigning properties on function objects. If you're looking to store extra information associated with a function, use a separate `Map(..)` (or `WeakMap(..)`) with the function object as the key, and the extra information as the value.
746+
747+
```js
748+
extraInfo = new Map();
749+
750+
extraInfo.set(help,"this is some important information");
751+
752+
// later:
753+
extraInfo.get(help); // "this is some important information"
754+
```
755+
483756
## Objects Overview
484757
485-
Objects are not just containers for multiple values
758+
Objects are not just containers for multiple values, though that's the context for most interactions with objects.
486759
487760
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.
488761

objects-classes/toc.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
* About This Book
1313
* Object As Containers
1414
* Object Characteristics
15+
* Sub-Object Types
1516
* Objects Overview
1617
* Appendix A: TODO

0 commit comments

Comments
 (0)