Udostępnij za pośrednictwem


ECMAScript 5 Part 1: Reusable Code

In June, we wrote about IE9’s support for ECMAScript 5 (ES5) and published a TestDrive demo to explore some of the features. With IE9 Beta available, it’s a good time to talk about how developers can use ES5 to improve their code. Many of the features in ES5 are designed to help developers working with very large code bases by making the language both more robust and easier to maintain. In this series of blog posts we’ll look specifically at how ES5 helps you write more reusable code.

For this post, we’ll focus on how ES5 makes it easier to create and clone objects and helps you ensure that your code is robust to mistakes when it is reused – even if you’re the only developer who will be using it.

The example we’ll use is a simple set of calendar objects, similar to the kinds of objects you see in a web mail program like Hotmail or Outlook Web Access.

Creating Objects: new and Object.create

There’s a saying that the best code is the code you don’t have to write. ES5’s object constructor methods cut down on the amount of repetitious code quite a bit. As an example, let’s start by creating an object representing an appointment. The simplest way to create a JavaScript object with some initialized properties is with an object literal:

 var appointment = {
    name: "unnamed appointment",
    startTime:  new Date(NaN),
    endTime:  new Date(NaN), 
    location: "unknown"
};

That object can serve as a pattern for any other appointment objects we want to create, analogous to creating a class. Prior to ES5, if we wanted to create a prototype from this object we would define a constructor function and specify the prototype to be used:

 function Appointment(){};
Appointment.prototype = appointment; 
var myAppointment = new Appointment();

(Note that the Appointment function doesn’t actually do anything in this case. It just provides the place to specify the value to use as the prototype for new objects.)

This pattern is familiar to JavaScript developers today. But with the object constructor methods in ES5, there’s a more direct way to create an object with a specific prototype:

  var myAppointment = Object.create(appointment);

Any object created by the above call to Object.create will have the appointment object as its prototype and inherit all its properties; there’s no need to create a constructor function.

 var myAppointment = Object.create(appointment);
myAppointment.name = "Dentist Appointment";
myAppointment.startTime = new Date("10/31/2010 08:00");

These assignments override the like-named inherited properties rather than actually modify the properties of the prototype object. The value of appointment.name remains “unnamed appointment”; the value of unassigned inherited properties like location remains “unknown.”

Extend and Constrain

Now that we’ve created a base class pattern and have leveraged the prototype to create another instance using Object.create, let’s look at how ES5 helps you extend the base class to create another reusable class. Let’s start by creating a class which can serve as a meeting prototype. The second argument of create is a property descriptor which enables us to create and set the value of properties. We’ll set conferenceCall to a default value, just in case we use it in a later operation without setting the value.

 var meeting = Object.create(appointment, {
    conferenceCall: {value: "In-person meeting" }
});

Alternatively, we could do this by calling the defineProperty method, which is also new to ES5.

 var meeting = Object.create(appointment);
Object.defineProperty(meeting, "conferenceCall", { value: "In-person meeting" });

You might ask how the code examples above are different than the following code:

 var meeting = Object.create(appointment);
meeting.conferenceCall = "In-person meeting"; 

The defineProperty method defaults the writable, enumerable, and configurable attributes of a new property to false. If we did not want to make the property read-only, not enumerable, and configurable then we would have used the simpler property initialization. The property definition parameters to Object.create also have the same defaults.

For a computed field, a property which is a function of other modifiable fields, we want to ensure that the property value can’t be modified directly. We can use the getter functionality in ES5 to specify the function which computes the value. (Actual function has been simplified for illustrative purposes.)

 Object.defineProperty(meeting, "duration",
    { get: function () {
        return this.endTime.getHours() - this.startTime.getHours(); 
    }}
);

We now create an instance of the meeting.

 var teamMeeting = Object.create(meeting);
teamMeeting.name = "IE Team Meeting";
teamMeeting.startTime = new Date("10/31/2010 08:00");
teamMeeting.endTime = new Date("10/31/2010 09:00");
teamMeeting.location = "Conference Room 33";

With the enumerable attribute of the properties created with defineProperty being false by default, the duration and conferenceCall properties are not enumerated when looping through the members of the object. The enumeration only contains those properties that are necessary to describe the object.

 Output:
name: IE Team Meeting
startTime: Sun Oct 31 08:00:00 PDT 2010
endTime: Sun Oct 31 09:00:00 PDT 2010
location: Conference Room 33

