Objects

Objects in JavaScript are reference types. An object is a dynamic unordered collection of properties. A property has a unique name represented by a string (or Symbol) and an associated value. The value may be any JavaScript value (including functions and other objects). Properties may be inherited from a prototype object or defined directly on the object.

Creating objects

An object literal is an expression that creates and initializes a new distinct object each time it is evaluated. A trailing comma following the last property in an object literal is allowed.

let empty = {};
let book = {
  title: 'JavaScript',
  likes: 10,
  author: {
    firstName: 'Roman',
    lastName: 'Akchurin',
  },
};

An object can be created with new operator, which must be followed by a function invocation, called constructor, which initializes a newly created object.

let o = new Object(); // same as {}
let a = new Array(); // same as []
let d = new Date();
let m = new Map();

Lastly, objects can be created with Object.create() method with its first argument specifying the prototype of that object. A prototype is an object which another object inherits its properties from. All objects created from object literals have the same prototype Object.prototype. Objects created by using the new keyword use the value of the prototype property of the constructor function that they invoke. Similarly, objects created by new Array() use Array.prototype, and objects created by new Date() use Date.prototype as their prototype.

let o1 = Object.create({ x: 1, y: 2 }); // o1 inherits properties x and y
let o2 = Object.create(null); // o2 inherits no properties or methods
let o3 = Object.create(Object.prototype); // same as {} or new Object()
let o4 = { x: 'preserve this value' };
let o5 = Object.create(o4); // o5 guards o4 against modification

Object properties can be obtained, created, and changed by either dot notation . or array notation []. Array notation is more flexible because strings can be created and manipulated while the program is running.

let author = book.author; // Get the 'author' property of book
book.edition = 1; // Create the 'edition' property of book
book['title'] = ['LoveScript']; // Change the 'title' property of book

Inheritance

The prototype attribute of an object creates a chain of linked list from which properties are inherited. Inheritance occurs when querying properties but not when setting them, therefore we can selectively override inherited properties. It is not possible to override a read-only inherited property.

let unitCircle = { r: 1 }; // Object to inherit from
let circle = Object.create(unitCircle); // create object from prototype
circle.x = 1; // circle defines its own property x
circle.r; // => 1: circle inherits property r
circle.r = 2; // circle overrides its inherited property r
unitCircle.r; // => 1: the prototype is not affected

Property access errors

If a queried property is not found as an own property or an inherited property, the property access expression evaluates to undefined. But querying or setting a property of a non-existing object raises an error. In such case, use the conditional property access operator ?..

let lastName = book?.editor?.lastName; // => undefined

Deleting properties

The delete operator removes an own property from an object and evaluates to true of the delete operation was successful or if it had no effect. If the property cannot be deleted, the delete expression evaluates to false.

delete book.author; // => true
delete book['likes']; // => true
delete book['likes']; // => true: does nothing (property does not exist)
delete book.toString(); // => true: does nothing (inherited property)

delete Object.prototype; // => false: property is non-configurable
function f() {} // Declare a global function
delete globalThis.f; // => false: cannot delete global function
globalThis.p = 1; // Create a configurable global property
delete globalThis.p; // => true: this property can be deleted

Testing properties

The in operator returns true if the object has an own or inherited property. The hasOwnProperty() method of an object tests if the object has an own property.

'title' in book; // => true: own property
'author' in book; // => false: no property
'toString' in book; // => true: inhertited property
book.subtitle = undefined;
'subtitle' in book; // => true: property exists but is undefined

book.hasOwnProperty('title'); // => true: own property
book.hasOwnProperty('subtitle'); // => true: own property
book.hasOwnProperty('author'); // => false: no property
book.hasOwnProperty('toString'); // => false: inhertited property

Enumerating properties

To get an array of property names, use the for/of loop with the following static functions defined on the Object class. Object.keys() returns an array of enumerable own property names. Object.getOwnPropertyNames() returns an array of enumerable and non-enumerable own property names. Object.getOwnPropertySymbols() returns enumerable and non-enumerable own properties whose names are Symbols. Reflect.ownKeys() returns enumerable and non-enumerable own property names for both strings and Symbols.

for (let p of Object.keys(book)) {
  console.log(p); // Prints title, edition, subtitle
}

Extending objects

The static function of Object class Object.assign() copies enumerable own properties (including Symbols) from the source objects into the target object. The target object is the first argument.

