Fundamentals of Web Application Development · DRAFTFreeman

Flexible Objects

Earlier, we described objects as simple collections of key-value pairs called properties. Objects are often used in this way as basic map-like structures to store information, but JavaScript also allows for more dynamic and customizable behavior.

In most of this chapter’s examples, we will use object literal notation, which uses curly braces {} to enclose a list of property definitions. It is important to note that, with object literals, a new, unique object instance is created in memory each time its expression is evaluated. Even if two literals look identical,

let obj1 = { a: 123 };
let obj2 = { a: 123 };

obj1 === obj2; // false

// obj1 and obj2 are two distinct objects in memory

or if the same literal expression is run twice,

function foo() {
  return { a: 123 };
}

let obj1 = foo();
let obj2 = foo();

obj1 === obj2; // false

a different instance is created each time.

Using Object Literals

Computed Property Names

We’ve already seen how to create objects using object literal syntax:

let obj = {
  a: 42,
  b: 'abc',
  c: true,
  longPropertyName: 123,
};

We can also assign computed property names using square brackets to create properties whose keys are not known ahead of time. Similarly to the square bracket notation used to access properties on an object (e.g., obj[40 + 2] to access property '42' on object obj), the expression inside the square brackets will be evaluated and coerced to a string in order to determine the property name.

let foo = 'b';
let bar = 'Name';

let obj = {
  a: 42,
  [foo]: 'abc',
  c: true,
  ['long' + 'Property' + bar]: 123,
};

This notation must also be used to specify property names which are symbols rather than strings.

let sym = Symbol();

let obj = {
  [sym]: 'a symbol-keyed property',
};

obj[sym]; // 'a symbol-keyed property'

Shorthand Property Names

Sometimes we may want to encapsulate local lexical variables in an object using the same variable names.

let a = 42, b = 'abc', c = true;

let obj = { a: a, b: b, c: c };

If the property we want to create has the same name as a lexically available variable, we can use shorthand to assign that property in the object literal.

let a = 42, b = 'abc', c = true;

let obj = { a, b, c }; // { a: 42, b: 'abc', c: true }

Method Shorthand

Object properties can contain any value, including a reference to a function.

let obj = {
  add: function(a, b) {
    return a + b;
  },
};

obj.add(1, 2); // 3

Object properties which are functions are commonly referred to as methods. Method shorthand lets us define a function property in object literals using only its name and parentheses with any parameters.

let obj = {
  add(a, b) {
    return a + b;
  },
};

obj.add(1, 2); // 3

Accessor Properties: Getters and Setters

All of the properties we’ve used so far have been data properties, which are direct one-to-one mappings from a given key (name) to a value. By default, when we create properties on objects, they are data properties.

let obj = { foo: 123 };

obj.bar = 'abc';

obj.foo; // 123
obj.bar; // "abc"

Object properties can be defined in a different paradigm: accessor properties allow us to specify custom behavior to intercept and handle reading from and writing to a property.

We can define a getter function to dynamically calculate the value of a property when it is accessed – a pattern commonly referred to as computed properties (not to be confused with computed property names as described earlier). The value returned by the getter function will be used as the value of the property lookup.

let person = {
  firstName: 'Alice',
  lastName: 'Programmer',
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  },
};

person.fullName; // "Alice Programmer"

Notice that the function was invoked without the use of parentheses. From the perspective of the code using the object, it is not apparent whether accessing a property will read a data value directly or invoke a getter function.

Similarly, a setter function can dynamically intercept a value being assigned to an object property. Setter functions accept one parameter, which will be bound to the value being assigned to the property.

let person = {
  set fullName(value) {
    let [first, last] = value.split(' ');
    this.firstName = first;
    this.lastName = last;
  },
};

person.fullName = 'Bob Programmer';

person.firstName; // "Bob"
person.lastName; // "Programmer"

Both a getter and setter may be set for accessor properties. However, neither a getter nor setter can coexist with a normal data property of the same name on the same object.

let obj = {
  get a() { return 123; },
  a: 'abc'
};

obj.a; // "abc"

