Lakshan Perera

Revisiting JavaScript Objects

During the holidays, I spent some time catching up on the developments in ES6 (next version of JavaScript). While going through some of the proposals such as Minimal Class Definitions, Proxy API and Weak Maps; I noticed most of these enhancements make extensive use of the object manipulation features introduced in ES5 (ie. ECMAScript5 - the current JavaScript standard).

One of the main focuses of ES5, has been to improve the JavaScript's object structure and manipulation. The features it introduced do make lot of sense, especially if you're working with large and complex applications.

We've been little reluctant to adopt ES5 features, especially due to browser compatibility issues. We rarely see production code that make use of these features. However, all modern browsers (ie. IE9, FF4, Opera 12 & Chrome) do have JavaScript engines that implement the ES5 standard. Also, ES5 features can be used in Node.js based projects without any issue. So I think it would be a worthwhile exercise to revisit the ES5 object features and see how they can be used in real-life scenarios.

Data and Accessor Properties

ES5 introduces two kinds of object properties - data and accessors. A data property directly maps a name to a value (eg. integer, string, boolean, array, object or a function). An accessor property maps a name to a defined getter and/or setter function.

var square = {
    length: 10,
    get area() { return this.length * this.length },
    set area(val) { this.length = Math.sqrt(val) }
}

Here we have defined a square object, with length as a data property and area as an accessor property.

> square.length
  10
> square.area
  100
> square.area = 400
  400
> square.length
  20

When we access the area property, its getter will calculate and return the value in terms of the length property. Also, when we assign a value to area, its setter function will change the length property.

Property Descriptor

ES5 allows you to have more fine-grained control over the properties defined in an object. There's a special attribute collection associated with each property, known as the property descriptor.

You can check the attributes associated to a property by calling the Object.getOwnPropertyDescriptor method.

> Object.getOwnPropertyDescriptor(square, "length")
{
    configurable: true
    enumerable: true
    value: 20
    writable: true
}

> Object.getOwnPropertyDescriptor(square, "area")
{
    configurable: true
    enumerable: true
    get: function area() { return this.length * this.length }
    set: function area(val) { this.length = Math.sqrt(val) }
}

As you can see from the above two examples - value and writeable attributes are only defined for data property descriptors, while get and/or set are defined for accessor property descriptors. Both configurable and enumerable attributes applies to any kind of property descriptor.

The writable attribute specify whether a value can be assigned to a property. If writable is false, property becomes read-only. As the name implies, configurable specifies whether the property's attributes are configurable and also whether the property can be deleted from the object (using the delete operation). The enumerable attribute determines whether the property should be visible in for..in loops or Object.keys methods.

We can modify these attributes in the property descriptor by using the Object.defineProperty method.

Object.defineProperty(square, "length", {
    value: 10,
    writable: false
});

This will make the length property in square read-only and permanently set to 10.

> square.length
  10
> square.area = 400
  400
> square.length
  10
> square.area
  100

Tamper-proof Objects

On some instances, you need to preserve the objects in its current state during the run-time without any further extensions or modifications to the properties. ES5 provides three levels of controls that you can apply to the objects.

Calling preventExtensions method will make the object non-extensible. This means no further properties can be defined for the object.

> Object.preventExtensions(square);

> Object.defineProperty(square, "text", { value: "hello" });
  TypeError: Cannot define property:text, object is not extensible.

Sealing the object, will prevent both defining of new properties and the deletion of existing properties in the object.

> Object.seal(square);

> delete square.length
  false

If we go one step further and freeze the object, it will also disallow changing the existing property values in the object. At this point, whole object effectively becomes a constant.

> Object.freeze(square);

> square.length = 20
  20
> square.length
  10

You can use the methods Object.isSealed, Object.isFrozen and Object.isExtensible to programmatically check the state of an object.

Even though an object is protected, it would still be possible to extend its prototype. Check the following example:

var obj = Object.create({}, { onlyProp: { value: true } });
Object.preventExtensions(obj);

var proto = Object.getPrototypeOf(obj);
proto.anotherProp = true;

> obj.anotherProp
  true

Enumerations

Often, we use JavaScript objects as associative arrays or collections. On such instances, we are tempted to use for...in loops to enumerate over the properties. However, the loop will step through all enumerable properties available in object's prototypal chain, resulting with undesired outcomes.

To avoid such side effects, JSLint suggests to manually check whether the given property is defined in the object.

for (name in object) { if (object.hasOwnProperty(name)) { .... } }

ES5 provides Object.keys method, which would return an array of own enumerable properties of an object.

We can use this method to safely iterate over a property list:

Object.keys(obj).forEach( function(key) {
    console.log(key);
});

Note: Array.forEach is also a new feature introduced in ES5

Inheritance

We know JavaScript provides behavior reuse in terms of prototypal inheritance. However, lack of direct mechanism to create a new object using another object as a prototype, has been one of pet peeves in the language.

The standard way to create a new object is to use a constructor function. This way, the newly created object will inherit the prototype of the constructor function.

var Person = function(first_name, last_name) {
    this.first_name = first_name;
    this.last_name = last_name;
}

Person.prototype = {
    say: function(msg) {
        return this.first_name + " says " + msg;
    }
}

var ron = new Person("Ron", "Swanson");

If someone calls the constructor function without the new operator, it could lead to unwarranted side-effects during the execution. Also, there's no semantical relationship between the constructor function and its prototype, which could cause confusions when trying to comprehend the code.

For those who prefer to have a alternate syntax, ES5 provides the Object.create method. It takes a prototype object and a property descriptor as arguments.

Here's an alternate implementation that can be used to create Person objects, using the Object.create and module pattern.

var Person = (function() {

    var proto = {
        say: function(msg) {
            return this.first_name + " says " + msg;
        }
    }

    return {
        init: function(first_name, last_name) {
            return Object.create(proto, {
                first_name: { value: first_name, enumerable: true },
                last_name: { value: last_name, enumerable: true }
            });
        }
    }

})();

var ron = Person.init("Ron", "Swanson");

However, compared to constructor functions using Object.create could be considerably slow. So choose which implementation you want to use depending on the context and requirements.

Even if you use prefer to use constructor functions, Object.create will come in handy when you want to have multiple levels of inheritance.

var Person = function(first_name, last_name) {
    this.first_name = first_name;
    this.last_name = last_name;
};

Person.prototype = {
    say: function(msg) {
        return this.first_name + " says " + msg;
    }
};

var Employee = function(first_name, last_name) {
    Person.call(this, first_name, last_name);
}

Employee.prototype = Object.create(Person.prototype, {
    department: { value: "", enumerable: true },
    designation:{ value: "", enumerable: true }
});

var ron = new Employee("Ron", "Swanson");

We've extended the Person prototype to create the prototype of Employee.

Cloning Objects

Finally, let's see how to create a shallow clone of an object using ES5's object methods.

var clone = function(obj) {
    // create clone using given object's prototype
    var cloned_obj = Object.create(Object.getPrototypeOf(obj));

    // copy all properties
    var props = Object.getOwnPropertyNames(obj);
    props.forEach(function(prop) {
        var propDescriptor = Object.getOwnPropertyDescriptor(obj, prop);
        Object.defineProperty(cloned_obj, prop, propDescriptor);
    });

    return cloned_obj;
}

Here, we retrieve the prototype of the given object and using it to create the clone. Then we traverse all properties defined in the object (including the non-enumerable properties) and copy their property descriptors to the clone.

Further Reading

If you're interested in learning more about the JavaScript objects and how to manipulate them, I would recommend you to peruse the following resources: