Advanced objects in JavaScript

This posts looks beyond everyday usage of JavaScript’s objects. The fundamentals of JavaScripts objects are for the most part about as simple as using JSON notation. However, JavaScript also provides sophisticated tools to create objects in interesting and useful ways, many of which are now available in the latest versions of modern browsers.

The last two topics I talk about, Proxy and Symbol, are based on the ECMAScript 6 specification and are only partially implemented and of limited availability across browsers.

 getters and setters

Getters and setters have been available in JavaScript for some time now but I have not found myself using them much. I often fallback to writing regular functions to get properties. I usually end up writing something like this:

/**
 * @param {string} prefix
 * @constructor
 */
function Product(prefix) {
  /**
   * @private
   * @type {string}
   */
  this.prefix_ = prefix;
  /**
   * @private
   * @type {string}
   */
  this.type_ = "";
}

/**
 * @param {string} newType
 */
Product.prototype.setType = function (newType) {
  this.type_ = newType;
};

/**
 * @return {string}
 */
Product.prototype.type = function () {
  return this.prefix_ + ": " + this.type_;
}

var product = new Product("fruit");
product.setType("apple");
console.log(product.type());  //logs fruit: apple

jsfiddle

Using a getter I could simplify this code.

/**
 * @param {string} prefix
 * @constructor
 */
function Product(prefix) {
  /**
   * @private
   * @type {number}
   */
  this.prefix_ = prefix;
  /**
   * @private
   * @type {string}
   */
  this.type_ = "";
}

/**
 * @param {string} newType
 */
Product.prototype = {
    /**
     * @return {string}
     */
    get type () {
      return this.prefix_ + ": " + this.type_;
    },
    /**
     * @param {string}
     */
    set type (newType) {
      this.type_ = newType;
    }
};

var product = new Product("fruit");

product.type = "apple";
console.log(product.type); //logs "fruit: apple"

console.log(product.type = "orange");  //logs "orange"
console.log(product.type); //logs "fruit: orange"

jsfiddle

The code is still a bit verbose and the syntax is a bit unusual but the benefit of get and set is realized in using the property. Personally I find something like

product.type = "apple";
console.log(product.type);

a lot more readable and accessible than

product.setType("apple");
console.log(product.type());

Although directly accessing and setting properties on instances still sets off my internal bad-javascript buzzer. Over time I have been trained by bugs and technical debt to avoid arbitrarily setting values on instances as a means to pass information around. Also, a particular caveat is in order for the return value of an assignment. Note this bit of code in the example above:

console.log(product.type = "orange");  //logs "orange"
console.log(product.type); //logs "fruit: orange"

Notice that “orange” is logged at first and then “fruit: orange” on the next line. The getter isn’t executed on the return value from assigning to a property, so this kind of shortcut where you return the value from an assignment could get you into trouble here. Return statements on a set are ignored. Adding return this.type; to the set wont fix this problem. Usually reusing the value of an assignment would work, but you may have problems with a property that has a getter.


 defineProperty

The get propertyname () syntax works on object literals and in the previous example I assigned an object literal to Product.prototype. This is OK, but using object literals like this makes it harder to chain prototypes to get inheritance. You can create getters and setters on a prototype without an object literal using defineProperty.

/**
 * @param {string} prefix
 * @constructor
 */
function Product(prefix) {
  /**
   * @private
   * @type {number}
   */
  this.prefix_ = prefix;
  /**
   * @private
   * @type {string}
   */
  this.type_ = "";
}

/**
 * @param {string} newType
 */
Object.defineProperty(Product.prototype, "type", {
  /**
   * @return {string}
     */
  get: function () {
      return this.prefix_ + ": " + this.type_;
  },
  /**
   * @param {string}
  */
  set: function (newType) {
    this.type_ = newType;
  }
});

jsfiddle

This code behaves the same as the previous example. There’s more to defineProperty than adding getters or setters. The third argument to defineProperty is called the descriptor and in addition to set and get it allows you to customize the property’s accessibility and set a value. You could use the descriptor argument to defineProperty to create something like a constant that can never be modified or removed.

var obj = {
    foo: "bar",
};


//A normal object property
console.log(obj.foo); //logs "bar"

obj.foo = "foobar";
console.log(obj.foo); //logs "foobar"

delete obj.foo;
console.log(obj.foo); //logs undefined


Object.defineProperty(obj, "foo", {
    value: "bar",
});

console.log(obj.foo); //logs "bar", we were able to modify foo