With the configurable attribute of the property defaulting to false, users of the class cannot write over the duration computed property.

Send in the Clones

ES5 makes it easier to implement object cloning. Object cloning is mostly a feature you see in JavaScript frameworks like jQuery or Dojo. These frameworks have to do a lot of work to make sure that the right things happen when you clone an object. With previous versions of ECMAScript, it was really easy to write a naïve clone function which only copies the members they enumerate.

 function naiveClone (obj) {
  var n={};
  for (var p in obj) {
    n[p]=obj[p];
    }; 
  return n;
}

For some implementations of object cloning, when you just want to copy the data, you might end up with too many members including those that contain behavior. For other implementations of object cloning when you want a deep copy, you might end up with too few members. With the new Object methods of ES5, writing a good clone function is much easier.

 Object.clone = function (o) {
  var n = Object.create(Object.getPrototypeOf(o));
  var props = Object.getOwnPropertyNames(o);
  var pName;
  for (var p in props) {
    pName = props[p];
    Object.defineProperty(n, pName, Object.getOwnPropertyDescriptor(o, pName));  
  };
  return n;
};

The getOwnPropertyNames function only gets those properties that are unique to this object. So, for example, it doesn’t re-clone the inherited methods that exist on all Objects like toString and valueOf. Going back to our earlier example, using getOwnPropertyNames, we can recreate a series of team meetings over the next few months:

 var quarterlyTeamMeeting = Object.clone(teamMeeting);
quarterlyTeamMeeting.startTime = new Date("12/31/2010 07:00");
quarterlyTeamMeeting.endTime = new Date("12/31/2010 09:00");

The duration property exists on this object and yet it does not show up in the enumeration. Of course, writing a robust, one-size-fits-all, clone method is not necessarily as simple as the code above; for example, you have to take into account special characteristics of some of the built-in objects such as Strings or Arrays. For this reason, the ECMAScript 5 committee left creating a cloning function to frameworks authors.

Getters and Setters

Getters and setters are among the most anticipated features in ES5. Suppose you’ve got a property which triggers an event whenever anyone sets its field. In this example, we’ll instead define the location property such that if the location is updated, we want to fire an event to allow subscribers to see if the location is available. To explore this, we’ll revisit the meeting declaration to create a new property which fires an event when set.

 (function () {
    var loc = "undetermined location";

    Object.defineProperty(meeting, "location", {
        get: function () { return loc; },
        set: function (value) {
            loc = value;             
            /*fire some event */
        }
    });
})();

In the example above, we wrapped the property declaration with a function to limit the scope of the variable loc to the property. This makes the variable serve as a backing store as opposed to an accessible field.

During IE8, while the ES5 specification was still in the making, we introduced accessor support for DOM objects. Now that the spec is ratified, we have added full support for this feature.

Moving the web forward with ES5

These are some of the ways you can use ES5 to create more robust code that can be more easily reused. Remember, to leverage these features, the browser must be in IE9 Standards Document Mode which is the default in IE9 for a standard document type.

As the web evolves, JavaScript continues to be used for larger applications. As usage patterns emerge, they are incorporated into browser implementations and web standards. Some of the principles the standards working group applied to ES5 to support these demands are:

  • No New Syntax. When there’s a syntax error, the page stops loading. Many of the constructs explored in this blog post which are new to ES5 are designed to be simple methods on the Object constructor, thereby leveraging method calling syntax.
  • Minimize construct collision. Introducing new members on Object could conflict with frameworks which extend Object’s prototype. Note that the functions used above are all extensions on the Object constructor (with a capitol “O”) which is a less commonly used design pattern.
  • Appropriate layering of abstractions. Meta-level methods that manipulate the structure and definition of objects are clearly separated from the “business logic” methods.

ES5 delivers on these principles and represents increased productivity for frameworks authors. We think these extensions, and others we’ll discuss in future posts, will enable developers to create richer standards-based experiences on the web. We’re excited to see how developers will use the features of ES5 with IE9; we’d like to hear how you plan to put these features to use. We’ll continue to explore the additions to ES5 in upcoming posts, stay tuned.

Amanda Silver
Program Manager
JavaScript

