With ECMAScript 6 inching closer and closer to becoming an accepted standard, it’s a perfect time to look at some of the incredibly useful changes in the last big ES release. ECMAScript 5, published in 2009, gave JavaScript developers some great new options. One of its most powerful additions was Object.defineProperty
. This method allows you to make better use of the browser’s internals. John Resig explains:
This new code gives you the ability to dramatically affect how users will be able to interact with your objects, allowing you to provide getters and setters, prevent enumeration, manipulation, or deletion, and even prevent the addition of new properties. In short: You will be able to replicate and expand upon the existing JavaScript-based APIs (such as the DOM) using nothing but JavaScript itself.
This post shows you how to use Object.defineProperty
and some other useful Object
static methods. If you’re completely unfamiliar with those methods, you might want to review these resources before you read further:
(Note: there’s no shim for Object.defineProperty, but you can use it via Node.js.)
Creating a Class
In JavaScript, creating a class is pretty simple: define a function, then add some properties and methods. But defining a class this way has some serious flaws. Everything you do can be overridden!
Let’s take a look.
function Dog(name, age) { this.name = name; this.age = age; } |
If we instantiate this class, we can change the values at any time. We could even modify methods on the prototype. Depending on our requirements, this might be fine. But in this example, we can change the dog’s name, which doesn’t seem desirable. Let’s fix this using Object.defineProperties
, and also provide a setter and getter for age
while we’re at it:
function Dog(name, age) { Object.defineProperties(this, { name: { value: name, enumerable: true }, age: { set: function (value) { value = parseInt(value, 10); if (isNaN(value)) throw new Error("Value set on age is not a number"); age = value; }, get: function () { return age; }, enumerable: true } }); } |
The name
property now has a value
. And the writable
attribute, which we didn’t set for name
, defaults to false
. So Fido’s name will stick.
Additionally, the age
property now has a setter and a getter. Whenever we set a value for age
, it will automatically be converted to a Number
or throw an error.
Awesome, but we’re not done yet! Let’s add some methods to our Dog
class:
Object.defineProperties(Dog.prototype, { bark: { value: function () { console.log("Bark!"); } } }); |
Dog
now has a basic bark
method, but notice the omission of the enumerable
attribute. This attribute is false
by default. If a user of this class decides to iterate over a Dog
object for some reason, the bark
method will be omitted from the iteration — the user won’t need to test whether the current property is a function. Nice!
(You can always use the Object.getOwnPropertyNames
method to see all properties on the prototype, enumerable or not.)
Adding Inheritance
At this point, we have a fully working Dog
class. But there are many dog breeds. Let’s start subclassing!
In this section, we’ll be using a heavily modified inheritance script that CoffeeScript generates to “subclass” objects. Review the extend script, then come back here to create some subclasses.
Note: Most of the complexity of this post lies in the extend script. It’s thoroughly commented to help you understand how it works.
Let’s create a Bulldog
class:
__extends(Bulldog, Dog); function Bulldog() { Dog.super.constructor.apply(this, arguments); } Object.defineProperties(Bulldog.prototype, { bark: { value: function () { console.log("Wooooof"); } }, beLazy: { value: function () { console.log("Zzzzzzz."); } } }); |
Pretty simple. We just override Dog
‘s bark method. Neither method has properties set other than value
. That keeps them nice and safe: these properties cannot be overridden or deleted.
Going Final
JavaScript doesn’t have classes, so we simulate them. JavaScript doesn’t have the notion of final
for a “class” either, but we can approximate it. Object.freeze
allows us to stop any sort of modification of an object.
Let’s make sure our Bulldog class isn’t extensible. The extends method will throw an error if any parent.prototype
returns true
for Object.isFrozen
.
All we have to do is call Object.freeze(Bulldog.prototype)
. If we then try to extend this class, we’ll get a helpful error:
TypeError: Extending this class is forbidden |
Strict Mode
ECMAScript’s strict mode is not required to use these object property features, but strict mode is very helpful for debugging purposes. In strict mode, if you try to change the value of a property that isn’t writable or try to delete a property that isn’t configurable, a clear error will be thrown.
Demo
Let’s put it all together. This code should run in all modern browsers and in a Node.js environment.
Postscripts
Sealing Objects
Let’s say we want a subclass to be able to override existing methods, but not add more methods to the object. Object.seal
prevents new methods from being added to the prototype and also prevents them from being deleted. All writable methods remain writable (and all unwritable remain unwritable).
Object.seal(Bulldog.prototype) Bulldog.prototype.foo = function () {}; // TypeError: Can't add property foo, object is not extensible |
Constants
On a related note, Object.freeze
allows us to make real (sort of) constants!
var CONST: { PI: Math.PI, FOO: "bar" }; Object.freeze(CONST); |
Conclusion
Object.defineProperty
helps you make your code more rigorous using native JavaScript. You can protect objects and properties without wasting time on custom code or extra syntax. JavaScript feels the same as you use your objects, but with the delicious sugar of set
, get
, writable
, configurable
, and enumerable
.
Thanks to ECMAScript, we can add some strictness to a pretty loose language. You may not find all of these techniques to be realistic for a project, but you can abstract some smaller ideas from them to make your code safer and stronger.