obj.foo = "foobar";
console.log(obj.foo); //logs "bar", write failed silently

delete obj.foo;
console.log(obj.foo); //logs bar, delete failed silently

jsfiddle

The result is:

bar
foobar
undefined
bar 
bar
bar 

The last 2 attempts to modify foo.bar in the example failed silently as the default behavior of defineProperty is to prevent further changes. You can use configurable and writable to change this behavior. If you are using strict mode the failures are not silent, they are JavaScript errors.

var obj = {};

Object.defineProperty(obj, "foo", {
    value: "bar",
    configurable: true,
    writable: true,
});

console.log(obj.foo); //logs "bar"
obj.foo = "foobar";
console.log(obj.foo); //logs "foobar"
delete obj.foo;
console.log(obj.foo); //logs undefined

jsfiddle

The configurable key allows you to prevent the property from being deleted from the object. It also allows you to prevent the property from being modified in the future with another call todefineProperty later. The writable key enables you to write to the property and change its value.

If configurable is false as is the default case, attempting to call defineProperty a second time will result in a JavaScript error, it does not fail silently.

var obj = {};

Object.defineProperty(obj, "foo", {
    value: "bar",
});


Object.defineProperty(obj, "foo", {
    value: "foobar",
});

// Uncaught TypeError: Cannot redefine property: foo 

jsfiddle

If configurable is set to true you can modify the property again later. You might use that to change the value on a non-writable property.

var obj = {};

Object.defineProperty(obj, "foo", {
    value: "bar",
    configurable: true,
});

obj.foo = "foobar";

console.log(obj.foo); // logs "bar", write failed

Object.defineProperty(obj, "foo", {
    value: "foobar",
    configurable: true,
});

console.log(obj.foo); // logs "foobar"

jsfilddle;

Also note that values defined by defineProperty are by default not iterated over in a for in loop:

var i, inventory;

inventory = {
    "apples": 10,
    "oranges": 13,
};

Object.defineProperty(inventory, "strawberries", {
    value: 3,
});

for (i in inventory) {
    console.log(i, inventory[i]);
}

jsfiddle

apples 10 
oranges 13 

Use the enumerable key to allow this:

var i, inventory;

inventory = {
    "apples": 10,
    "oranges": 13,
};

Object.defineProperty(inventory, "strawberries", {
    value: 3,
    enumerable: true,
});

for (i in inventory) {
    console.log(i, inventory[i]);
}

jsfiddle

Outcome:

apples 10
oranges 13
strawberries 3 

You can use isPropertyEnumerable to test whether a property will appear in a for in loop:

var i, inventory;

inventory = {
    "apples": 10,
    "oranges": 13,
};

Object.defineProperty(inventory, "strawberries", {
    value: 3,
});

console.log(inventory.propertyIsEnumerable("apples")); //console logs true
console.log(inventory.propertyIsEnumerable("strawberries")); //console logs false

jsfilddle

propertyIsEnumerable will also return false for properties defined further up an object’s prototype chain or for properties that aren’t otherwise defined on the object, obviously.

Finally, a last couple points about using defineProperty: It is an error to combine the accessors set and get with writable set to true or to combine them with a value. Defining a property as a number simply converts the number into a string just as it would in any other circumstance. You can also use defineProperty to set value to be a function.


 defineProperties

Also Object.defineProperties exists. It allows you to define multiple properties in a single go. I found a jsperf that compared the performance of defineProperty versus defineProperties and in Chrome at least it didn’t seem to matter much which was used.

var foo = {}

Object.defineProperties(foo, {
    bar: {
        value: "foo",
        writable: true,
    },
    foo: {
        value: function() {
           console.log(this.bar);
        }
    },
});

foo.bar = "foobar";
foo.foo();  //logs "foobar"

jsfiddle


 Object.create

Object.create is an alternative to new that lets you create an object with a given prototype. This function has two arguments, the first is the prototype you want use for the newly created object, the second argument is a property descriptor and it takes the same form as you would give to Object.defineProperties.

var prototypeDef = {
    protoBar: "protoBar",
    protoLog: function () {
        console.log(this.protoBar);
    }
};
var propertiesDef = {
    instanceBar: {
        value: "instanceBar"
    },
    instanceLog: {
        value: function () {
            console.log(this.instanceBar);
        }
    }
}

var foo = Object.create(prototypeDef, propertiesDef);
foo.protoLog(); //logs "protoBar"
foo.instanceLog(); //logs "instanceBar"

jsfiddle

Properties created in the properties descriptor argument to Object.create overwrite the values in the prototype argument:

