ptg 7.1 Objects and Properties 123 Listing 7.7 Looping arrays with for and for-in TestCase("ArrayLoopTest", { "test looping should iterate over all items": function () { var array = [1, 2, 3, 4, 5, 6]; var result = []; // Standard for-loop for (var i = 0, l = array.length; i < l; i++) { result.push(array[i]); } assertEquals("123456", result.join("")); }, "test for-in loop should iterate over all items": function () { var array = [1, 2, 3, 4, 5, 6]; var result = []; for (var i in array) { result.push(array[i]); } assertEquals("123456", result.join("")); } }); These two loops both attempt to copy all the elements of one array onto another, and then join both arrays into a string to verify that they do indeed contain the same elements. Running this test reveals that the second test fails with the message in Listing 7.8. Listing 7.8 Result of running test in Listing 7.7 expected "123456" but was "123456function () { [ snip]" To understand what’s happening, we need to understand the for-in enu- meration. for (var property in object) will fetch the first enumerable property of object. property is assigned the name of the property, and the body of the loop is executed. This is repeated as long as object has more enu- merable properties, and the body of the loop does not issue break (or return if inside a function). From the Library of WoweBook.Com Download from www.eBookTM.com ptg 124 Objects and Prototypal Inheritance For an array object, the only enumerable properties are its numeric indexes. The methods and the length property provided by Array.prototype are not enumerable. This is why a for-in loop will only reveal the indexes and their associated values for array objects. However, when we add properties to an object or one of the objects in its prototype chain, they are enumerable by default. Because of this fact, these new properties will also appear in a for-in loop, as shown by the test failure above. I recommend you don’t use for-in on arrays. The problem illustrated above can be worked around, as we will see shortly, but not without trading off per- formance. Using for-in on arrays effectively means we can’t normalize browser behavior by adding missing methods to Array.prototype without inferring a performance hit. 7.1.4.1 Object.prototype.hasOwnProperty Object.prototype.hasOwnProperty(name) returns true if an object has a property with the given name. If the object either inherits the property from the prototype chain, or doesn’t have such a property at all, hasOwnProperty returns false. This means that we can qualify a for-in loop with a call to hasOwnProperty to ensure we only loop the object’s own properties, as seen in Listing 7.9. Listing 7.9 Qualifying a loop with hasOwnProperty "test looping should only iterate over own properties": function () { var person = { name: "Christian", profession: "Programmer", location: "Norway" }; var result = []; for (var prop in person) { if (person.hasOwnProperty(prop)) { result.push(prop); } } var expected = ["location", "name", "profession"]; assertEquals(expected, result.sort()); } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 7.1 Objects and Properties 125 This test passes because we now filter out properties added to the proto- type chain. There are two things to keep in mind when dealing with Object. prototype.hasOwnProperty. • Some browsers, such as early versions of Safari don’t support it. • Objects are frequently used as hashes; there is a risk of hasOwnProperty being shadowed by another property. To guard our code against the latter case, we can implement a custom method that accepts an object and a property and returns true if the property is one of the object’s own properties, even when the object’s hasOwnProperty method is shadowed or otherwise unavailable. Listing 7.10 shows the method. Add it to the tdd.js file from Chapter 6, Applied Functions and Closures. Listing 7.10 Sandboxed hasOwnProperty tddjs.isOwnProperty = (function () { var hasOwn = Object.prototype.hasOwnProperty; if (typeof hasOwn == "function") { return function (object, property) { return hasOwn.call(object, property); }; } else { // Provide an emulation if you can live with possibly // inaccurate results } }()); For browsers that do not support this method we can emulate it, but it is not possible to provide a fully conforming implementation. Chances are that browsers that lack this method will present other issues as well, so failing to provide an emulation may not be your main problem. We will learn techniques to deal with such cases in Chapter 10, Feature Detection. Because properties are always enumerable when added by JavaScript, and because globals make it hard for scripts to co-exist, it is widely accepted that Object.prototype should be left alone. Array.prototype should also be treated with care, especially if you are using for-in on arrays. Although such loops should generally be avoided for arrays, they can be useful when dealing with large, sparse arrays. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 126 Objects and Prototypal Inheritance Keep in mind that although you may decide to avoid extending native objects, others may not be so nice. Filtering for-in loops with hasOwnProperty—even when you are not modifying Object.prototype and Array.prototype yourself—will keep your code running as expected, regardless of whether third- party code such as libraries, ad, or analytics related code decide to do so. 7.1.5 Property Attributes ECMA-262 defines four properties that may be set for any given property. It is important to note that these attributes are set for properties by the inter- preter, but JavaScript code you write has no way of setting these attributes. The ReadOnly and DontDelete attributes cannot be inspected explicitly, but we can deduce their values. ECMA-262 specifies the Object.prototype. propertyIsEnumerable method, which could be used to get the value of DontEnum; however, it does not check the prototype chain and is not reliably implemented across browsers. 7.1.5.1 ReadOnly If a property has the ReadOnly attribute set, it is not possible to write to the pro- perty. Attempting to do so will pass by silently, but the property attempted to update will not change. Note that if any object on the prototype chain has a property with the attribute set, writing to the property will fail. ReadOnly does not imply that the value is constant and unchanging—the interpreter may change its value internally. 7.1.5.2 DontDelete If a property has the DontDelete attribute set, it is not possible to delete it using the delete operator. Much like writing to properties with the ReadOnly attribute, deleting properties with the DontDelete attribute will fail silently. The expression will return false if the object either didn’t have the given property, or if the property existed and had a DontDelete attribute. 7.1.5.3 DontEnum DontEnum causes properties to not appear in for-in loops, as shown in Listing 7.9. The DontEnum attribute is the most important property attribute to understand because it is the one that is most likely to affect your code. In Listing 7.7 we saw an example of how enumerable properties may trip up badly written for-in loops. The DontEnum attribute is the internal mechanism that decides whether or not a property is enumerable. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 7.1 Objects and Properties 127 Internet Explorer (including version 8) has a peculiar bug concerning the DontEnum attribute—any property on an object that has a property by the same name anywhere on its prototype chain that has DontEnum will act as though it has DontEnum as well (even though it should be impossible to have DontEnum on a user-provided object). This means that if you create an object and shadow any of the properties on Object.prototype, neither of these properties will show up in a for-in loop in Internet Explorer. If you create an object of any of the native and host types, all the properties on the respective prototype chains with DontEnum will magically disappear from a for-in loop, as seen in Listing 7.11. Listing 7.11 Overriding properties with DontEnum TestCase("PropertyEnumerationTest", { "test should enumerate shadowed object properties": function () { var object = { // Properties with DontEnum on Object.prototype toString: "toString", toLocaleString: "toLocaleString", valueOf: "valueOf", hasOwnProperty: "hasOwnProperty", isPrototypeOf: "isPrototypeOf", propertyIsEnumerable: "propertyIsEnumerable", constructor: "constructor" }; var result = []; for (var property in object) { result.push(property); } assertEquals(7, result.length); }, "test should enumerate shadowed function properties": function () { var object = function () {}; // Additional properties with DontEnum on //Function.prototype object.prototype = "prototype"; object.call = "call"; object.apply = "apply"; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 128 Objects and Prototypal Inheritance var result = []; for (var property in object) { result.push(property); } assertEquals(3, result.length); } }); Both of these tests fail in all versions of Internet Explorer, including IE8; result.length is 0. We can solve this issue by making a special case for the non-enumerable properties on Object.prototype as well as Function. prototype if the object in question inherits from it. The tddjs.each method in Listing 7.12 can be used to loop properties of an object, accounting for Internet Explorer’s bug. When defined, the method attempts to loop the properties of an object that shadows all the non-enumerable proper- ties on Object.prototype as well as a function that shadows non-enumerable properties on Function.prototype. Any property that does not show up in the loop is remembered and looped explicitly inside the each function. Listing 7.12 Looping properties with a cross-browser each method tddjs.each = (function () { // Returns an array of properties that are not exposed // in a for-in loop on the provided object function unEnumerated(object, properties) { var length = properties.length; for (var i = 0; i < length; i++) { object[properties[i]] = true; } var enumerated = length; for (var prop in object) { if (tddjs.isOwnProperty(object, prop)) { enumerated -= 1; object[prop] = false; } } if (!enumerated) { return; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 7.1 Objects and Properties 129 } var needsFix = []; for (i = 0; i < length; i++) { if (object[properties[i]]) { needsFix.push(properties[i]); } } return needsFix; } var oFixes = unEnumerated({}, ["toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf", "constructor", "propertyIsEnumerable"]); var fFixes = unEnumerated( function () {}, ["call", "apply", "prototype"]); if (fFixes && oFixes) { fFixes = oFixes.concat(fFixes); } var needsFix = { "object": oFixes, "function": fFixes }; return function (object, callback) { if (typeof callback != "function") { throw new TypeError("callback is not a function"); } // Normal loop, should expose all enumerable properties // in conforming browsers for (var prop in object) { if (tddjs.isOwnProperty(object, prop)) { callback(prop, object[prop]); } } // Loop additional properties in non-conforming browsers var fixes = needsFix[typeof object]; if (fixes) { var property; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 130 Objects and Prototypal Inheritance for (var i = 0, l = fixes.length; i < l; i++) { property = fixes[i]; if (tddjs.isOwnProperty(object, property)) { callback(property, object[property]); } } } }; }()); If we change the for-in loops in the tests in Listing 7.11 to use the new tddjs.each method, the tests will run, even on Internet Explorer. Addition- ally, the method smoothes over a similar bug in Chrome in which function objects prototype property is not enumerable when shadowed. 7.2 Creating Objects with Constructors JavaScript functions have the ability to act as constructors when invoked with the new operator, i.e., new MyConstructor(). There is nothing that differentiates the definition ofa regular function and one that constructs objects. In fact, JavaScript provides every function with a prototype object in case it is used with the new operator. When the function is used as a constructor to create new objects, their internal [[Prototype]] property will be a reference to this object. In the absence of language level checks on functions vs. constructors, con- structor names are usually capitalized to indicate their intended use. Regardless of whether you use constructors or not in your own code, you should honor this idiom by not capitalizing names of functions and objects that are not constructors. 7.2.1 prototype and [[Prototype]] The word “prototype” is used to describe two concepts. First, a constructor has a public prototype property. When the constructor is used to create new ob- jects, those objects will have an internal [[Prototype]] property that is a refer- ence to the constructor’s prototype property. Second, the constructor has an internal [[Prototype]] that references the prototype of the constructor that cre- ated it, most commonly Function.prototype. All JavaScript objects have an internal [[Prototype]] property; only function objects have the prototype property. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 7.2 Creating Objects with Constructors 131 7.2.2 Creating Objects with new When a function is called with the new operator, a new JavaScript object is created. The function is then called using the newly created object as the this value along with any arguments that were passed in the original call. In Listing 7.13 we see how creating objects with constructors compares with the object literal we’ve been using so far. Listing 7.13 Creating objects with constructors function Circle(radius) { this.radius = radius; } // Create a circle var circ = new Circle(6); // Create a circle-like object var circ2 = { radius: 6 }; The two objects share the same properties—the radius property along with properties inherited from Object.prototype. Although both objects inherit from Object.prototype, circ2 does so directly (i.e., its [[Prototype]] prop- erty is a reference to Object.prototype), whereas circ does so indirectly through Circle.prototype. We can use the instanceof operator to deter- mine the relationship between objects. Additionally, we can use the constructor property to inspect their origin, as seen in Listing 7.14. Listing 7.14 Inspecting objects TestCase("CircleTest", { "test inspect objects": function () { var circ = new Circle(6); var circ2 = { radius: 6 }; assertTrue(circ instanceof Object); assertTrue(circ instanceof Circle); assertTrue(circ2 instanceof Object); assertEquals(Circle, circ.constructor); assertEquals(Object, circ2.constructor); } }); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 132 Objects and Prototypal Inheritance The expression a instanceof b will return true whenever the internal [[Prototype]] property of a, or one of the objects on its prototype chain, is the same object as b.prototype. 7.2.3 Constructor Prototypes Functions are always assigned a prototype property, which will be set as the in- ternal [[Prototype]] property of objects created by the function when used as a con- structor.The assigned prototype object’sprototype is in turn Object.prototype and it defines a single property, constructor, which is a reference to the con- structor itself. Because the new operator may be used with any expression that results in a constructor, we can use this property to dynamically create new objects of the same type as a known object. In Listing 7.15 we use the constructor property of a circle object to create a new circle object. Listing 7.15 Creating objects of the same kind "test should create another object of same kind": function () { var circle = new Circle(6); var circle2 = new circle.constructor(9); assertEquals(circle.constructor, circle2.constructor); assertTrue(circle2 instanceof Circle); } 7.2.3.1 Adding Properties to the Prototype We can give our new circle objects new functionality by augmenting the proto- type property of the constructor, much like we extended the behavior of native objects in Section 7.1.3, Extending Objects through the Prototype Chain. Listing 7.16 adds three methods for circle objects to inherit. Listing 7.16 Adding properties to Circle.prototype Circle.prototype.diameter = function () { return this.radius * 2; }; Circle.prototype.circumference = function () { return this.diameter() * Math.PI; }; From the Library of WoweBook.Com Download from www.eBookTM.com . Filtering for-in loops with hasOwnProperty—even when you are not modifying Object.prototype and Array.prototype yourself—will keep your code running as expected, regardless of whether third- party code. Attributes ECMA-262 defines four properties that may be set for any given property. It is important to note that these attributes are set for properties by the inter- preter, but JavaScript code. loop the properties of an object that shadows all the non-enumerable proper- ties on Object.prototype as well as a function that shadows non-enumerable properties on Function.prototype. Any property