分享

更多

   

Two Methods of Creating JavaScript Objects: Prototype Inheritance and the Xerox Method

2008-09-29  moonboat

Two Methods of Creating JavaScript Objects: Prototype Inheritance and the Xerox Method

In this blog entry, I examine different methods of building new JavaScript objects from existing JavaScript objects. For lack of better names, I’m calling the first method the Prototype Inheritance method and the second method the Xerox method. The goal of this blog entry is discuss the relative strengths and weaknesses of the two methods. At the very end of this entry, I briefly examine the approach taken by the ASP.NET AJAX framework.

Prototype Inheritance

Prototype Inheritance is the standard method of building JavaScript objects described in most JavaScript books and the Mozilla JavaScript documentation. Here’s how Prototype Inheritance works. Every JavaScript object has an associated prototype object. If you attempt to read a property from a JavaScript object, and the object does not have the property, then an attempt is made to read the property from the object’s prototype.

Since the prototype object for an object can also have a prototype object, every object has something called a prototype chain. Whenever you attempt to read a property, each link in the chain is checked for the property. If the property is not found in the object’s prototype chain, then the property value undefined is returned.

Prototype Inheritance is illustrated by Figure 1.

clip_image002

Figure 1 – Prototype Inheritance

In Figure 1, there is an object with two prototype objects in its prototype chain (a prototype chain is terminated with a Null value). The object itself (located at the bottom of the figure) has two properties named PropA and PropB. This object’s first prototype object, Prototype 1, has one property named PropC. Finally, Prototype 1 also has a prototype object named Prototype 2. Prototype 2 has two properties named PropD and PropA.

When reading the value of a property from the object, the entire prototype chain is used to retrieve the value of the property. For example, if you attempt to read the value of PropC, the value of the property is retrieved from Prototype 1.

Notice that a prototype chain can contain duplicate properties. In Figure 1, PropA is included twice: as a property of the original object and as a property of the Prototype 2 object. Prototype chains use a first match algorithm. Therefore, when reading the value of PropA, the Prototype 2 object is ignored (lower objects in a chain can mask properties of higher objects in the chain).

Here’s what is a little confusing about the way that Prototype Inheritance works. You don’t specify the prototype object for an object itself. Instead, you specify the prototype object with an object’s constructor function. This point is clearer with a sample of Prototype Inheritance in hand. Listing 1 contains two objects: BaseControl and TreeViewControl. These classes represent visual controls (widgets) that might be used in a web page. The TreeViewControl object inherits from the BaseControl object.

Listing 1 – PrototypeInheritance.js

   1:  <script type="text/javascript">
   2:   
   3:  function BaseControl()
   4:  {
   5:      this.PropA = "BaseControl PropA";
   6:      this.PropB = "BaseControl PropB";    
   7:  }
   8:   
   9:  function TreeViewControl()
  10:  {
  11:      this.PropA = "TreeViewControl PropA";
  12:  }
  13:   
  14:  // Assign prototype
  15:  TreeViewControl.prototype = new BaseControl();
  16:   
  17:  // Create object
  18:  var treeView = new TreeViewControl();
  19:   
  20:  alert(treeView.PropA); // displays "TreeViewControl PropA"
  21:  alert(treeView.PropB); // displays "BaseControl PropB"
  22:  alert(treeView.PropZ); // displays undefined
  23:      
  24:  </script>

In Listing 1, two object constructor functions named BaseControl and TreeViewControl are created. Next, an instance of BaseControl is created and assigned to the TreeViewControl function’s prototype property. Assigning an object to the Prototype property creates the prototype relationship between the BaseControl and TreeViewControl functions/objects.

When an instance of the TreeViewControl is created, the following sequence of events happen (for details, see section 13.2.2 of the ECMAScript 3.0 specification at http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf):

1. A new object is created by the new operator

2. The value of the constructor function’s Prototype property is copied to the new object’s internal [[Prototype]] property

3. The new object is passed to the constructor function (within the constructor function, the keyword this refers to the new object)