let obj = {
  x: 2,
};
const defaults = {
  x: 1,
  y: 1,
};
// Copy default properties into another object
// if a property does not already exist
obj = Object.assign({}, defaults, obj);
obj.x; // => 2

Serializing objects

Object serialization is the process of converting an object's state to a string from which it can later be restored. JSON.stringify() serializes an object, and JSON.parse() restores JavaScript objects. JSON stands for "JavaScript Object Notation". Objects, arrays, strings, finite numbers, boolean values, and null can be serialized and restored. NaN, Infinity, -Infinity are serialized to null. Functions, regular expressions, Error objects and undefined cannot be serialized or restored.

let myObj = { x: 1, y: { z: [false, null, ''] } };
let myStr = JSON.stringify(myObj);
// => myStr === '{"x":1,"y":{"z":[false,null,""]}}'
let restoredObj = JSON.parse(myStr);

Object methods

The toString() method returns a string that represents the value of the object. You can define your own toString() method.

let point = {
  x: 1000,
  y: 2000,
};

point.toString = function() {return `(${this.x}, ${this.y})`; };
String(point); // => '(1000, 2000)'

The toLocaleString() method returns a localized string representation of the object. By default, it simply calls the toString() method.

point.toLocaleString = function() {
  return `(${this.x.toLocaleString()}, ${this.y.toLocaleString()})`;
};
point.toLocaleString(); // => '(1,000, 2,000)'

The valueOf() method is called when JavaScript needs to convert an object to a number.

point.valueOf = function() { return Math.hypot(this.x, this.y); };
Number(point); // => 2236.06797749979
point > 2300; // => false
point < 2400; // => true

Extended syntax

You can create object properties from variable names in a simplified way.

let x = 1, y = 2;
let sObj = { x, y };
sObj.x + sObj.y; // => 3

Computed property names can be used to name a property with an arbitrary JavaScript expression in square brackets.

const PROPERTY_NAME = 'p1';
function computePropertyName() { return 'p' + 2; }
let cObj = {
  [PROPERTY_NAME]: 1,
  [computePropertyName()]: 2
};
cObj.p1 + cObj.p2; // => 3

Symbols are primitive values that can be used as object property names. Symbols are created by calling the Symbol() function, which produces a symbol primitive value. Symbols are all unique even when created with the same string argument. Use symbols when you need to add own properties to an object that must not conflict with existing property names.

const extension = Symbol('my extension symbol');
let eObj = {
  [extension]: { /* extention data object */ }
};
// This won't conflict with other properties of eObj
eObj[extension].x = 0;

In object literals, the spread operator ... enables the interpolation of one object into another. When property names coincide, the value of that property will be determined by the object that appears last in the interpolation sequence. Only own properties of an object are spread.

let pos = { x: 1, y: 2 };
let rect = { ...pos, x: 3 };
rect.x + rect.y; // => 5

Method definition in objects have a shorthand syntax where function keyword and a colon are omitted. Moreover, you can use computed property names, including symbols.

const METHOD_NAME = 'm';
const symbol = Symbol();
let square = {
  side: 10,
  area: function() { return this.side * this.side; },
  perimeter() { return 4 * this.side; },
  'my func'(x) { return x + 1; },
  [METHOD_NAME](x) { return x + 2; },
  [symbol](x) { return x + 3; }
}
square.area(); // => 100
square.perimeter(); // => 40
square['my func'](1); // => 2
square[METHOD_NAME](1); // => 3
square[symbol](1); // => 4

Computed properties

Computed properties (or accessor properties) as opposed to stored properties, do not hold a single value. Instead, they have getter and setter methods with the same name. The getter method is called when the property is accessed, computing the value dynamically. The setter method is called when the property is assigned a new value, which allows you to perform actions or validations when the property is modified.

let myPoint = {
  x: 1.0,
  y: 1.0,
  get r() { return Math.hypot(this.x, this.y); },
  set r(newValue) {
    if (r < 0) throw new Error('Negative numbers are not allowed!');
    let oldValue = Math.hypot(this.x, this.y);
    let ratio = newValue / oldValue;
    this.x *= ratio;
    this.y *= ratio;
  },
  get theta() { return Math.atan2(this.y, this.x); }
};
myPoint.r; // => 1.4142135623730951
myPoint.theta // => 0.7853981633974483