var prototypeDef = {
    bar: "protoBar",
};
var propertiesDef = {
    bar: {
        value: "instanceBar",
    },
    log: {
        value: function () {
            console.log(this.bar);
        }
    }
}

var foo = Object.create(prototypeDef, propertiesDef);
foo.log(); //logs "instanceBar"

jsfiddle

Setting a non-primitive type like an Array or Object as a defined properties value in Object.create is probably a mistake since you will create a single instance shared by all created objects:

var prototypeDef = {
    protoArray: [],
};
var propertiesDef = {
    propertyArray: {
        value: [],
    }
}

var foo = Object.create(prototypeDef, propertiesDef);
var bar = Object.create(prototypeDef, propertiesDef);

foo.protoArray.push("foobar");
console.log(bar.protoArray); //logs ["foobar"] 
foo.propertyArray.push("foobar");
console.log(bar.propertyArray); //also logs ["foobar"] 

jsfiddle

You could fix this problem by initialize the value of propertyArray with null and then add the array when you needed, or you could do something fancy like this using a getter:

var prototypeDef = {
    protoArray: [],
};
var propertiesDef = {
    propertyArrayValue_: {
        value: null,
        writable: true
    },
    propertyArray: {
        get: function () {
            if (!this.propertyArrayValue_) {
                this.propertyArrayValue_ = [];
            }
            return this.propertyArrayValue_;
        }
    }
}

var foo = Object.create(prototypeDef, propertiesDef);
var bar = Object.create(prototypeDef, propertiesDef);

foo.protoArray.push("foobar");
console.log(bar.protoArray); //logs ["foobar"] 
foo.propertyArray.push("foobar");
console.log(bar.propertyArray); //logs [] 

jsfiddle

This is a neat way to combine initialization of properties with their definitions. I think I prefer keeping property definitions with initialization together a great deal over doing this work in a constructor. In the past I often ended up writing a giant constructor method full of such initialization code.

The previous example demonstrates that you have to remember that the expressions provided to any value in an Object.create property descriptor is evaluated when the property descriptor object is defined. That is why the array was shared across instances. I also recommend never depending on a fixed order for when multiple properties are evaluated in a single call. If you really wanted to initialize one property before the other perhaps just use Object.defineProperty in that instance.

Since using Object.create does not involve a constructor function you lose the ability to use instanceof to test Object identity. Instead use isPrototypeOf which is checked against the prototype object. This would be MyFunction.prototype in the case of a constructor, or the object provided to the first argument of Object.create.

function Foo() {
}

var prototypeDef = {
    protoArray: [],
};
var propertiesDef = {
    propertyArrayValue_: {
        value: null,
        writable: true
    },
    propertyArray: {
        get: function () {
            if (!this.propertyArrayValue_) {
                this.propertyArrayValue_ = [];
            }
            return this.propertyArrayValue_;
        }
    }
}

var foo1 = new Foo();

//old way using instanceof works with constructors
console.log(foo1 instanceof Foo); //logs true

//You check against the prototype object, not the constructor function
console.log(Foo.prototype.isPrototypeOf(foo1)); //true

var foo2 = Object.create(prototypeDef, propertiesDef);

//can't use instanceof with Object.create, test against prototype object...
//...given as first agument to Object.create
console.log(prototypeDef.isPrototypeOf(foo2)); //true

jsfiddle

isPrototypeOf will walk down the prototype chain and return true if any prototype matches the prototype object tested against.

var foo1Proto = {
    foo: "foo",
};

var foo2Proto = Object.create(foo1Proto);
foo2Proto.bar = "bar";

var foo = Object.create(foo2Proto);

console.log(foo.foo, foo.bar); //logs "foo bar"
console.log(foo1Proto.isPrototypeOf(foo)); // logs true
console.log(foo2Proto.isPrototypeOf(foo)); // logs true

jsfiddle


 sealing objects, freezing them and preventing extensibility

Adding arbitrary properties to random objects and instances, just because you can, has always been at the very least a code smell. With modern browsers and in node.js it is possible to restrict changes to an entire object in addition to restricting individual properties of an object via defineProperty. Object.preventExtensions, Object.seal and Object.freeze each in turn add increasingly stricter restrictions on an object. In strict mode violating restrictions placed by these methods will result in JavaScript errors, otherwise they fail silently.

Object.preventExtensions will prevent new properties from being added to an object. It will not prevent changes to existing writable properties, and it will not prevent deletion of configurable properties. Object.preventExtensions will also not remove the ability to call Object.defineProperty to modify existing properties.

