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
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"
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;
}
});
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.test); //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
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.test); //logs undefined
The configureable
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
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"
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]);
}
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]);
}
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
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"
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"
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"
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"]
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 []
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 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
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
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
(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"
});
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
Finally, Object.freeze
makes an object entirely immutable. You cannot add, remove, 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"
});
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"
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);
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);
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}
getOwnPropertyNames
You can use Object.getOwnPropertyNames
to get all the properties defined on an object. This might be a nice alternative to a for in
loop. If you’re familiar with Python, it is basically analogous to a Python dictionary’s keys
method:
var obj = {
foo: "foo"
};
Object.defineProperty(obj, "bar", {
value: "bar"
});
console.log(Object.getOwnPropertyNames(obj)); //logs ["foo", "bar"]
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 the following jsfiddle examples will only work in Canary, 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 only)
Symbols are unique and immutable.
//console logs false, symbols are unique:
console.log(Symbol("foo") === Symbol("foo"));
jsfiddle (Chrome Canary 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 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 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 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 objects 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.