ECMAScript version 5
is the latest, official, approved standard version of the
ECMAScript programming language. If you've never heard of
ECMAScript, well, don't feel too bad as it is better known, used, loved
(and hated) by its other name - JavaScript. As far as names go
JavaScript is probably not a whole lot better than ECMAScript either -
as
Douglas Crockford so eloquently (half joking actually) put it in a
recent session
that he did during Microsoft's MIX
event:
"Maybe it's time to change the name of it. Because if it isn't
Java and if it isn't script then the name is completely wrong!"
Douglas was actually responding to a
comment from
Allen Wirfs-Brock that JavaScript really isn't a "scripting" language if
you think of scripting languages as being programming languages that are
somewhat limited in capabilities or are designed to be used with very
specific use cases in mind. JavaScript is a full-fledged general purpose
programming language that is
turing complete which has been used with great success in contexts other than the browser
with the node.js
project perhaps being the most renowned. It kind of goes to show how things might
end up being given names that have little to do with the thing that is being named!
So then, ECMAScript version 5 (referred to as
ES5 henceforth) brings a nifty set of features designed,
among other things, to introduce an element of discipline to JavaScript
development. As far as being a dynamic language is concerned, JavaScript is
about as dynamic as a language could probably get! While that dynamism grants
considerable expressive power, it also makes it rather trivial to write
incorrect code. Consider the following snippet for instance (Note: I
have used the JavaScript Eval Console for
testing all of the code given below; so when you see calls to functions
such as print and sprintf please understand that
those are functions provided by the console; please read that post to
see what those functions do if it isn't clear from the context):
var data = [
{
month: 1,
revenue: 102010,
expense: 95000
},
{
month: 2,
revenue: 143232,
expense: 98000
},
{
month: 3,
revenue: 323212,
expense: 195000
}
];
function Results() {
this.totalRevenue = 0;
this.totalExpense = 0;
this.netProfit = 0;
}
var r = new Results();
//
// iterate through the data set and compute total
// revenue and expense
//
data.forEach(function(d) {
r.totalrevenue += d.revenue;
r.totalExpense += d.expense;
});
//
// net profit is revenue - expense
//
r.netProfit = r.totalRevenue - r.totalExpense;
print(r.netProfit);
Here's the output we get.
-388000
Clearly, something is not right here because
when you look at the data the revenue exceeds expenses for every single
month - so we really shouldn't be ending up with a net loss. Let's take a
closer look at the code that's iterating through the array and computing
total revenue and expense. Pay particular attention to the part highlighted
in bold:
data.forEach(function(d) {
r.totalrevenue += d.revenue;
r.totalExpense += d.expense;
});
As you can see, there's a typo there. The 'r'
in totalrevenue should have been upper case. Instead of
flagging that as an error the JavaScript engine just went right ahead and created a new property on the
Results instance with a lower case 'r'. This of course messed
up the net profit computation later on. Wouldn't it be nice if we could
somehow prevent the arbitrary creation of new members on objects? It is
precisely for situations such as this that the ES5 spec includes a slew of
new methods (well, three of them at least) on the Object type
that allow you to exercise greater control on the extensibility aspects of
your objects. Let's review what they are and what they do.
Object.preventExtensions
Object.preventExtensions does exactly what it sounds like it
would do - it prevents arbitrary properties from being tacked on to the
object in question. Here's an example:
var o = {};
o.newFangledProperty1 = "zoo";
print(typeof(o.newFangledProperty1));
Object.preventExtensions(o);
o.newFangledProperty2 = "moo";
print(typeof(o.newFangledProperty2));
And here's the output we get:
string
undefined
As is evident from the output, the attempt to create
newFangledProperty2 was not successful. The JavaScript engine
silently ignored the creation of the new property and it did this because
we marked the object as permanently non-extensible by calling
Object.preventExtensions on it. You can verify whether an
object is extensible or not by calling Object.isExtensible
on it which would return a boolean indicating true if it is extensible and
false otherwise.
print(Object.isExtensible(o));
Output:
false
We can easily use this method on our Results object
above to prevent the creation of arbitrary properties on it like so:
var r = new Results();
Object.preventExtensions(r);
Now the line with the typo will end up being a no-op statement having no
effect on the object. The result will still be wrong of course since the
totalRevenue property will continue to have the value zero.
So what's the point of using preventExtensions you might wonder.
In scenarios such as this, using Object.preventExtensions makes
the most sense when used in tandem with ES5's support for
"strict mode". ES5's "strict mode" is a new feature that
causes the JavaScript engine to interpret the relevant section of code (i.e.
the part that you have marked as being strict) with a stricter set of rules
than what is allowed in ES3. One of the consequences of switching the engine
into strict mode is that it'll throw more errors in situations where it
would otherwise have simply silently failed/ignored the error.
Assigning an arbitrary property to an object that has been marked as not
being extensible via preventExtensions for instance causes an
exception to be thrown. Here's the updated profit computation snippet using
strict mode and preventExtensions. Take note of the lines
highlighted in bold.
"use strict"; // make engine switch into strict mode
var data = [
{
month: 1,
revenue: 102010,
expense: 95000
},
{
month: 2,
revenue: 143232,
expense: 98000
},
{
month: 3,
revenue: 323212,
expense: 195000
}
];
function Results() {
this.totalRevenue = 0;
this.totalExpense = 0;
this.netProfit = 0;
}
var r = new Results();
Object.preventExtensions(r); // disallow arbitrary extensibility
//
// iterate through the data set and compute total
// revenue and expense
//
data.forEach(function(d) {
r.totalrevenue += d.revenue;
r.totalExpense += d.expense;
});
//
// net profit is revenue - expense
//
r.netProfit = r.totalRevenue - r.totalExpense;
print(r.netProfit);
And here's the output we get on running the script in a browser that can
understand strict modes (IE10 Platform Preview 1 in this case; but recent versions of Chrome,
FireFox, Safari and Opera should be able to handle it as well).
Cannot create property for a non-extensible object
As you can tell, an exception was thrown upon encountering the line where
an attempt was made to create a new property on the object r.
Object.seal
Object.seal is a superset of Object.preventExtensions
in functionality in that while it prevents tacking on of arbitrary properties
to objects it also prevents altering the attributes of the properties that
already exist on the object. Also, it disallows deleting properties. First we'll define a
little helper function that will allow us to enumerate and print all the
"own" properties (i.e. properties defined directly on the object instead
of its prototype) that are present on a given object which we'll use to examine the
object's property state as we go along.
//
// prints the property descriptors for all the "own"
// properties on the object "o"
//
function printDesc(o) {
Object.getOwnPropertyNames(o).forEach(function(p) {
print(sprintf("%s: %s", p,
JSON.stringify(Object.getOwnPropertyDescriptor(o, p),
null, " ")));
});
}
With this function in hand let's go ahead and define a simple object with
a single property called "name" using Object.defineProperty and
print it to the console via printDesc.
//
// define object with one property
//
var person = {};
Object.defineProperty(person, "name", {
value: "no name",
writable: true,
enumerable: true,
configurable: true
});
print("Default state:");
printDesc(person);
Here's the output we get on running this:
Default state:
name: {
"value": "no name",
"writable": true,
"enumerable": true,
"configurable": true
}
Let's next examine the effect calling Object.preventExtensions
has on the attribute state:
//
// prevent extensions
//
Object.preventExtensions(person);
print("preventExtensions:");
printDesc(person);
Output:
preventExtensions:
name: {
"value": "no name",
"writable": true,
"enumerable": true,
"configurable": true
}
As you can tell nothing at all has
changed on the attributes of the property name. This makes
sense as the only effect that preventExtensions has on the
object is to make it non-extensible. Let's next attempt to edit the
property descriptor for the property name and see what
happens:
//
// manually change property descriptor
//
Object.defineProperty(person, "name", {
value: "no name",
writable: false, // make property read only
enumerable: false, // disallow enumeration
configurable: true // allow editing prop descriptor
});
print("Manual config reset:");
printDesc(person);
Output:
Manual config reset:
name: {
"value": "no name",
"writable": false,
"enumerable": false,
"configurable": true
}
So far everything seems to be working as
expected. Let's continue the experiment and seal the object
and see what happens.
//
// seal
//
Object.seal(person);
print("seal:");
printDesc(person);
Output:
seal:
name: {
"value": "no name",
"writable": false,
"enumerable": false,
"configurable": false <-- this changed!
}
Sealing the object seems to have had the
effect of setting the configurable attribute on the property
descriptor to false. So what happens when we attempt to
delete a property or tack on a new property?
//
// delete property "name"
// add a new property
//
delete person.name;
person.age = 10;
print("Delete/add properties:");
printDesc(person);
Output:
Delete/add properties:
name: {
"value": "no name",
"writable": false,
"enumerable": false,
"configurable": false
}
As you can tell, nothing happened! Calling
Object.seal on person causes the JavaScript
engine to ignore "delete" calls and attempts to introduce new properties. In
strict mode both of these attempts would have caused an error to be thrown.
Finally, let's try and redefine the property descriptor on the
sealed instance of person and see what happens.
//
// manually change property descriptor again
//
Object.defineProperty(person, "name", {
value: "no name",
writable: true,
enumerable: true,
configurble: true
});
print("Second manual config reset:");
printDesc(person);
Output:
Cannot redefine non-configurable property 'name'
The JavaScript engine throws an error indicating
that this operation is not allowed on a sealed object. Note that this error is
thrown even when we are not running in strict mode. You can check if
an object has been sealed by calling Object.isSealed which returns
true if the object has been sealed and false otherwise.
Object.freeze
Object.freeze is a superset of both Object.seal and
Object.preventExtensions in that it subsumes the functionality of
both of those functions with the added responsibility of making all of the
existing properties on the object read-only. Here's an example:
var ShapeType = {
None: 0,
Line: 1,
Ellipse: 2,
Rectangle: 3,
RoundedRectangle: 4
};
Object.freeze(ShapeType);
ShapeType.None = 5;
print(ShapeType.None);
ShapeType.Polygon = 5;
print(typeof(ShapeType.Polygon));
Output:
0
undefined
Once an object has been frozen attempts to
assign a value to an existing property or to define a new property are
silently ignored in normal mode. In strict mode this would cause the
engine to throw an error. To determine if an object has been frozen call
Object.isFrozen.
Here's a table that summarizes the
functionalities of preventExtensions, seal and
freeze taken from MSDN:
| Function |
Object made non-extensible? |
"configurable" set to false for all properties? |
"writable" set to false for all properties? |
Object.preventExtensions |
Yes |
No |
No |
Object.seal |
Yes |
Yes |
No |
Object.freeze |
Yes |
Yes |
Yes |
In summary therefore, preventExtensions,
seal and freeze allow you to exercise very precise
control on how extensible or modifiable your objects are going to be. In combination
with "strict mode" these new enhancements to JavaScript should help developers
catch errors much earlier in the development cycle. And the fact that these
capabilities are provided through new methods on the Object type
should make it trivial to gracefully degrade when your site runs on older
browsers that do not have support for ES5 features. In short there's no
reason why you should not start using these features in your web apps from,
like, right now!