var obj = {
    foo: "foo",
};

obj.bar = "bar";
console.log(obj); // logs Object {foo: "foo", bar: "bar"} 

Object.preventExtensions(obj);

delete obj.bar;
console.log(obj); // logs Object {foo: "foo"} 

obj.bar = "bar";
console.log(obj); // still logs Object {foo: "foo"} 

obj.foo = "foobar"
console.log(obj); // logs {foo: "foobar"} can still change values

jsfiddle

(note you should run the previous jsfiddle with web inspector open at the start or refresh because logging objects with it closed may result in the console showing only the final form the object takes)

Object.seal goes further than Object. preventExtensions. In addition to preventing new properties from being added to an object, this function also prevents further configurability and removes the ability to delete properties. Once an object has been sealed, you can no longer modify existing properties with defineProperty. As previously mentioned if you try to violate restrictions in strict mode the result will be a JavaScript error.

"use strict"; 

var obj = {};

Object.defineProperty(obj, "foo", {
    value: "foo"
});

Object.seal(obj);

//Uncaught TypeError: Cannot redefine property: foo 
Object.defineProperty(obj, "foo", {
    value: "bar"
});

jsfiddle

You can also not remove properties anymore, even if they were initially configurable. You can still change the values of properties.

"use strict"; 

var obj = {};

Object.defineProperty(obj, "foo", {
    value: "foo",
    writable: true,
    configurable: true,
});

Object.seal(obj);

console.log(obj.foo); //logs "foo"
obj.foo = "bar";
console.log(obj.foo); //logs "bar"
delete obj.foo; //TypeError, cannot delete

jsfiddle

Finally, Object.freeze makes an object entirely immutable. You cannot add, remove, or change the values of properties on frozen objects. You can also no longer use Object.defineProperty on the object to change the values of existing properties.

"use strict"; 

var obj = {
    foo: "foo1"
};

Object.freeze(obj);

//All of the following will fail, and result in errors in strict mode
obj.foo = "foo2"; //cannot change values
obj.bar = "bar"; //cannot add a property
delete obj.bar; //cannot delete a property
//cannot call defineProperty on a frozen object
Object.defineProperty(obj, "foo", {
    value: "foo2"
});

jsfiddle

Methods are provided to allow you to test if objects are frozen, sealed or not extensible: Object.isFrozen, Object.isSealed and Object.isExtensible.


 valueOf and toString

You can use valueOf and toString to customize how an object you have defined behaves in contexts where JavaScripts expects a primitive value.

Here’s an example using toString:

function Foo (stuff) {
    this.stuff = stuff;
}

Foo.prototype.toString = function () {
    return this.stuff;
}


var f = new Foo("foo");
console.log(f + "bar"); //logs "foobar"

jsfiddle

And here’s an example using valueOf:

function Foo (stuff) {
    this.stuff = stuff;
}

Foo.prototype.valueOf = function () {
    return this.stuff.length;
}

var f = new Foo("foo");
console.log(1 + f); //logs 4 (length of "foo" + 1);

jsfiddle

If you combine both toString and valueOf you may get unexpected results.

function Foo (stuff) {
    this.stuff = stuff;
}

Foo.prototype.valueOf = function () {
    return this.stuff.length;
}

Foo.prototype.toString = function () {
    return this.stuff;
}

var f = new Foo("foo");
console.log(f + "bar"); //logs "3bar" instead of "foobar"
console.log(1 + f); //logs 4 (length of "foo" + 1);

jsfiddle

A neat way to use toString might be to make your object hashable:

function Foo (stuff) {
    this.stuff = stuff;
}

Foo.prototype.toString = function () {
    return this.stuff;
}

var f = new Foo("foo");

var obj = {};
obj[f] = true;
console.log(obj); //logs {foo: true}

jsfiddle


 getOwnPropertyNames and keys

You can use Object.getOwnPropertyNames to get all the properties defined on an object. If you’re familiar with Python, it is basically analogous to a Python dictionary’s keys method, and there actually is an Object.keys method as well. The difference between Object.keys and Object.getOwnPropertyNames is that the latter also iterates over property names that are not enumberable, that is Object.getOwnPropertyNames will also return property names that are not iterated over in a for in loop.

var obj = {
    foo: "foo",
};

Object.defineProperty(obj, "bar", {
    value: "bar"
});

console.log(Object.getOwnPropertyNames(obj)); //logs ["foo", "bar"]
console.log(Object.keys(obj));  //logs ["foo"]