When these steps are completed, you have a new object named treeView that has one property named PropA. In addition, the treeView object has an internal [[Prototype]] property that points to an instance of the BaseControl object. When you read a property from an object, the internal [[Prototype]] property is read to find a particular object’s prototype. Therefore, when you read treeView.PropB, the value “BaseControl PropB” is returned from the treeView prototype chain.

 

The Xerox Method

The other common approach to building JavaScript objects from other objects I call the Xerox method. When you use the Xerox method, you copy a set of properties from an existing object into a new object.

This is the approach taken by the very popular Prototype framework (download from http://www.prototypejs.org/download). The Prototype framework includes an Object.extend() method that looks like this:

Listing 3 – Prototype.js Extend Method

   1:  Object.extend = function(destination, source) {
   2:    for (var property in source)
   3:      destination[property] = source[property];
   4:    return destination;
   5:  };

This method does something really simple: it copies all of the properties from a source object into a destination object.

Here’s how you can use the Object.extend() method with the BaseControl and TreeViewControl objects:

Listing 4 – Extend.js

   1:  <script type="text/javascript">
   2:   
   3:  var BaseControl =
   4:  {
   5:      PropA: "BaseControl PropA",
   6:      PropB: "BaseControl PropB"    
   7:  }
   8:   
   9:  var TreeViewControl =
  10:  {
  11:      PropA: "TreeViewControl PropA"
  12:  }
  13:   
  14:  // Extend TreeViewControl with BaseControl
  15:  Object.extend(TreeViewControl, BaseControl);
  16:   
  17:  // Create object
  18:  var treeView = Object.extend( {}, TreeViewControl);
  19:   
  20:  alert(treeView.PropA); // displays "BaseControl PropA"
  21:  alert(treeView.PropB); // displays "BaseControl PropB"
  22:  alert(treeView.PropZ); // displays undefined
  23:      
  24:  </script>

In Listing 4, the Object.extend() method is used to copy all of the properties of the BaseControl object into the TreeViewControl object. Next, the extend method is called a second time to create a new copy of the TreeView object.

There are a couple of things that you must be careful about when using Object.extend(). First, notice that treeView.PropA now returns the value “BaseControl PropA” instead of TreeViewControl.PropA unlike previous listings. When we copied the properties from BaseControl into TreeViewControl, we overwrote existing TreeViewControl properties. If this behavior bothers you, you can modify the Object.extend() method to look like this:

Listing 5 – Extend2.js

   1:  Object.extend2 = function(destination, source) {
   2:    for (var property in source)
   3:      if (destination[property] === undefined)
   4:          destination[property] = source[property];
   5:    return destination;
   6:  };

This modified version of Object.extend() won’t overwrite existing properties in the “derived” object.

You also should be warned that the Object.extend() method creates a shallow clone. When you create a new object with Object.extend(), value types are copied but not reference types. This behavior is clarified by the following code:

Listing 6 – ShallowClone.js

   1:  <script type="text/javascript">
   2:   
   3:  var BaseControl =
   4:  {
   5:      MethodA: function() { alert("Hello"); },
   6:      PropB: []
   7:  }
   8:   
   9:  var TreeViewControl =
  10:  {
  11:      PropA: "TreeViewControl PropA"
  12:  }
  13:   
  14:  // Extend TreeViewControl with BaseControl
  15:  Object.extend(TreeViewControl, BaseControl);
  16:   
  17:  // Create objects
  18:  var treeView1 = Object.extend( {}, TreeViewControl);
  19:  var treeView2 = Object.extend( {}, TreeViewControl);
  20:   
  21:  treeView1.MethodA = function() { alert("Bye"); };    
  22:  treeView1.PropB.push( "some item" );    
  23:      
  24:  alert( treeView2.PropB.length ); // displays 1    
  25:  treeView2.MethodA(); // displays "Hello"
  26:      
  27:  </script>

In Listing 6, BaseControl has two properties that represent Object types rather than value types. For example, PropB represents an Array. Since an Array is an Object type, every object extended with the BaseControl will share the same reference to the very same Array. Adding an item to treeView1.PropB also adds the item to treeView2.PropB.

The fact that Object.extend creates a shallow rather than a deep clone is both a good and a bad thing. On the one hand, from the point of view of memory consumption, it is a good thing. On the other hand, if you don’t know what you are doing, then you might shoot yourself in the foot.

There is a variation on the Xerox method included in the Mozilla JavaScript documentation (see More Flexible Constructors at http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Guide:Details_of_the_Object_Model). Instead of using the Object.extend() method, you call the base object from the derived object’s constructor. Here’s how you would use this variation with the BaseControl and TreeViewControl objects:

Listing 7 – BaseCall.js

   1:  <script type="text/javascript">
   2:   
   3:  function BaseControl()
   4:  {
   5:      this.PropA = "BaseControl PropA";
   6:      this.PropB = "BaseControl PropB";    
   7:  }
   8:   
   9:  function TreeViewControl()
  10:  {
  11:      // call base constructor
  12:      this.base = BaseControl;
  13:      this.base();
  14:      
  15:      this.PropA = "TreeViewControl PropA";
  16:  }
  17:   
  18:  // Create object
  19:  var treeView = new TreeViewControl();
  20:   
  21:  alert(treeView.PropA); // displays "TreeViewControl PropA"
  22:  alert(treeView.PropB); // displays "BaseControl PropB"
  23:  alert(treeView.PropZ); // displays undefined
  24:      
  25:  </script>

Of all of the methods of constructing objects described in this blog entry, I find this method the most intuitive and easy to use.

Notice that the relationship between BaseControl and TreeViewControl is specified within the TreeViewControl constructor function with the following two lines of code:

// call base constructor

this.base = BaseControl;

this.base();

The BaseControl function is assigned to a property of the TreeViewControl constructor function. Next, the base constructor function is called. That’s it.

It is important to understand that base is not a JavaScript keyword. You can use any property name here and the code would work the same way. For example, this code works just fine:

// call base constructor

this.steve = BaseControl;

this.steve();

The point of this code is to call the BaseControl constructor with the right value for the this keyword. When the BaseControl() function is called, you want the keyword this to refer to the current object being built by the TreeViewControl constructor function.

If you want to make the code in Listing 7 even shorter, then you can use the JavaScript apply() method. The apply() method enables you to modify the value of the this keyword when calling a function. This approach is illustrated in Listing 8:

Listing 8 – Apply.js

   1:  <script type="text/javascript">
   2:   
   3:  function BaseControl()
   4:  {
   5:      this.PropA = "BaseControl PropA";
   6:      this.PropB = "BaseControl PropB";    
   7:  }
   8:   
   9:  function TreeViewControl()
  10:  {
  11:      // call base constructor
  12:      BaseControl.apply(this);
  13:    
  14:      this.PropA = "TreeViewControl PropA";
  15:  }
  16:   
  17:  // Create object
  18:  var treeView = new TreeViewControl();
  19:   
  20:  alert(treeView.PropA); // displays "TreeViewControl PropA"
  21:  alert(treeView.PropB); // displays "BaseControl PropB"
  22:  alert(treeView.PropZ); // displays undefined
  23:      
  24:  </script>

The method used in this last code sample is the shortest way of building objects from existing objects that I know. When BaseControl.apply(this) is called, the BaseControl function is called with the current object as the value for the BaseControl function’s this keyword. Therefore, the BaseControl function ends up decorating the current object with the PropA and PropB properties.

Prototype Inheritance versus the Xerox Method

In this blog entry, I’ve compared two general methods for building new JavaScript objects from existing JavaScript objects: Prototype Inheritance and the Xerox method. So which method is better?

The chief advantage of Prototype Inheritance is saving memory. When you use Prototype Inheritance, you are not creating copies of object properties. Instead, Prototype Inheritance enables you to specify relationships between distinct objects with distinct sets of properties. If you are working with thousands of objects, then saving memory might really matter to you.

The advantage of the Xerox method is performance. Following a prototype chain to resolve a property takes time. When using the Xerox method, on the other hand, every property of an object is a local property. An object’s prototype chain is never used.

One other advantage of the Xerox method is flexibility. The Xerox method supports multiple inheritance. If you want to derive a TreeViewControl from both a BaseControl and a BaseDataBoundControl, then you can do this with the Xerox method but not with the Prototype Inheritance method.

Building Objects with ASP.NET AJAX

When building new objects out of existing objects using ASP.NET AJAX, you use some of the features of Prototype Inheritance and some of the features of the Xerox method. Let‘s look at a code sample.

Listing 2 – ASPNETAjaxInheritance.aspx

   1:  <script type="text/javascript">
   2:   
   3:   
   4:  function BaseControl()
   5:  {
   6:  }
   7:  BaseControl.prototype.PropA = "BaseControl PropA";
   8:  BaseControl.prototype.PropB = "BaseControl PropB";
   9:   
  10:   
  11:  // Register BaseControl class
  12:  BaseControl.registerClass("BaseControl");
  13:   
  14:  function TreeViewControl()
  15:  {
  16:      TreeViewControl.initializeBase(this);
  17:  }
  18:  TreeViewControl.prototype.PropA = "TreeViewControl PropA";
  19:   
  20:  // Register TreeViewControl class
  21:  TreeViewControl.registerClass("TreeViewControl", BaseControl);
  22:   
  23:  // Create object
  24:  var treeView = new TreeViewControl();
  25:   
  26:  alert(treeView.PropA); // displays "TreeViewControl PropA"
  27:  alert(treeView.PropB); // displays "BaseControl PropB"
  28:  alert(treeView.PropZ); // displays undefined
  29:    
  30:  </script>

In Listing 2, a method named registerClass() is called for both the BaseControl and the TreeViewControl functions. The registerClass() method accepts the following parameters:

· typeName – The class being registered

· baseType – (optional) the base class for the class being registered

· interfaceTypes – (optional) an array of interfaces implemented by the class being registered

First, the registerClass() method is called for the BaseControl constructor function. Next, the method is called for the TreeViewControl constructor function. When the method is called for the TreeViewContructor function, a second parameter is passed that represents the TreeViewControl’s base class.

There is one other important line of code that you need to notice in Listing 2. Notice that TreeViewControl.initializeBase() is called in the TreeViewControl constructor function. If you don’t call this method, then the prototype relationship between BaseControl and TreeViewControl will never be created.

One last thing that you should notice about Listing 2. Notice that the object properties are not defined within the constructor functions. For example, PropA is not defined in the BaseControl constructor function. Instead, PropA is defined with the BaseControl.prototype object. This is a requirement when using prototype inheritance with ASP.NET AJAX. If you define the properties within an object’s constructor function, then the prototype chain never comes into play.

As I mentioned before, the ASP.NET AJAX Framework uses both Prototype Inheritance and Xerox Inheritance. On the one hand, ASP.NET AJAX uses Prototype Inheritance. Every instance of a particular class (such as the TreeViewControl class) shares the same prototype. The prototype and not the object instances have the PropA and PropB method. Memory is saved since these properties do not need to be copied to each and every instance of a TreeViewControl.

On the other hand, ASP.NET AJAX uses Xerox Inheritance. When the ASP.NET AJAX initializeBase() method is called, the prototype chain is collapsed. All of the properties of each prototype object higher in the chain are copied lower in the chain. Presumably, the ASP.NET AJAX framework collapses the prototype chain to improve performance so that the chain does not need to be climbed when reading the value of object properties.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。如发现有害或侵权内容,请点击这里 或 拨打24小时举报电话:4000070609 与我们联系。

    来自: moonboat > 《Ajax》

    猜你喜欢

    0条评论

    发表

    类似文章
    喜欢该文的人也喜欢 更多