Comments

  • Anonymous
    October 27, 2010
    Why doesn't IE9 treat CSS values as CSSPrimitiveValues as specified in www.w3.org/.../ecma-script-binding.html  Seems like the first step in becoming DOM compatible.

  • Anonymous
    October 27, 2010
    This was considered when we were planning the CSS object model support in IE9, but ultimately rejected based on the recommendation of the CSS WG: lists.w3.org/.../0347.html. The working group took an action to update the DOM L2 Style spec with this info, but it hasn't been done yet. Sorry for the confusion.

  • Anonymous
    October 27, 2010
    Your "new Date("12/31/2010 07:00");" is unambiguous, but what if you had written "new Date("12/03/2010 07:00");"?  Would that be interpreted the same way regardless of browser language/regional settings, or not?  If not then it might be more consistent to avoid it in the case of 12/31/2010 as well.

  • Anonymous
    October 27, 2010
    <blockquote>If we did not want to make the property read-only, not enumerable, and configurable then we would have used the simpler property initialization</blockquote> Or to change default states of needed property attributes to <em>true</em>. <blockquote>With the configurable attribute of the property defaulting to false, users of the class cannot write over the duration computed property.</blockquote> This sentence a bit ambiguous -- one may think that you talking about the absence of the <code>duration</code>'s <em>setter</em> (about writing over the duration's value, and not redefining the getter). However, a value may be changed even if enumerable == false (of course if writable is true in case of data-property or there is a setter in case of accessor-property). <blockquote>for (var p in props) {</blockquote> Not so good practice for enumerating arrays, even if now we may control enumerable attribute (again, because of the same <em>enumerable</em> properties placed in the <code>Array.prototype</code>). If the article about ES5, it's better to mention <em>forEach</em> method for that. P.S.: In addition: <a href="dmitrysoshnikov.com/.../">ECMA-262-5 in detail. Chapter 1. Properties and Property Descriptors.</a> Dmitry.

  • Anonymous
    October 27, 2010
    Standard tags formatting -- <em>, <blockquote>, <code>, <pre >, etc would be good for a / any blog, by the way ;)

  • Anonymous
    October 27, 2010
    The comment has been removed

  • Anonymous
    October 27, 2010
    "Minimize construct collision. Introducing new members on Object could conflict with frameworks which extend Object’s prototype." and the framework already risks collision with new / future native features of javascript, e.g. what if a framework used to extent Object by a property named 'defineProperty'... or worse, what if a future native addition conflicts with a widely used framework's extension? do i have to hear "we couldnt name the new native property as we wanted, because n% of all websites would break" then? IMHO native constructors or prototypes should never be extended, and who uses this anti pattern is acting irresponsible.. g

  • Anonymous
    October 27, 2010
    Please address IE8 and above's failure to set memory usage based on available physical ram on the machine.   Ie8 does not consider the amount of physical ram and will extremely slow down a machine with 1gb or less of ram.   Additionally, please address more granular filtering in Ie8.  This is important for connections that have good bandwidth but very slow connect times.   This is a large problem when using sites needing 100+ separate objects to be downloaded for a single page when 33% or more of them are 1x1 pixel web bugs or url requests to web metrics sites.  A page info report that lists all of the url's accessed for a page as well as a 'block from this url' ability would help greatly.  Smartscreen does not help given it filters at the www.domain.com level and does not let you block *.domain.com.  A nice add on feature would be a block all from *.domain.com.

  • Anonymous
    October 28, 2010
    @Amanda, @Travis Thanks for the great article. Be aware of this issue that I filed back on 25 Sept 2010 about using 'Object.defineProperty()' with the global/self/window object in IE9. There is a very useful use-case stated in the feedback item: connect.microsoft.com/.../object-defineproperty-cannot-be-used-with-the-self-window-i-e-global-object Cheers,

  • Bill
  • Anonymous
    October 28, 2010
    @Travis Then is there a simple way to change a CSS color (like the background color of a div) in ecmascript without parsing the rgb(255,255,255)?

  • Anonymous
    October 28, 2010
    Hey IE team, you guys might want to make a post about 'edge mode' for document compatibility in a blog post. msdn.microsoft.com/.../cc288325%28VS.85%29.aspx

  • Anonymous
    October 30, 2010
    @jab: Microsoft has already said "Don't use edge mode" a million times since it was created. What else do you want?

  • Anonymous
    November 02, 2010
    How about protected javascript code? Like compiled+obfuscated... and how about allowing the code to be written in C# syntax