javascript-definitive-guide/notes/chap6_objects.md

22 KiB
Raw Blame History

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
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
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()
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())
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())
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)
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
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
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

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

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

book.subtitle    // => undefined: property doesn't exist

Query property of an non-exist object

let len = book.subtitle.length; // !TypeError: undefined doesn't have length

Method to guard against this problem type:

  • Method 1: verbose and explicit
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
surname = book && book.author && book.author.surname;
  • Method 3: Rewrite method 2 using ?.
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 os 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
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
// 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
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:
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:

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
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
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

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)
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.
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

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:

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
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
o = Object.assign({}, defaults, o);

A function can be created to solve this problem via copies properties only if they are missing:

// 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"

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()
let s = { x: 1, y: 1 }.toString();  // s evaluate to "[object Object]", without show real information
  • We can redefine toString() method:
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:
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
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

  • All obj properties mentioned above are data properties. JS also supports accessor properties (
  • A accessor property does not have a value, but has 1 or 2 accessor methods: getter or setter)

How accessor property works:

  • When JS program queries value of this accessor property, JS invoke getter method.
  • When JS program sets value of the accessor property, JS invokes setter method, and passing value.

R/W property?:

  • If a accessor property has both getter & setter, it's a read/write property.
  • If it only has a getter method, it's a read-only property.
  • If it only has a setter method, it's a write-only property. (not possible with data properties)
    • Read it return undefined

Accessor property syntax using an extension to object literal syntax:

let o = {
    // An ordinary data property
    dataProp: value,

    // An accessor property defined as a pair of functions.
    get accessorProp() { return this.dataProp; },
    set accessorProp(value) { this.dataProp = value; }
};