jsfiddle


 Symbol

Symbol is a special new primitive type defined in ECMAScrpt 6 harmony and will be available in the next iteration of JavaScript. You can already start using it in Chrome Canary and Firefox Nightly and the following jsfiddle examples will only work in these two browsers, at least at the time of writing this post, August 2014.

Symbols can be used as way to create and reference properties on objects.

var obj = {};

var foo = Symbol("foo");

obj[foo] = "foobar";

console.log(obj[foo]); //logs "foobar"

jsfiddle (Chrome Canary and Firefox Nightly only)

Symbols are unique and immutable.

//console logs false, symbols are unique:
console.log(Symbol("foo") === Symbol("foo"));

jsfiddle (Chrome Canary and Firefox Nightly only)

You can also use symbols with Object.defineProperty:

var obj = {};

var foo = Symbol("foo");

Object.defineProperty(obj, foo, {
    value: "foobar",
});

console.log(obj[foo]); //logs "foobar"

jsfiddle (Chrome Canary and Firefox Nightly only)

Properties added to objects with symbols will not be iterated over in a for in loop, but calling hasOwnProperty will work fine:

var obj = {};

var foo = Symbol("foo");

Object.defineProperty(obj, foo, {
    value: "foobar",
});

console.log(obj.hasOwnProperty(foo)); //logs true

jsfiddle (Chrome Canary and Firefox Nightly only)

Symbols will not appear in the returned array from a call to Object.getOwnPropertyNames but there is a Object. getOwnPropertySymbols.

var obj = {};

var foo = Symbol("foo");

Object.defineProperty(obj, foo, {
    value: "foobar",
});

//console logs []
console.log(Object.getOwnPropertyNames(obj));

//console logs [Symbol(foo)]
console.log(Object.getOwnPropertySymbols(obj));

jsfiddle (Chrome Canary and Firefox Nightly only)

Symbols could be handy in the case where you want to not only prevent a property from being accidentally modified, but you don’t even want it to show up in the normal course of business. I haven’t really thought through all the potential use cases, but I think I can think of more.


 Proxy

Another new ECMAScript 6 addition is Proxy. As of August 2014, proxies only work in Firefox. The following jsfiddle example will only work in Firefox, and I actually tested it using Firefox beta because that is what I have installed.

Proxies are exciting to me because it allows for the creation of catch all properties. Check out the following example:

var obj = {
    foo: "foo",
};
var handler = {
    get: function (target, name) {
        if (target.hasOwnProperty(name)) {
            return target[name];
        }
        return "foobar";
    },
};
var p = new Proxy(obj, handler);
console.log(p.foo); //logs "foo"
console.log(p.bar);  //logs "foobar"
console.log(p.asdf); //logs "foobar"

jsfiddle (Firefox only)

In this example we are proxying object obj. We define a handler object that handles interaction with the proxy object we end up creating. The get method on handler should be pretty simple to understand. It gets the target object as an argument as well as the name of the property that was accessed. We can use this information to return whatever we want, but in this case I return the object’s actual value if it has one and if not I return “foobar”. I love this, there are so many interesting use cases for this, maybe one could even use this for a kind of type switch as it exists in Scala or use your own imagination.

Another great place where Proxy would come in handy is in testing. Beyond just get there are other handlers you can add including set, has and more. Once Proxy becomes better supported and more stable I would not mind writing an entire blog post just about Proxy. I recommend reading the MDN documentation on Proxy in full and checking out the provided examples. There’s also a jsconf talk about the many ways that Proxy is awesome that I recommend checking out: video slides.


So there is way more to JavaScript objects than simply using them as glorified bags of arbitrary data. Even now powerful property definitions are possible, and the future has even more in store as you can see when you think about all the ways that Proxy can change the way that JavaScript will be written. If you have any questions or corrections, pretty please let me know on Twitter, you can find me there as @bjorntipling.

I have updated this blog post to clarify the difference between Object.keys and Object. getOwnPropertyNames thanks wging and TazeTSchnitzel

I also updated this post to clarify that Firefox Nightly has Symbol thanks Excavator

 
2,885
Kudos
 
2,885
Kudos

Now read this

all this

Coming from a sane language you might think that this in JavaScript is kind of like this in an object oriented language like Java, something that refers to values stored in instance properties. Not so. In JavaScript it is best to think... Continue →

Subscribe to Bjorn Tipling

Don’t worry; we hate spam with a passion.
You can unsubscribe with one click.

0obbK04LYREL9i0ib5l