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 ClassIn 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 FinalJavaScript 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:
Strict ModeTypeError: Extending this class is forbidden
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.
TypeError: Cannot assign to read only property 'name' of #<Bulldog> TypeError: Cannot delete property 'name' of #<Bulldog>Demo
Let's put it all together. This code should run in all modern browsers and in a Node.js environment.
PostscriptsSealing 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.
Â
Trevor Landau is a software developer on the Mobile Web Tech team at The New York Times. He is passionate about open source, clean code and back-end performance. His real love is JavaScript, but he is interested in all facets of web technologies. Twitter: @trevor_landau
No comments:
Post a Comment