21 KiB
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
meansy
holds a reference to the same obj, not a copy of that obj.
- e.g.
- 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:
- using object literal
- using keyword
new
- 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'sprototype
property as prototypenew Object()
inherits fromObject.prototype
new Array()
inherits fromArray.prototype
- Only few objects have
prototype
property, they are used to defineprototypes
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()
)
- Created obj inherit no property or method (e.g.
let o2 = Object.create(null); // o2 inherits no props or methods.
- Create ordinary new empty obj using
Object.prototype
(like obj returned by{}
orObject()
)
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 propertyx
, and that property is an accessor property with a setter method, then the setter method is called rather than creating a new propertyx
withino
.
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
orundefined
causes aTypeError
- 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
:
o
has an own propertyp
that is read-onlyo
has an inherited propertyp
that is read-only: No way to overwrite this propertyo
does not have an own propertyp
;o
does not inherit a propertyp
with a setter method, ando
’s extensible attribute (see §14.2) isfalse
.- Since
p
does not already exist ino
, and if there is no setter method to call, thenp
must be added too
. - But if
o
is not extensible, then no new properties can be defined on it.
- Since
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 offalse
. - 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
operatorhasOwnProperty()
methodpropertyIsEnumerable()
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 namefalse
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 istrue
- 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
- runs the body of the loop once for each enumerable property (own or inherited) of the specified obj.
- 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 SymbolObject.getOwnPropertyNames()
: returns same asOjbect.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:
- String properties whose names are non-negative integers, numeric order from smallest to largest. i.e. arrays are enumerated in order
- All remaining properties with string names. In order they were added to object.
- 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 sourcedefaults
. Directly assign will overwriteo
'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 tonull
- 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 owntoString()
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 calltoString()
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
- Date class define its
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)"]'