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.defineProperty, and also provide a setter and getter for age while weâre at it:
function Dog(name, age) { Object.defineProperty(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.getOwnPopertyNames 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