javascript-definitive-guide/notes/chap6_objects.md

549 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Chapter 6. Objects
## 6.1 Introduction to Objects
* An object is an unordererd collection of properties (each is a name/value pair)
* JS object can inherit properties from another object (aka "prototype")
* JS objects are dynamic; i.e. properties can be added/removed
* Any value in JS is object except string, number, Symbol, `true`/`false`, `null`/`undefined`
* Objects are mutable, and manupulated by reference rather than value.
* e.g. `let y=x` means `y` holds a reference to the same obj, not a copy of that obj.
* Common operations on obj: create, set, query, delete, test, and enumerate
* Property has name & value, but no obj has two properties with the same name (that's why we use Symbol)
* JS use *own property* to refer to non-inherited properties.
* Each property has 3 property **attributes**:
* **writable**: whether value of property can be set
* **enumerable**: whether the property name is returned by for/in loop
* **configurable**: whether the property can be deleted and its attributes can be altered
## 6.2 Creating Objects
Creating obj, 4 methods:
1. using object literal
2. using keyword `new`
3. using `Object.create()` function
### 6.2.1 Object Literals
**Object literal** in simplest form:
* comma-separated list of colon-separated `name:value` pairs, enclosed within `{}`.
* property *name*: JS identifier or string
* property *value*: JS expression
```js
let empty = {}; // An object with no properties
let point = { x: 0, y: 0 }; // Two numeric properties
let p2 = { x: point.x, y: point.y+1 }; // More complex values
let book = {
"main title": "JavaScript", // These property names include spaces,
"sub-title": "The Definitive Guide", // and hyphens, so use string literals.
for: "all audiences", // for is reserved, but no quotes.
author: { // The value of this property is
firstname: "David", // itself an object.
surname: "Flanagan"
}
};
```
When object literal works:
* Object literal creates & initializes a new & distinct obj every time it's evaluated.
* In loop body, a new obj can be created repeatedly.
### 6.2.2 Creating Objects with new
* **`new` operator**
* creates & initialize a new object
* syntax: `new` followed by function invocation as **constructor**
```js
let o = new Object(); // Create an empty object: same as {}.
let a = new Array(); // Create an empty array: same as [].
let d = new Date(); // Create a Date object representing the current time
let r = new Map(); // Create a Map object for key/value mapping
```
### 6.2.3 Prototypes
Almost all JS obj has a prototype associate with it:
* All objs created using **object literal** (shown in 6.2.1) are associated with the same prototype obj, referred by `Object.prototype`
* objs created using `new` (invoking constructor) use *value of constructor function's `prototype` property* as prototype
* `new Object()` inherits from `Object.prototype`
* `new Array()` inherits from `Array.prototype`
* Only few objects have `prototype` property, they are used to define `prototypes` for all other objs.
### 6.2.4 Object.create()
3 Methods below demonstrated ability to create a new obj with an arbitrary prototype:
* Create new obj w/ defined prototype using **`Object.create()`**
```js
let o1 = Object.create({x: 1, y: 2}); // o1 inherits properties x and y.
o1.x + o1.y // => 3
```
* Create new obj w/o prototype by parsing `null`
* Created obj inherit no property or method (e.g. `toString()`)
```js
let o2 = Object.create(null); // o2 inherits no props or methods.
```
* Create ordinary new empty obj using **`Object.prototype`** (like obj returned by `{}` or `Object()`)
```js
let o3 = Object.create(Object.prototype); // o3 is like {} or new Object().
```
#### Use created object to guard unintended modification
* Q: How to guard against unintended modification of an obj by a function (from other library)?
* A: Instead of passing the obj directly to the function, pass an obj that inherit from it. So writing property do not affect original value. (like passing a read-only)
```js
let o = { x: "don't change this value" };
library.function(Object.create(o)); // Guard against accidental modifications
```
## 6.3 Querying and Setting Properties
Obtain value of property:
* using dot (`.`): RHS of dot should be simple identifier (not string) of property
* using square bracket (`[]`): value within `[]` should be an expression that evalutes to a string (or sth can convert to string) that contains property name
```js
let author = book.author; // Get the "author" property of the book.
let name = author.surname; // Get the "surname" property of the author.
let title = book["main title"]; // Get the "main title" property of the book.
```
Create/Set a property:
* Query property, and place it on LHS
```js
book.edition = 7; // Create an "edition" property of book.
book["main title"] = "ECMAScript"; // Change the "main title" property.
```
### 6.3.1 Objects As Associative Arrays
```js
object.property // C like structure access
object["property"] // associative array
```
JS objects are **Associative Arrays** (e.g. hash or map or dictionary)
* In strong typed language (e.g. C/C++), obj's property are defined. While, JS program can **create any number of properties in any object in runtime**
* `.` operator requires name of the property as identifier, which may be unknown in code.
* `[]` operator allow access properties dynamically
Following code shows calculate portfolio value in runtime via associative arrays
```js
function computeValue(portfolio) {
let total = 0.0;
for(let stock in portfolio) { // For each stock in the portfolio:
let shares = portfolio[stock]; // get the number of shares
let price = getQuote(stock); // look up share price
total += shares * price; // add stock value to total value
}
return total; // Return total value.
}
```
### 6.3.2 Inheritance
JS obj have a set of "own properties", and they also inherit properties from prototype chain.
Read properties:
* If a property cannot be found in a JS obj, it will search one by one (bottom to top, from child to parent) through prototype chain
```js
let o = {}; // o inherits object methods from Object.prototype
o.x = 1; // and it now has an own property x.
let p = Object.create(o); // p inherits properties from o and Object.prototype
p.y = 2; // and has an own property y.
let q = Object.create(p); // q inherits properties from p, o, and...
q.z = 3; // ...Object.prototype and has an own property z.
let f = q.toString(); // toString is inherited from Object.prototype
q.x + q.y // => 3; x and y are inherited from o and p
```
Write (assign) properties:
* check prototype chain only to verify whether read-only.
* If inherited property `x` is read-only, assignment is not allowed
* If assignment is allowed, the property is created/set within the current object, and do not modify prototype chain
* Only exception: if `o` inherits property `x`, and that property is an accessor property with a setter method, then the setter method is called rather than creating a new property `x` within `o`.
```js
let unitcircle = { r: 1 }; // An object to inherit from
let c = Object.create(unitcircle); // c inherits the property r
c.x = 1; c.y = 1; // c defines two properties of its own
c.r = 2; // c overrides its inherited property
unitcircle.r // => 1: the prototype is not affected
```
### 6.3.3 Property Access Errors
Errors during accessing property:
* It's not error to query a property/object that does not exist. Return `undefined`
* It's error to query property of an non-existent object. Return `TypeError`
Query non-exist object
```js
book.subtitle // => undefined: property doesn't exist
```
Query property of an non-exist object
```js
let len = book.subtitle.length; // !TypeError: undefined doesn't have length
```
Method to guard against this problem type:
* Method 1: verbose and explicit
```js
let surname = undefined;
if (book) {
if (book.author) {
surname = book.author.surname;
}
}
```
* Method 2: A concise and idiomatic alternative to get surname or null or undefined
* Check Chap4.10.1 for short-circuiting behavior of && operator
```js
surname = book && book.author && book.author.surname;
```
* Method 3: Rewrite method 2 using `?.`
```js
let surname = book?.author?.surname;
```
Lists of tips:
* Attempting to set property on `null` or `undefined` causes a `TypeError`
* Attempting to set property may fail due to
* Some properties are read-only
* Some objects don't allow adding new properties
* Error from property assignment:
* In strict mode (Chap 5.6.3), a TypeError is thown whenever an attempt to set a property fails.
* Outsie strict mode, silent when fail
3 circumstances when failed to set a property `p` of obj `o`:
1. `o` has an own property `p` that is read-only
2. `o` has an inherited property `p` that is read-only: No way to overwrite this property
3. `o` does not have an own property `p`; `o` does not inherit a property `p` with a setter method, and `o`s extensible attribute (see §14.2) is `false`.
1. Since `p` does not already exist in `o`, and if there is no setter method to call, then `p` must be added to `o`.
2. But if `o` is not extensible, then no new properties can be defined on it.
## 6.4 Deleting Properties
`delte` operator removes a property from an obj. Its operand is *property access expression*
`delete` & prototypes:
* only delete own properties, cannot delete inherited ones (from prototypes).
* deleting inherited properties need to it on prototype, which has affect all children obj.
`delete` return true if:
* delete succeeded
* when delete had no effect (e.g.
* on inherited properties
* non-exist properties
* when used with an expression that's not a property access expression
```js
let o = {x: 1}; // o has own property x and inherits property toString
delete o.x // => true: deletes property x
delete o.x // => true: does nothing (x doesn't exist) but true anyway
delete o.toString // => true: does nothing (toString isn't o's own property)
delete 1 // => true: nonsense, but true anyway
```
`delete` fails if:
* trying to rm properties that have a `configurable` attribute of `false`.
* trying to delete non-configurable properties of built-in objects
* properties of the global object created by variable declaration and function declaration
`delete`'s failure in strict & non-strict mode:
* In strict mode: causes TypeError
* In non-strict mode: evaluate to `false` e.g. shown below
```js
// In strict mode, all these deletions throw TypeError instead of returning false
delete Object.prototype // => false: property is non-configurable
var x = 1; // Declare a global variable
delete globalThis.x // => false: can't delete this property
function f() {} // Declare a global function
delete globalThis.f // => false: can't delete this property either
```
`delete` configurable properties of global object:
* In non-strict mode, via directly delete property name
```js
globalThis.x = 1; // Create a configurable global property (no let or var)
delete x // => true: this property can be deleted
```
* In strict mode, method above will create **SyntaxError**. Hence need to be explicit:
```js
delete x; // SyntaxError in strict mode
delete globalThis.x; // This works
```
## 6.5 Testing Properties
JS obj = sets of properties. We want to test membership in the set using:
* `in` operator
* `hasOwnProperty()` method
* `propertyIsEnumerable()` method
* querying property
`in` operator return true if object has an own property or an inherited property **by that name**:
```js
let o = { x: 1 };
"x" in o // => true: o has an own property "x"
"y" in o // => false: o doesn't have a property "y"
"toString" in o // => true: o inherits a toString property
```
`hasOwnProperty()` method of an obj return:
* `true` if obj has an own property with the given name
* `false` for inherited properties
```js
let o = { x: 1 };
o.hasOwnProperty("x") // => true: o has an own property x
o.hasOwnProperty("y") // => false: o doesn't have a property y
o.hasOwnProperty("toString") // => false: toString is an inherited property
```
`propertyIsEnumerable()` refines `hasOwnProperty()`. It returns:
* `true` if named property is not inherited and its *enumerable* attribute is `true`
* Properties created by normal JS code are enumerable unless specified
```js
let o = { x: 1 };
o.propertyIsEnumerable("x") // => true: o has an own enumerable property x
o.propertyIsEnumerable("toString") // => false: not an own property
Object.prototype.propertyIsEnumerable("toString") // => false: not enumerable
```
Qeurying method is simple by using `!=` to make sure whether property is undefined
```js
let o = { x: 1 };
o.x !== undefined // => true: o has a property x
o.y !== undefined // => false: o doesn't have a property y
o.toString !== undefined // => true: o inherits a toString property
```
Simple querying method vs. `in` operator:
* `in` operator can tell whether a property does not exit (`null`) or exit but not defined (`undefined`)
```js
let o = { x: undefined }; // Property is explicitly set to undefined
o.x !== undefined // => false: property exists but is undefined
o.y !== undefined // => false: property doesn't even exist
"x" in o // => true: the property exists
"y" in o // => false: the property doesn't exist
delete o.x; // Delete the property x
"x" in o // => false: it doesn't exist anymore
```
## 6.6 Enumerating Properties
We want to iterate through or obtain a list of all properties of an object using:
* `for/in` loop.
* Get an array of property names for an object and then loop through that array with `for/of` loop.
**Method 1: Use `for/in` loop to enumerate properties**
1. runs the body of the loop once for each enumerable property (own or inherited) of the specified obj.
2. assigning the name of the property to the loop variable.
Note:
* Inherited built-in methods are not enumerable (e.g. "toString")
* Properties that added by code are enumerable by default.
```js
let o = {x: 1, y: 2, z: 3}; // Three enumerable own properties
o.propertyIsEnumerable("toString") // => false: not enumerable
for(let p in o) { // Loop through the properties
console.log(p); // Prints x, y, and z, but not toString
}
```
Trick: Stop enumerating inherited properties with `for/in`, add an explicit check inside loop body
```js
for(let p in o) {
if (!o.hasOwnProperty(p)) continue; // Skip inherited properties
}
for(let p in o) {
if (typeof o[p] === "function") continue; // Skip all methods
}
```
**Method 2: Get an array of property names for an object and then loop through that array with `for/of` loop.**
Four ways to get an array of property names:
* `Object.keys()`: returns an array of the names of the enumerable own properties of an object, excluding non-enumerable properties, inherited properties, properties whose name is Symbol
* `Object.getOwnPropertyNames()`: returns same as `Ojbect.keys()` + non-enum own properties (as long as name are strings)
* `Object.getOwnPropertySymbols()`: returns own properties whose names are Symboles (no matter they are enumerable or not)
* `Reflect.ownKeys()`: returns all own property names, both enum or non-enum, and both string and Symbol.
### 6.6.1 Property Enumeration Order
Order Summary:
1. String properties whose names are non-negative integers, numeric order from smallest to largest. i.e. arrays are enumerated in order
2. All remaining properties with string names. In order they were added to object.
3. Properties whose names are Symbol in order they were added to obj.
## 6.7 Extending Objects
Common practice in pure JS to copy properties from one obj to another:
```js
let target = {x: 1}, source = {y: 2, z: 3};
for(let key of Object.keys(source)) {
target[key] = source[key];
}
target // => {x: 1, y: 2, z: 3}
```
Various JS frameworks have developed utility function `extend()` to cover this operation, and it's standarized in ES6 as `Object.assign()`
**`Object.assign()`**:
* Syntax: `Object.assign(target_obj, src_obj_1, src_obj_2)`
* Operations: Copy enumerable own properties from 2nd and subsequent args (i.e. **source object**) to 1st arg (i.e. **target object**)
* target obj is modified & returned
* source obj is not changed
* Copy order follows order of arg, so 1st source object property will overwrite target obj property, while 2nd source obj property will overwrite 1st obj property
* How copy works: using ordinary property get/set operations.
* if source has getter method, and target has setter, these 2 methods will be invoked. But themselves won't be copied.
Trick to use `.assign()`:
* Given obj `o`, and source `defaults`. Directly assign will overwrite `o`'s original properties if there are same property names
```js
Object.assign(o, defaults); // overwrites everything in o with defaults
```
* Correct way of safely copy properties while keeping target's original property values: create a new object, copy the defaults into it, and then override those defaults with the properties in `o`
```js
o = Object.assign({}, defaults, o);
```
A function can be created to solve this problem via copies properties only if they are missing:
```js
// Like Object.assign() but doesn't override existing properties
// (and also doesn't handle Symbol properties)
function merge(target, ...sources) {
for(let source of sources) {
for(let key of Object.keys(source)) {
if (!(key in target)) { // This is different than Object.assign()
target[key] = source[key];
}
}
}
return target;
}
Object.assign({x: 1}, {x: 2, y: 2}, {y: 3, z: 4}) // => {x: 2, y: 3, z: 4}
merge({x: 1}, {x: 2, y: 2}, {y: 3, z: 4}) // => {x: 1, y: 2, z: 4}
```
## 6.8 Serializing Objects
**Object serialization** = process to convert an object's state to a string. Then later we can restore it.
2 functions:
* `JSON.stringify()`: serialize JS objects.
* `JSON.parse`: restore JS objects.
JSON stands for **"JavaScript Object Notation"**
```js
let o = {x: 1, y: {z: [false, null, ""]}}; // Define a test object
let s = JSON.stringify(o); // s == '{"x":1,"y":{"z":[false,null,""]}}'
let p = JSON.parse(s); // p == {x: 1, y: {z: [false, null, ""]}}
```
What can/cannot be serialized?
* CAN be serialized: Objects, arrays, strings, finite numbers, `true`, `false`, `null`
* `NaN`, `Infinity`, `-Infinity` serialized to `null`
* CANNOT be serialized: Function, RegExp, Error objects, `undefined` value
## 6.9 Object Methods
All JS objects (excpet explicitly created w/o prototype) inherits properties from `Object.prototype`. Hence they inherited some **primarily methods**, which are universally available.
* `hasOwnProperty()`, `propertyIsEnumerable()`
* `Object.create()`, `Object.keys()`
* `toString`
* ...
### 6.9.1 The toString() Method
* `toString()` takes no arg, and returns a string that represents the value of obj.
* Default `toString()` is not useful (e.g. shown below), each class define their own `toString()`
```js
let s = { x: 1, y: 1 }.toString(); // s evaluate to "[object Object]", without show real information
```
* We can redefine `toString()` method:
```js
let point = {
x: 1,
y: 2,
toString: function() { return `(${this.x}, ${this.y})`; }
};
String(point) // => "(1, 2)": toString() is used for string conversions
```
### 6.9.2 The toLocaleString() Method
`toLocaleString()`:
* All objs have this method
* Purpose of this method: return a localized (vs. internationalization) string representation of the obj.
* default `toLocaleString()` defined by Object call `toString()` directly
* Date/Number class defin customized versions of `toLocaleString()` to format date, currency, time, etc.
* User can create their own method:
```js
let point = {
x: 1000,
y: 2000,
toString: function() { return `(${this.x}, ${this.y})`; },
toLocaleString: function() {
return `(${this.x.toLocaleString()}, ${this.y.toLocaleString()})`;
}
};
point.toString() // => "(1000, 2000)"
point.toLocaleString() // => "(1,000, 2,000)": note thousands separators
```
### 6.9.3 The valueOf() Method
`valueOf()`:
* is called when JS needs to convert an obj to some non-string primitive type (e.g. number)
* Many built-in class has its own `valueOf()`
* Date class define its `valueOf()` to convert dates to number, so can perform comparison
### 6.9.4 The toJSON() Method
`toJSON()` method:
* didn't get defined in `Object.prototype`;
* `JSON.stringify()` looks for & invoke it on any object that will be serialized
```js
let point = {
x: 1,
y: 2,
toString: function() { return `(${this.x}, ${this.y})`; },
toJSON: function() { return this.toString(); }
};
JSON.stringify([point]) // => '["(1, 2)"]'
```
## 6.10 Extended Object Literal Syntax
### 6.10.1 Shorthand Properties
### 6.10.2 Computed Property Names
### 6.10.3 Symbols as Property Names
### 6.10.4 Spread Operator
### 6.10.5 Shorthand Methods
### 6.10.6 Property Getters and Setters