Listing 7.50 Borrowing from Array.prototype "test arguments should borrow from Array.prototype": function { function addToArray { var args = Array.prototype.slice.callarguments; var arr
Trang 1accept an optional properties argument We will discuss this method further in
Chapter 8, ECMAScript 5th Edition.
7.5.2 The tddjs.extend Method
Often we want to borrow behavior from one or more other objects to build the functionality we’re after We’ve seen this a couple of times already Remember the argumentsobject? It acts roughly like an array, but it is not a true array, and
as such, lacks certain properties we might be interested in The arguments ob-ject does, however, possess the most important aspects of an array: the length property, and numerical indexes as property names These two aspects are enough for most methods on Array.prototype to consider arguments an object that “walks like a duck, swims like a duck, and quacks like a duck” and there-fore is a duck (or rather, an array) This means that we can borrow methods from Array.prototype by calling them with arguments as this, as seen in Listing 7.50
Listing 7.50 Borrowing from Array.prototype
"test arguments should borrow from Array.prototype":
function () { function addToArray() { var args = Array.prototype.slice.call(arguments);
var arr = args.shift();
return arr.concat(args);
} var result = addToArray([], 1, 2, 3);
assertEquals([1, 2, 3], result);
} The example borrows the slice function and calls it on the arguments object Because we don’t give it any other arguments, it will return the whole array, but the trick is now we’ve effectively converted arguments to an array, on which
we can call the usual array methods
Remember in Chapter 5, Functions, we illustrated implicit binding of this
by copying a function from one object to another Doing so causes both objects
to share the same function object, so it’s a memory efficient way to share behavior
Listing 7.51 shows an example
Trang 2Listing 7.51 Borrowing explicitly
"test arguments should borrow explicitly from Array.prototype":
function () {
function addToArray() { arguments.slice = Array.prototype.slice;
var args = arguments.slice();
var arr = args.shift();
return arr.concat(args);
} var result = addToArray([], 1, 2, 3);
assertEquals([1, 2, 3], result);
}
Using this technique, we can build objects that are collections of methods related over some topic, and then add all the properties of this object onto another object
to “bless” it with the behavior Listing 7.52 shows the initial test case for a method
that will help us do exactly that
Listing 7.52 Initial test case for tddjs.extend
TestCase("ObjectExtendTest", {
setUp: function () { this.dummy = { setName: function (name) { return (this.name = name);
}, getName: function () { return this.name || null;
} };
},
"test should copy properties": function () { var object = {};
tddjs.extend(object, this.dummy);
assertEquals("function", typeof object.getName);
assertEquals("function", typeof object.setName);
} });
Trang 3The test sets up a dummy object in the setUp method It then asserts that when extending an object, all the properties from the source object is copied over
This method is definitely eligible for the Internet Explorer DontEnum bug, so Listing 7.53 uses the tddjs.each method to loop the properties
Listing 7.53 Initial implementation of tddjs.extend
tddjs.extend = (function () { function extend(target, source) { tddjs.each(source, function (prop, val) { target[prop] = val;
});
} return extend;
}());
The next step, seen in Listing 7.54, is to ensure that the two arguments are safe
to use Any object will do on both sides; we simply need to make sure they’re not nullor undefined
Listing 7.54 Extending null
"test should return new object when source is null":
function () { var object = tddjs.extend(null, this.dummy);
assertEquals("function", typeof object.getName);
assertEquals("function", typeof object.setName);
} Note the expected return value Listing 7.55 shows the implementation
Listing 7.55 Allowing target to be null
function extend(target, source) { target = target || {};
tddjs.each(source, function (prop, val) { target[prop] = val;
});
return target;
}
Trang 4If the source is not passed in, we can simply return the target untouched, as seen in Listing 7.56
Listing 7.56 Dealing with only one argument
"test should return target untouched when no source":
function () {
var object = tddjs.extend({});
var properties = [];
for (var prop in object) {
if (tddjs.isOwnProperty(object, prop)) { properties.push(prop);
} } assertEquals(0, properties.length);
}
Now something interesting happens This test passes in most browsers, even when source is undefined This is because of browsers’ forgiving nature, but
it is violating ECMAScript 3, which states that a TypeError should be thrown
when a for-in loop is trying to loop null or undefined Interestingly, Internet
Explorer 6 is one of the browsers that does behave as expected here ECMAScript
5 changes this behavior to not throw when the object being looped is null or
undefined Listing 7.57 shows the required fix
Listing 7.57 Aborting if there is no source
function extend(target, source) {
target = target || {};
if (!source) { return target;
} /* */
}
Note that tddjs.extend always overwrites if target already defines a given property We could embellish this method in several ways—adding a boolean
option to allow/prevent overwrite, adding an option to allow/prevent shadowing
of properties on the prototype chain, and so on Your imagination is your limit
Trang 57.5.3 Mixins
An object that defines a set of properties that can be used with the tddjs.extend
method to “bless” other objects is often called a mixin For instance, the Ruby
standard library defines a bunch of useful methods in its Enumerable module,
which may be mixed in to any object that supports the each method Mixins provide
an incredibly powerful mechanism for sharing behavior between objects We could easily port the enumerable module from Ruby to a JavaScript object and mix it in with, e.g., Array.protoype to give all arrays additional behavior (remember to not loop arrays with for-in) Listing 7.58 shows an example that assumes that the enumerableobject contains at least a reject method
Listing 7.58 Mixing in the enumerable object to Array.prototype
TestCase("EnumerableTest", {
"test should add enumerable methods to arrays":
function () { tddjs.extend(Array.prototype, enumerable);
var even = [1, 2, 3, 4].reject(function (i) { return i % 2 == 1;
});
assertEquals([2, 4], even);
} });
Assuming we are in a browser that supports Array.prototype.forEach,
we could implement the reject method as seen in Listing 7.59
Listing 7.59 Excerpt of JavaScript implementation of Ruby’s enumerable
var enumerable = { /* */
reject: function (callback) { var result = [];
this.forEach(function (item) {
if (!callback(item)) { result.push(item);
} });
Trang 6return result;
} };
7.6 Summary
In this chapter we’ve seen several approaches to JavaScript object creation, and
sharing of behavior between them We started by gaining a thorough understanding
of how JavaScript properties and the prototype chain work We then moved on
to constructors and used them in conjunction with their prototype property
to implement an emulation of classical inheritance Pseudo-classical inheritance
can be tempting for developers unfamiliar with prototypes and JavaScript’s native
inheritance mechanism, but can lead to complex solutions that are computationally
inefficient
Dispensing the constructors, we moved on to prototype-based inheritance and
explored how JavaScript allows us to work solely on objects by extending
ob-jects with other obob-jects By implementing a simple Object.create function, we
avoided some of the confusion introduced by constructors and were able to see
clearer how the prototype chain helps us extend the behavior of our objects
Functional inheritance showed us how closures can be used to store state and
achieve truly private members and methods
To wrap it all up, we looked at object composition and mixins in JavaScript, combining all of the previously discussed patterns Mixins are a great match for
JavaScript, and often offer a great way to share behavior between objects
Which technique to use? The answer will vary depending on whom you ask
There is no one right answer, because it depends on your situation As we’ve seen,
there are trade-offs when choosing between a pseudo-classical approach and a
func-tional approach that will be affected by whether object creation, method invocation,
memory usage, or security is the most crucial aspect of your application
Through-out this book we’ll see how to use a few of the techniques presented in this chapter
in real life examples
Trang 78
ECMAScript 5th Edition
In December 2009, ECMA-262 5th Edition, commonly referred to as ECMAScript 5, or simply ES5, was finalized and published by ECMA International
This marked the first significant update to the core JavaScript language in 10 years
ECMAScript 5 is the successor to ECMAScript 3, and is a mostly backwards com-patible update of the language that codifies innovation by browser vendors over the past decade and introduces a few new features
ECMAScript 4 was never realized, and is part of the answer to why the language could go without standardized updates for 10 years This draft was widely considered too revolutionary an update, and introduced several features that would not work well with existing browsers To this day, Adobe’s ActionScript (used in Flash) and Microsoft’s JScript.Net are the only runtimes to implement a significant amount of the proposed updates from ES4
In this chapter we will take a cursory look at the most interesting changes in ES5, and have a look at some of the programming patterns the new specification enables Particularly interesting are new additions to objects and properties, and these will be afforded the bulk of our attention Note that this chapter does not
cover all changes and additions in ECMAScript 5.
8.1 The Close Future of JavaScript
Backwards compatibility has been a major concern of ES5 JavaScript is ubiquitous—every web browser released since the mid to late nineties supports
Trang 8it in some form or other; it’s on phones and other mobile devices; it’s used to
de-velop extensions for browsers such as Mozilla Firefox and Google Chrome and has
even taken the front seat in Gnome Shell, the defining technology in the Gnome 3
desktop environment for Linux JavaScript runtimes are wild beasts When
deploy-ing a script on the web, we can never know what kind of runtime will attempt to
run our code Couple this with the huge amount of JavaScript already deployed on
the web, and you will have no problem imagining why backwards compatibility has
been a key concern for ES5 The goal is not to “break the web,” but rather bring it
forward
ES5 has worked hard to standardize, or codify, existing de facto standards—
innovation in the wild adopted across browser vendors as well as common use
cases found in modern JavaScript libraries String.prototype.trim and
Function.prototype.bindare good examples of the latter, whereas attribute
getters and setters are good examples of the former
Additionally, ES5 introduces strict mode, which points out the way moving
forward Strict mode can be enabled with a simple string literal, and makes ES5
compliant implementations, well, stricter in their parsing and execution of scripts
Strict mode sheds some of JavaScript’s bad parts and is intended to serve as the
starting point for future updates to the language
The reason this section is entitled the close future of JavaScript is that there
is reason to believe that we won’t have to wait another 10 years for good browser
support This is of course speculation on my (and others) part, but as ES5
cod-ifies some de facto standards, some of these features are already available in
a good selection of browsers today Additionally, the last couple of years have
seen a somewhat revitalized “browser war,” in which vendors compete harder
than in a long time in creating modern standards compliant and performant
browsers
Microsoft and their Internet Explorer browser have slowed down web devel-opers for many years, but recent development seems to suggest that they’re at least
back in the game trying to stay current Besides, browser usage looks vastly different
today compared with only 5 years ago, and fair competition regulations are already
forcing Windows users in Europe to make a conscious choice of browser
All in all, I am fairly positive to the adoption of ES5 Some of it is already supported in browsers like Chrome, Firefox, and Safari, and preview releases of all
the aforementioned browsers adds more At the time of writing, even previews of
Internet Explorer 9 already implement most of ES5 I expect the situation to look
even brighter once this book hits the shelves
Trang 98.2 Updates to the Object Model
Of all the updates in ECMAScript 5, I find the updated object model to be the
most exciting As we discussed in Chapter 7, Objects and Prototypal Inheritance,
JavaScript objects are simple mutable collections of properties, and even though ES3 defines attributes to control whether properties can be overwritten, deleted, and enumerated, they remain strictly internal, and thus cannot be harnessed by client objects This means that objects that are dependent on their (public and mutable) properties need to employ more error checking than desired to remain reasonably robust
8.2.1 Property Attributes
ES5 allows user-defined property descriptors to overwrite any of the following attributes for a given property
• enumerable — Internal name [[Enumerable]], formerly [[DontEnum]], controls whether the property is enumerated in for-in loops
• configurable — Internal name [[Configurable]], formerly [[DontDelete]], controls whether the property can be deleted with delete
• writable — Internal name [[Writable]], formerly [[ReadOnly]], controls whether the property can be overwritten
• get — Internal name [[Get]], a function that computes the return value of property access
• set — Internal name [[Set]], a function that is called with the assigned value when the property is assigned to
In ES5 we can set a property in two ways The old school way, shown in Listing 8.1, in which we simply assign a value to a property, or the new way, shown
in Listing 8.2
Listing 8.1 Simple name/value assignment
var circle = {};
circle.radius = 4;
Trang 10Listing 8.2 Empowered ES5 properties
TestCase("ES5ObjectTest", {
"test defineProperty": function () { var circle = {};
Object.defineProperty(circle, "radius", { value: 4,
writable: false, configurable: false });
assertEquals(4, circle.radius);
} });
The Object.defineProperty method can be used not only to define new properties on an object, but also to update the descriptor of a property Updating
a property descriptor only works if the property’s configurable attribute is set
to true Listing 8.3 shows an example of how you can use the existing descriptor
to update only some attributes
Listing 8.3 Changing a property descriptor
"test changing a property descriptor": function () {
var circle = { radius: 3 };
var descriptor = Object.getOwnPropertyDescriptor(circle, "radius");
descriptor.configurable = false;
Object.defineProperty(circle, "radius", descriptor);
delete circle.radius;
// Non-configurable radius cannot be deleted assertEquals(3, circle.radius);
}
In addition to controlling the property attributes, ES5 also allows trol over the internal [[Extensible]] property of an object This property
con-trols whether or not properties can be added to the object Calling Object
preventExtensions(obj)shuts the object down for further extension and
cannot be undone
Preventing object extensions and setting property attributes writable and configurableto false means you can now create immutable objects This
removes a lot of error checking and complexity brought on by the fact that