549 lines
21 KiB
Markdown
549 lines
21 KiB
Markdown
# 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 |