The goal of this blog entry is to explain how you can create new JavaScript objects when building a Windows Web Application. You learn how to use the WinJS.Class.define(), WinJS.Class.derive(), and WinJS.Class.mix() methods. All three methods are standard methods of the WinJS library.
Before we discuss these three methods, we first do a quick review of creating JavaScript objects in both ECMAScript 3 and ECMAScript 5. Because the WinJS library builds on top of these ECMAScript methods, understanding how these methods work is important for understanding how the new WinJS methods work.
Creating Objects with ECMAScript 3
Until recently, the ECMAScript 3 standard described the version of JavaScript which was supported by most browsers. The ECMAScript 3 standard was published in December 1999. This version of JavaScript is supported by Internet Explorer 5.5 and higher and every modern browser.
Creating Simple Objects
Fundamentally, objects in JavaScript are property bags. There are two ways of creating a simple object:
// Using an Object() constructor
var myObject1 = new Object();
// Using an object literal
var myObject2 = {};
The code above shows two ways of creating a JavaScript object. The first way is to use the Object() constructor and the second way is to use an object literal. Most people prefer the second way of creating a simple object.
An advantage of using an object literal to create a JavaScript object is that you can both define an object and provide values for its properties in a single line of code like this:
// Create a Robot object
var myRobot = {
name: "Robby the Robot",
sayName: function () {
return "My name is " + this.name;
}
};
// Call sayName method
console.log( myRobot.sayName() );
This code creates a new object which represents a robot. The robot has a single property and a single method. The property represents the name of the robot and the method returns the robot’s name. Notice that a JavaScript method is simply a property which represents a function.
When the myRobot.sayName() method is called, the message “My name is Robby the Robot” is written to the browser console.
Creating Constructor Functions
In most modern computer languages — C#, Java, Visual Basic – when you create a class, you can create a constructor method. The purpose of a constructor method is to execute initialization code when an object is first created.
JavaScript also supports constructors. You create a constructor by creating a constructor function like this:
// Define constructor function
function Robot() {
this.name = "Robby the Robot";
this.sayName = function () {
return "My name is " + this.name;
};
}
// Create a robot and call sayName
var myRobot1 = new Robot();
console.log(myRobot1.sayName());
By convention, constructor functions are Pascal-cased. Normal functions are camel-cased.
You can think of a constructor function as a factory method. Within the constructor function, the this keyword refers to the new object being created. You can add whatever properties and methods to this that you want within the constructor function.
Prototype Inheritance
Languages such as C# and Java make a distinction between classes and objects. A class defines the methods and properties of every object created from the class. Metaphorically speaking, a class is a mold and an object is the result of applying the mold.
JavaScript proudly avoids the concept of a class. In JavaScript, there are no classes, there are only objects. Instead of using class inheritance, JavaScript uses prototype inheritance.
Here’s how it works. Every JavaScript object has a prototype property. The prototype property represents the prototype of the object in the prototype chain. For example, the following figure illustrates a prototype chain of Thing, Robot, Roomba:
According to the diagram above, the prototype for a Roomba is a Robot and the prototype for a Robot is a Thing. When you call the Roomba vacuum() method, then this method is simply called on the Roomba object and the prototype chain is not used.
Imagine, however, that you call the Roomba doSomething() method. The Roomba object does not have this method. This is when the prototype chain comes into play. The prototype chain is ascended until an object is found with a doSomething() method. The first doSomething() method found in the prototype chain is called.
In the diagram above, both the Robot and the Thing objects have a doSomething() method. The Robot doSomething() method is called because this object is closest in the prototype chain. The Thing doSomethng() method is hidden by the Robot doSomething() method in the prototype chain.
The prototype chain is used when reading a property or invoking a method. The prototype chain is not used when setting a property. Setting a property of an object matters only the current object and not any objects in the prototype chain.
Here’s how the diagram above can be implemented in code when using ECMAScript 3 (there is a better way with ECMAScript 5):
/*
Define Thing
*/
function Thing() {
this.name = "Thing";
this.doSomething = function () {
return "Doing something...";
};
this.makeNoise = function () {
return "Crunch, boom, bop!";
};
}
/*
Define Robot
*/
function Robot() {
this.name = "Robot";
this.doSomething = function () {
return "Taking over world...";
};
}
Robot.prototype = new Thing();
/*
Define Roomba
*/
function Roomba() {
this.name = "Roomba";
this.vacuum = function () {
return "Vacuuming...";
};
}
Roomba.prototype = new Robot();
/*
Create a Roomba
*/
var myRoomba1 = new Roomba();
console.log(myRoomba1.doSomething()); // writes "Taking over world..."
console.log(myRoomba1.makeNoise()); // writes "Crunch, boom, bop!"
In the code above, a constructor function is created for the Thing, Robot, and Roomba objects. Next, the prototype property of the Robot constructor function is set to Thing and the prototype property of the Roomba constructor function is set to Robot.
When the myRoomba1.doSomething() method is called, the prototype chain is ascended to the Robot object and the Robot doSomething() method is invoked. When the myRoomba1.makeNoise() method is called, the Thing makeNoise() method is invoked.
Notice that the prototype of an object is not specified directly on an object. The prototype relationship is specified between constructor functions. This is indirect and unwieldy. Things improve with ECMAScript 5.
Creating Objects with ECMAScript 5
ECMAScript 5 was published in December 2009. It is supported (more or less) by all recent browsers including Internet Explorer 9+, Firefox 4+, Chrome 7+, and Safari 5.1 (see http://kangax.github.com/es5-compat-table/). And, of course, Windows Web Applications can take full advantage of the features of ECMAScript 5.
One of the major areas of change in ECMAScript 5 concerns objects and properties. In this section, we do a quick review of these changes.
Using Object.defineProperty() and Object.defineProperties()
In the ECMAScript 3 version of JavaScript, properties were not actually properties. In reality, a property was just a field. Unlike a C# property, an ECMAScript 3 property did not support setters or getters. You could not execute any logic when setting or reading a property.
All of this changes in ECMAScript 5. When you create a new property using ECMAScript 5, you are provided with the option of specifying a property descriptor. A property descriptor provides you with the opportunity to control the behavior of a property.
ECMAScript 5 contains two new methods named defineProperty() and defineProperties(). Both methods enable you to specify a descriptor when you create a property.
For example, the following code illustrates how you can create a property named price which includes a getter and setter:
var product = {};
Object.defineProperty(product, "price", {
set: function(value) {
if (value < 1) {
throw new Error("Invalid price!");
}
this._price = value;
},
get: function () {
return this._price;
}
});
// Set valid price
product.price = 34;
console.log(product.price); // Writes 34
// Set invalid price
product.price = -4; // Throws "Invalid price!"
console.log(product.price);
The defineProperty() method accepts three arguments: the object to which the property is being added, the name of the property to add, and the property descriptor. In the code above, a descriptor which contains a get and set property is passed to the defineProperty() method.
In the code above, the setter for the price property is used to perform validation logic. The setter verifies that the value being assigned to the price property is greater than 0. When you attempt to assign an invalid value, an exception is thrown.
ECMAScript 5 continues to support ECMAScript 3 style JavaScript properties. You can continue to create a property which acts just like a field which does not include a setter or getter. For example, the following code adds a description property to the product object:
var product = {};
Object.defineProperty(product, "description",
{
value: "A product"
});
console.log(product.description); // Writes "A product"
product.description = "Another description";
console.log(product.description); // Writes "A product"
In the code above, a property named description is added to the product object. This property has the default value “A product”. The default value is specified by using the value property contained in the descriptor.
You cannot mix both methods of creating a property: you can either create a property by using an accessor descriptor or a value descriptor. A descriptor can contain a value property, or a descriptor can contain a set and get property, but not both. You get a TypeError exception if you try.
Why would you want to create a simple value property without a getter or a setter using the defineProperty() method? The defineProperty() enables you to set additional properties of a property. You can set the following property properties with a descriptor:
· value – The default value of a simple property. Can’t use this property in combination with get and set properties.
· get – A function called when the property is read. The getter for the property.
· set – A function called when the property is set. The setter for the property.
· writable – Specifies whether or not the value of the property can be changed. Only works when value property is set. The default value is false.
· configurable – Specifies whether this property descriptor can be changed. The default value is false.
· Enumerable – Specifies whether this property is included when enumerating the properties of an object. The default value is false.
Notice, by default, a property is not writable or configurable. If you want a simple value property created with the defineProperty() method to act like a normal value property – in other words, you want to be able to change its value – then you need to make the property writable like this:
var product = {};
Object.defineProperty(product, "unitsInStock", {
value: 100,
writable: true
});
console.log(product.unitsInStock) // Writes 100
product.unitsInStock = 17;
console.log(product.unitsInStock) // Writes 17
In the code above, the unitsInStock property is created as a writable property. If you did not create the unitsInStock property as a writable property then you could not assign a new value to the property.
When “use strict” is enabled, assigning a new value to a property which is not writable results in an exception. When “use strict” is not enabled, the new value is silently ignored (which is a good reason to always enable “use strict”).
Using Object.create()
ECMAScript 5 also greatly improves prototype inheritance. The new standard includes an easier way to take advantage of prototype inheritance.
In ECMAScript 3, if you want to specify a prototype relationship between two objects, then you must specify that relationship between the constructor functions of the two objects. In other words, you must create two constructor functions and then set the prototype property on the second constructor function to point to an object created with the first constructor function.
In ECMAScript 5, you can specify a prototype relationship between two objects directly by taking advantage of the new Object.create() method. For example, the following code illustrates how you can create three objects – a Thing, Robot, and Roomba object – and set the prototype relationship among the three objects:
/*
Define Thing
*/
var thing = {
name: { value: "Thing" },
doSomething: function () {
return "Doing something...";
},
makeNoise: function () {
return "Crunch, boom, bop!";
}
};
/*
Define Robot
*/
var robot = Object.create(thing, {
name: { value: "Robot" }
});
// Override thing doSomething() method
robot.doSomething = function () {
return "Taking over world...";
};
/*
Define Roomba
*/
var roomba = Object.create(robot, {
name: { value: "Roomba" }
});
// Add vacuum() method
roomba.vacuum = function () {
return "Vacuuming...";
};
/*
Create a Roomba
*/
var myRoomba1 = Object.create(roomba);
console.log(myRoomba1.doSomething()); // writes "Taking over world..."
console.log(myRoomba1.makeNoise()); // writes "Crunch, boom, bop!"
The Object.create() method accepts two arguments: the prototype for the new object and an object which represents a set of property descriptors. The second argument is optional. For example, in the code above, we did not use the second argument when creating myRoomba1.
Notice that the code above does not include constructor functions. For example, the Robot class does not include a constructor function and it is not immediately obvious how you would add a constructor function to the Robot class. One of the advantages of the WinJS.Class.define() method, as we will see later in this blog entry, is that it makes it easy to specify a constructor function when creating a new object.
Using Object.preventExtensions(), Object.seal(), and Object.freeze()
For the sake of completeness, I want to mention three other new methods that you get with ECMAScript 5: the Object.preventExtensions() method, the Object.seal() method, and the Object.freeze() method.
The Object.preventExtensions() method enables you to prevent new properties and methods from being added to an object. You can continue to delete properties and method from the object. You also can change the values of the existing properties and methods. But, you cannot add new properties and methods.
For example, in the following code, the Object.preventExtensions() method is used to lock down the properties and methods of the product object:
var product = {
name: "Product",
sayName: function () {
return "My name is " + this.name;
}
};
// Create fish sticks
var fishsticks = Object.create(product);
// Prevent extensions
Object.preventExtensions(fishsticks);
console.log( Object.isExtensible(fishsticks)); // Writes false
// Change property value
fishsticks.name = "Fishsticks"; // Throws exception when "use strict"
console.log(fishsticks.name); // Writes "Product"
In the code above, a new fishsticks product is created with the help of the Object.create() method. Next, the Object.preventExtensions() method is called to lock down the properties of the object.
Attempting to change the value of the product name property will end in failure. If “use strict” is enabled then changing the value of the product name will throw an exception. Otherwise, if “use strict” is not enabled, any attempted change will be ignored.
When you attempt to change the product name property, you are actually adding a new property to the fishsticks object. Because the fishsticks class is marked as not extensible, attempting to add the new name property to the fishsticks object ends in failure.
The Object.seal() method is more aggressive about preventing object change. After you call the Object.seal() method on an object, you cannot delete any properties from the object and you also cannot change the property descriptors associated with the properties of an object. You can change the values of existing properties, but nothing else. Essentially, the Object.seal() method converts a dynamic object into a static object.
Finally, the Object.freeze() method is the most aggressive method. This method does everything to prevent changes to an object performed by the Object.seal() method. However, the Object.freeze() method also prevents you from changing the values of the properties of the object.
Creating Windows Web Application Objects
When you create a new Windows Web Application by using any of the Visual Studio JavaScript templates then the Windows JavaScript (WinJS) library is included in the new project automatically. You can find the WinJS libraries in the winjs\js folder in your new project (a new copy of the files is created for each new project).
One of the JavaScript files included in the winjs\js folder is named base.js. The base.js library contains the Microsoft extensions to JavaScript for working with objects and namespaces. These methods are used extensively by the other WinJS JavaScript libraries. In particular, all of the JavaScript controls are created using the methods contained in the base.js library. In this section, we discuss how you can create new JavaScript objects by taking advantage of the methods in the base.js library.
Using WinJS.Class.define()
In the WinJS library, new JavaScript objects are created by calling the WinJS.Class.define() method. This method accepts three arguments:
· constructor – The constructor function used to initialize the new object. If you pass null then an empty constructor is created
· instanceMembers – A collection of instance properties and methods
· staticMembers – A collection of static properties and methods
The following code demonstrates how to create a Robot class and then create a Roomba robot object from the Robot class:
var Robot = WinJS.Class.define(
function (name, price) {
this.name = name;
this.price = price;
},
{
name: undefined,
price: {
set: function(value) {
if (value < 0) {
throw new Error("Invalid price!");
}
this._price = value;
},
get: function () { return this._price; }
},
makeNoise: function () {
return "Burp, Wow!, oops!";
}
}
);
// Create a robot
var roomba = new Robot("Roomba", 200.33);
console.log(roomba.price); // Writes "200.33"
console.log(roomba.makeNoise()); // Writes "Burp, Wow!, oops!"
// Set invalid price
roomba.price = -88; // Throws "Invalid price!"
The Robot class is defined using the Object.Class.define() method. The first argument passed to this method is the constructor function for the Robot class. This constructor function initializes the Robot name and price properties.
The next argument passed to the Object.Class.define() method is a collection of instance members. This collection is used to define the name and price properties. This collection also contains the definition of the makeNoise() method.
Notice that the price property includes a getter and a setter. If you attempt to assign an invalid price to the Robot then the setter throws an Error (in order to see the error, you need to enable first chance exceptions with the line of code Debug.enableFirstChangeException(true)).
Under the covers, the WinJS.Class.define() method calls the InitializeProperties() method to configure each of the properties that you pass to the WinJS.Class.define() method through the instanceMembers and staticMembers arguments.
The initializeProperties() method calls the ECMAScript 5 Object.createProperties() method to initialize the properties of the class. Any private members – a member with a name which starts with an underscore – are marked as not enumerable automatically. That way, when you enumerate the properties and method of a class, the private properties and methods do not appear.
The initializeProperties() method also adds any methods that you include in the instanceMembers or staticMembers arguments to the target object. Unlike the normal Object.create() method, the WinJS.Class.define() method enables you to define instance and static methods to the object that you are creating.
Using WinJS.Class.derive()
The WinJS.Class.derive() method enables you to use prototype inheritance to derive one class from another class. The WinJS.Class.derive() method accepts the following four arguments:
· baseClass – The class to inherit from.
· constructor – A constructor function that can be used to initialize the new class
· instanceMembers – New instance properties and methods
· staticMembers – New static properties and methods
Here is a basic example. In the following code, three classes are defined: Robot, Roomba, and AIBO. The Robot class is the base class and the Roomba and AIBO classes derive from the Robot class:
var Robot = WinJS.Class.define(
function () {
this.type = "Robot"
},
{
sayHello: function () {
return "My name is " + this.name
+ " and I am a " + this.type;
}
}
);
var Roomba = WinJS.Class.derive(
Robot,
function (name) {
this.name = name;
this.type = "Roomba";
}
);
var AIBO = WinJS.Class.derive(
Robot,
function (name) {
this.name = name;
this.type = "AIBO";
}
);
// Create a Roomba
var myRoomba = new Roomba("rover");
console.log(myRoomba.sayHello());
// Create an AIBO
var myAIBO = new AIBO("spot");
console.log(myAIBO.sayHello());
In the code above, the constructor function for the Robot class is never called. The Roomba and the AIBO constructor functions are called instead. However, both the Roomba and AIBO classes inherit the sayHello() method from the base Robot class.
When you use the WinJS.Class.derive() method, you get two magic properties named constructor and _super. The constructor function refers to the constructor function that you pass to the WinJS.Class.derive() method as the first argument. The _super property returns the super class of the derived class.
The following code sample demonstrates how you can use the _super property to call a hidden method from a super class (base class) in a derived class:
var Robot = WinJS.Class.define(
null,
{
makeNoise: function () {
return "beep";
}
}
);
var Roomba = WinJS.Class.derive(
Robot,
null,
{
makeNoise: function () {
return this._super.makeNoise() + "!";
}
}
);
var myRobot = new Roomba();
console.log(myRobot.makeNoise()); // Writes "beep!"
In the code above, the Robot makeNoise() method returns the string “beep”. The Roomba makeNoise() method extends the Robot makeNoise() method by adding an exclamation mark !. When you call myRobot.makeNoise(), you get “beep!”.
Using WinJS.Class.mix()
The final WinJS method that we discuss in the blog entry is the WinJS.Class.mix() method. This method enables you to create mixins. A mixin enables you to avoid prototype inheritance by combining methods into a single object.
Microsoft claims that prototype inheritance has performance drawbacks. Following a prototype chain requires processor time. Therefore, the suggestion is that you avoid prototype inheritance by using mixins instead.
The performance drawbacks of long prototype chains are discussed in the Building Metro Style Apps using JavaScript Build talk by Chris Tavares:
http://channel9.msdn.com/events/BUILD/BUILD2011/TOOL-527C
This same issue with prototype inheritance was raised with the original Microsoft Ajax Library many years ago. See my blog entry from back in 2008
http://StephenWalther.com/blog/archive/2008/03/06/asp-net-ajax-in-depth-object-inheritance.aspx).
When you use a mixin instead of prototype inheritance, the methods and properties are combined into a single object. You don’t get a long prototype chain.
The WinJS.Class.mix() method has the following parameters:
· constructor – a constructor function used to initialize the new class
· mixin – a parameter array which contains the mixin methods
The following code sample demonstrates how you can use the WinJS.Class.mix() method to simulate single inheritance:
var Robot = {
makeNoise: function () {
return "beep";
}
};
var Roomba = WinJS.Class.mix(
function (name) {
this.name = name;
},
Robot
);
var myRoomba = new Roomba("rover");
console.log(myRoomba.makeNoise()); // Writes "beep"
In the code above, the Roomba class contains all of the methods of the Robot class.
One of the advantages of mixins is that you can use mixins to support something like multiple inheritance. You can use a mixin to combine as many sets of methods and properties as you need. For example, the following code sample demonstrates how you can build a Roomba from Robot methods, Product methods, and Vacuum methods:
"use strict";
var Robot = {
makeNoise: function () {
return "beep";
}
};
var Product = {
price: {
set: function (value) {
if (value < 0) {
throw new Error("Invalid price!");
}
this._price = value;
},
get: function () { return this._price; }
},
sayName: function () {
return this.name;
}
}
var Vacuum = {
vacuum: function () { return "bzzzzzz"; }
}
var Roomba = WinJS.Class.mix(
function (name) {
this.name = name;
},
Robot, Product, Vacuum
);
var myRoomba = new Roomba("rover");
console.log(myRoomba.makeNoise()); // Writes "beep"
console.log(myRoomba.sayName()); // Writes "rover"
console.log(myRoomba.vacuum()); // Writes "bzzzzz"
myRoomba.price = -88 // Throws Error
Notice that a mixin can contain both methods and properties. Furthermore, a mixin property can contain a setter and getter. For example, the price property included in the Product mixin includes a setter which performs validation.
When you execute the code above, the following results are displayed in your Visual Studio JavaScript Console window:
Summary
The goal of this blog entry was to explain how to use the WinJS methods for creating JavaScript objects. First, I provided an overview of how to create JavaScript objects in both ECMAScript 3 and ECMAScript 5.
We discussed how to take advantage of prototype inheritance in ECMAScript 3. You learned how to create constructor functions and create a prototype relationship between constructor functions by using the prototype property.
Next, you learned about the new methods for working with objects introduced in ECMAScript 5. You learned how to associate property descriptors with properties by using the Object.defineProperty() and Object.defineProperties() methods. You also learned how to create a prototype relationship between two objects without creating a constructor function by using the new ECMAScript 5 Object.create() method.
Finally, you learned how to create objects using the WinJS library. You learned how to use the WinJS.Class.define() method to create a new object. You learned how to use WinJS.Class.derive() to create a new object from an existing object. And, you learned how to use the WinJS.Class.mix() method to avoid deep prototype chains and to create a new JavaScript object from one or more other objects.
No comments:
Post a Comment