Property Descriptors

Each property of an object has a few different attributes which specify the kinds of behaviors allowed for that property. Normal data properties have four attributes:

value
the value of the property
writable
(boolean) whether the value of the property can be changed using an assignment operator
configurable
(boolean) whether the property type (data or accessor) can be changed, and if the property can be deleted from its object
enumerable
(boolean) whether the property appears during enumeration of the object's properties

Together, these four attributes constitute a property’s descriptor. We can observe the values of these attributes using the static method Object.getOwnPropertyDescriptor. It accepts two arguments: the object on which to look, and the string (or symbol) of the property name. If the property exists on the specified object, it returns an object called a property descriptor which lists the property’s attributes and values. (If the property does not exist, it will return undefined).

By default, data properties created using object literal syntax or direct assignment are configurable, enumerable, and writable.

let obj = { a: 123 };

Object.getOwnPropertyDescriptor(obj, 'a');
// { value: 123, writable: true, enumerable: true, configurable: true }

obj.b = 'abc';

Object.getOwnPropertyDescriptor(obj, 'b');
// { value: "abc", writable: true, enumerable: true, configurable: true }

We can also define a new property and its accompanying attributes, or modify the attributes of an existing property, using the static method Object.defineProperty. The method accepts three arguments: the object on which to define the property, the name (or symbol) of the property, and a descriptor, which must itself be an object. The method will return a reference to the given target object.

If any of the attributes are not present on the provided descriptor, they default to false (and value: undefined).

let obj = {};

Object.defineProperty(obj, 'a', { value: 123 });

Object.getOwnPropertyDescriptor(obj, 'a');
// { value: 123, writable: false, enumerable: false, configurable: false }

writable

If a property is not writable, then its value cannot be changed using assignment operators. An assignment expression will still evaluate to the right-hand operand of the assignment operator, but the value of the property itself will not change.

let obj = Object.defineProperty({}, 'foo', { value: 123, writable: false });

obj.foo; // 123

obj.foo = 'abc'; // "abc"

obj.foo; // 123

enumerable

Properties which are enumerable will appear in contexts where an object’s properties are enumerated over, such as in for..in loops, with the object spread operator, or from the static methods Object.assign(), Object.keys(), Object.values(), and Object.entries() (each discussed more in ).

Non-enumerable properties can come in handy if we want to treat objects as key-value collections – and thus allow their entries to be enumerated over – but need to also store some other state or functionality on the same object that is not necessarily part of the collection itself.

We can explicitly check if an object’s property is enumerable using the inherited method Object.prototype.propertyIsEnumerable.

let obj = { foo: 123 };
obj.propertyIsEnumerable('foo'); // true

let obj2 = Object.defineProperty({}, 'bar', { value: 123 });
obj2.propertyIsEnumerable('bar'); // false

configurable

A property’s attributes (other than writable) are allowed to be changed, and the property is allowed to be deleted, only if it is configurable.

let obj = { foo: 123 };
Object.defineProperty(obj, 'bar', { value: 'abc', configurable: false });

delete obj.foo; // true
delete obj.bar; // false

obj.foo; // undefined
obj.bar; // "abc"

Object.defineProperty can also be used to modify an existing, configurable property. If used on an existing property, the descriptor provided will be “merged” with the existing descriptor; that is, attributes defined on the new descriptor will overwrite the old attribute values, but attributes not defined on the new descriptor will stay the same.

let obj = Object.defineProperty({}, 'foo', { value: 123, configurable: true });

obj.foo = 'abc'; // "abc"

obj.foo; // 123

// The property must be re-configured in order to make it writable again

Object.defineProperty(obj, 'foo', { writable: true });

obj.foo = 'abc'; // "abc"

obj.foo; // "abc"

obj.propertyIsEnumerable('foo'); // true
// Note: the second `defineProperty` did not overwrite
// obj.foo's existing `value`, `configurable`, or `enumerable` attributes

If a property is non-configurable but is writable, its value can be changed, and its writable attribute may also be changed from true to false.

Determining Property Existence

Freezing Objects