Chapter 16. JavaScript Objects
16.0. Introduction
Your JavaScript applications can consist entirely of functions and variables—both local and global—but if you want to ensure ease of reuse, compactness of code, and efficiency, as well as code that plays well with other libraries, you’re going to need to consider opportunities to encapsulate your code into objects.
Luckily, working with objects in JavaScript isn’t much more complicated than working with functions. After all, a JavaScript function is an object, and all objects are, technically, just functions.
Confused yet?
Unlike languages such as Java or C++, which are based on classes and class instances, JavaScript is based on prototypal inheritance. What prototypal inheritance means is that reuse occurs through creating new instances of existing objects, rather than instances of a class. Instead of extensibility occurring through class inheritance, prototypal extensibility happens by enhancing an existing object with new properties and methods.
Prototype-based languages have an advantage in that you don’t have to worry about creating the classes first, and then the applications. You can focus on creating applications, and then deriving the object framework via the effort.
It sounds like a mishmash concept, but hopefully as you walk through the recipes you’ll get a better feel for JavaScript’s prototype-based, object-oriented capabilities.
Note
ECMAScript and all its variations, including JavaScript, isn’t the only prototype-based language. The Wikipedia page on prototype-based languages, lists several.
See Also
Several of the recipes in this book are based on new functionality that was introduced in ECMAScript 5. You can access the complete ECMAScript 5 specification (a PDF) at http://www.ecmascript.org/docs/tc39-2009-043.pdf.
Note that the implementation of the new functionality is sketchy. I’ll point out browser support as I go.
16.1. Defining a Basic JavaScript Object
Solution
Define a new object explicitly using functional syntax, and then create new instances, passing in the data the object constructor is expecting:
function Tune (song, artist) { this.title = song; this.artist = artist; this.concat=function() { return this.title + "-" + this.artist; } } window.onload=function() { var happySong = new Array(); happySong[0] = new Tune("Putting on the Ritz", "Ella Fitzgerald"); // print out title and artist alert(happySong[0].concat()); }
Discussion
As you can see from the solution, there is no class description,
as you might expect if you’ve used other languages. A new object is
created as a function, with three members: two properties, title
and artist
, and one method, concat
. You could even use it like a
function:
Tune("test","artist");
However, using the object like a function, as compared to using it as an object constructor, has an odd and definitely unexpected consequence.
The new
keyword is used to create a new Tune
instance. Values are passed into the
object constructor, which are then assigned to the Tune
properties, title
and artist
, via the this
keyword. In the solution, this
is a reference to the object instance.
When you assign the property values to the object instance, you can then
access them at a later time, using syntax like
happySong[0].title
. In addition, the Tune
object’s concat
method also has access to these
properties.
However, when you use Tune
like
a regular function, this
doesn’t
represent an object instance, because there isn’t any. Instead, this
references the owner of the Tune
function, which in this case, is the
global window
object:
// treating Tune like a function Tune("the title", "the singer"); alert(window.concat()); // lo and behold, // "the title the singer" prints out
Completely unexpected and unwelcome behavior.
To summarize: to create a new object type, you can create a
function with both properties and methods. To ensure the properties are
assigned to the correct object, treat the object like a constructor
using the new
operator, rather than
as a function. The this
keyword
establishes ownership of the properties, which is the Tune
object instance, if the function is used
as an object constructor and not a regular function.
See Also
See Recipe 16.2 for more
information about the role this
plays
with JavaScript objects.
16.2. Keeping Object Members Private
Problem
You want to keep one or more object properties private, so they can’t be accessed outside the object instance.
Solution
When creating the private data members, do
not use the this
keyword with the member:
function Tune(song,artist) { var title = song; var artist = artist; this.concat = function() { return title + " " + artist; } } window.onload=function() { var happySongs = new Array(); happySongs[0] = new Tune("Putting on the Ritz", "Ella Fitzgerald"); try { // error alert(happySongs[0].title); } catch(e) { alert(e); } // prints out correct title and artist alert(happySongs[0].concat()); }
Discussion
Members in the object constructor (the function body), are not
accessible outside the object unless they’re assigned to that object
using this
. If they’re attached to
the object using the var
keyword,
only the Tune
’s inner
function, the concat
method, can access these now-private data members.
This type of method—one that can access the private data members,
but is, itself, exposed to public access via this
—has been termed a privileged method by Douglas
Crockford, the father of JSON (JavaScript Object Notation). As he
himself explains (at http://www.crockford.com/javascript/private.html):
This pattern of public, private, and privileged members is possible because JavaScript has closures. What this means is that an inner function always has access to the vars and parameters of its outer function, even after the outer function has returned. This is an extremely powerful property of the language [. . . .] Private and privileged members can only be made when an object is constructed. Public members can be added at any time.
See Also
See Recipe 6.5 for more on function closures. See Recipe 16.3 for more on adding public members after the object has been defined.
16.3. Expanding Objects with prototype
You want to extend an existing object with a new method.
Solution
Use the Object
prototype
to extend the object:
Tune.prototype.addCategory = function(categoryName) { this.category = categoryName; }
Discussion
Every object in JavaScript inherits from Object
, and all methods and other properties
are inherited via the prototype
object. It’s through the prototype
object that we can extend any object, and that includes the built-in
objects, such as String
and Number
. Once an object has been extended via
the prototype
property, all
instances of the object within the scope of the application have this
functionality.
In Example 16-1,
the new object, Tune
, is defined
using function syntax. It has two private data members, a title
and an artist
. A publicly accessible method, concat
, takes these two private data members,
concatenates them, and returns the result.
After a new instance of the object is created, and the object is extended with a new method and data member, the new method is used to update the existing object instance.
<!DOCTYPE html> <head> <title>Tune Object</title> <script> function Tune(song,artist) { var title = song; var artist = artist; this.concat = function() { return title + " " + artist; } } window.onload=function() { // create instance, print out values var happySong = new Tune("Putting on the Ritz", "Ella Fitzgerald"); // extend the object Tune.prototype.addCategory = function(categoryName) { this.category = categoryName; } // add category happySong.addCategory("Swing"); // print song out to new paragraph var song = "Title and artist: " + happySong.concat() + " Category: " + happySong.category; var p = document.createElement("p"); var txt = document.createTextNode(song); p.appendChild(txt); document.getElementById("song").appendChild(p); } </script> </head> <body> <h1>Tune</h1> <div id="song"> </div> </body> </html>
Figure 16-1 shows the page after the new element and data have been appended.
The prototype
property can also be used to override or extend external
or global objects. Before ECMAScript 5 added trim
as a default method for the String
object, applications used to extend the
String
object by adding a trim
method through the
prototype
object:
String.prototype.trim = function() { return (this.replace(/^[\s\xA0]+/, "").replace(/[\s\xA0]+$/, "")); }
Needless to say, you’d want to use extreme caution when using this
functionality with global objects. Applications that have extended the
String
object with a homegrown
trim
method may end up behaving
differently than applications using the new standard trim
method.
Using prototype
with your own
objects is usually safe. The only time you may run into problems is if
you provide your objects as an external library, and others build on
them.
16.4. Adding Getter/Setter to Objects
Solution
Use the getter/setter functionality introduced with ECMAScript 3.1 with your (or outside) objects:
function Tune() { var artist; var song; this.__defineGetter__("artist",function() { return artist}); this.__defineSetter__("artist",function(val) { artist = "By: " + val}); this.__defineGetter__("song",function() { return "Song: " + song}); this.__defineSetter__("song",function(val) { song=val}); } window.onload=function() { var happySong = new Tune(); happySong.artist="Ella Fitzgerald"; happySong.song="Putting on the Ritz"; alert(happySong.song + " " + happySong.artist); }
Discussion
We can add functions to our objects to get private data,
as demonstrated in Recipe 16.2. However, when we use the
functions, they are obvious functions. The getter/setter functionality
is a special syntax that provides property-like access to private data
members. The getter and setter functions provide an extra layer of
protection for the private data members. The functions also allow us to
prepare the data, as demonstrated with the solution, where the song
and artist
strings are concatenated to
labels.
You can define the getter and setter functions within the object constructor, as shown in the solution:
this.__defineGetter__("song",function() { return "Song: " + song}); this.__defineSetter__("song",function(val) { song=val});
You can also add a getter/setter with other objects, including DOM
objects. To add a getter/setter outside of the object constructor, you
first need to access the object’s prototype
object, and then add the
getter/setter functions to the prototype
:
var p = Tune.prototype; p.__defineGetter__("song",function() { return "Song: " + title}); p.__defineSetter__("song",function(val) { title=val});
You can also use getter/setters with “one-off” objects, used to provide JavaScript namespacing (covered later in the chapter):
var Book = { title: "The JavaScript Cookbook", get booktitle() { return this.title; }, set booktitle(val) { this.title = val; } }; Book.booktitle = "Learning JavaScript";
This approach can’t be used to hide the data, but can be used to control the display of the data, or to provide special processing of the incoming data before it’s stored in the data member.
See Also
See Recipe 16.6 for demonstrations of the new ECMAScript 5 property methods. Recipe 16.11 covers JavaScript namespacing.
16.5. Inheriting an Object’s Functionality
Problem
When creating a new object type, you want to inherit the functionality of an existing JavaScript object.
Solution
Use the concept of constructor chaining and the Function.apply
method to emulate traditional
class inheritance behavior in JavaScript:
function oldObject(param1) { this.param1 = param1; this.getParam=function() { return this.param1; } } function newObject(param1,param2) { this.param2 = param2; this.getParam2 = function() { return this.param2; } oldObject.apply(this,arguments); this.getAllParameters=function() { return this.getParam() + " " + this.getParam2(); } } window.onload=function() { newObject.prototype = new oldObject(); var obj = new newObject("value1","value2"); // prints out both parameters alert(obj.getAllParameters()); }
Discussion
In the solution, we have two objects: the original, oldObject
, and newObject
that inherits functionality from the
older object. For this to work, a couple of things are happening.
First, in the constructor for newObject
, an apply method is called on
oldObject
, passing in a reference to
the new object, and the argument array. The apply
method is inherited from the Function
object, and takes a reference to the
calling object, or the window object if the value is null, and an
optional argument array.
The second part of the inheritance is the line:
newObject.prototype=new oldObject();
This is an example of constructor chaining in JavaScript. What
happens is when you create a new instance of newObject
, you’re also creating a new instance
of oldObject
, in such a way that
newObject
inherits both the older
object’s methods and property.
It is this combination of constructor chaining, which chains the
constructors of the new objects together, and the apply
method, which passes both object context
and data to the inherited object, that implements inheritance behavior
in JavaScript. Because of this inheritance, the new object has access
not only to its own property, param2
,
and method, getParam2
, but also has
access to the old object’s param1
and
getParam
property and method.
To see another example of JavaScript inheritance, Example 16-2 shows it working
with another couple of objects: a Book
object and a TechBook
object, which inherits from Book
. The lines that implement the inheritance
are bolded.
<!DOCTYPE html> <head> <title>Constructor Chaining</title> <script type="text/javascript"> function Book (title, author) { var title = title; var author = author; this.getTitle=function() { return "Title: " + title; } this.getAuthor=function() { return "Author: " + author; } } function TechBook (title, author, category) { var category = category; this.getCategory = function() { return "Technical Category: " + category; } Book.apply(this,arguments); this.getBook=function() { return this.getTitle() + " " + author + " " + this.getCategory(); } } window.onload=function() { // chain the object constructors TechBook.prototype = new Book(); // get all values var newBook = new TechBook("The JavaScript Cookbook", "Shelley Powers", "Programming"); alert(newBook.getBook()); // now, individually alert(newBook.getTitle()); alert(newBook.getAuthor()); alert(newBook.getCategory()); } </script> </head> <body> <p>some content</p> </body>
Unlike the objects in the solution, all of the data members in
both objects in Example 16-2 are protected,
which means the data can’t be directly accessed outside the objects. Yet
notice when all three Book
properties—title
, author
, and category
—are printed out via the getBook
method in the TechBook
object, that TechBook
has access to the Book author
and title
properties, in addition to its own
category
. That’s because when the new
TechBook
object was created, a new
Book
object was also created and
inherited by the new TechBook
object.
To complete the inheritance, the data used in the constructor for
TechBook
is passed through the
Book
object using the apply
method.
Not only can the TechBook
object directly access the Book
instance data, you can access both of the Book
object’s methods, getAuthor
and getCategory
, directly on the instantiated
TechBook
object instance.
16.6. Extending an Object by Defining a New Property
Problem
You want to extend an existing object by adding a new property, but without changing the object’s constructor function.
Solution
Use the new ECMAScript Object.defineProperty
method to define one property:
Object.defineProperty(newBook, "publisher", { value: "O'Reilly", writable: false, enumerable: true, configurable: true});
Use the Object.defineProperties
method to define more than one property:
Object.defineProperties(newBook, { "stock": { value: true, writable: true, enumerable: true, }, "age": { value: "13 and up", writable: false } });
Discussion
Properties are handled differently in ECMAScript 5. Where before all you could do was assign a value, now you have greater control over how an object’s properties are managed. This greater control comes about through the provision for several new attributes that can be assigned to a property when it’s created. These new attributes make up what is known as the property descriptor object, and include:
The type of property descriptor can also vary. If the descriptor
is a data descriptor, another attribute is
value
, demonstrated in
the solution and equivalent to the following:
someObject.newProperty = "somevalue";
If the descriptor is an accessor descriptor, the property has
a getter/setter, similar to what was covered in Recipe 16.4. A restriction when
defining an accessor property is that you can’t set the writable
attribute:
Object.defineProperty(TechBook, "category", { get: function () { return category; }, set: function (value) { category = value; }, enumerable: true, configurable: true}); var newBook = new TechBook(...); newBook.publisher="O'Reilly";
You can also discover information about the property descriptor
for a property with the Object.getOwnPropertyDescription
method. To
use, pass in the object and the property name whose property descriptor
you wish to review:
var propDesc = Object.getOwnPropertyDescriptor(newBook,"category"); alert(propDesc.writable); // true if writable, otherwise false
The property has to be publicly accessible (not a private member).
Easily view all of the property attributes using the JSON object’s
stringify
method:
var val = Object.getOwnPropertyDescriptor(TechBook,"category"); alert(JSON.stringify(val)); // {"enumerable":true,"configurable":true}
If the property descriptor configurable
attribute
is set to true
, you can change
descriptor attributes. For instance, to change the writable
attribute from false
to true
, use the following:
Object.defineProperty(newBook, "publisher", { writable: true});
The previously set attributes retain their existing values.
Example 16-3 demonstrates the new property descriptors, first on a DOM object, then a custom object.
<!DOCTYPE html> <head> <title>Object Properties</title> <script type="text/javascript"> // Book custom object function Book (title, author) { var title = title; var author = author; this.getTitle=function() { return "Title: " + title; } this.getAuthor=function() { return "Author: " + author; } } // TechBook, inheriting from Book function TechBook (title, author, category) { var category = category; this.getCategory = function() { return "Technical Category: " + category; } Book.apply(this,arguments); this.getBook=function() { return this.getTitle() + " " + author + " " + this.getCategory(); } } window.onload=function() { try { // DOM test, WebKit bites the dust on this one var img = new Image(); // add new property and descriptor Object.defineProperty(img, "geolatitude", { get: function() { return geolatitude; }, set: function(val) { geolatitude = val;}, enumerable: true, configurable: true}); // test configurable and enumerable attrs var props = "Image has "; for (var prop in img) { props+=prop + " "; } alert(props); } catch(e) { alert(e); } try { // now we lose IE8 // chain the object constructors TechBook.prototype = new Book(); // add new property and property descriptor Object.defineProperty(TechBook, "experience", { get: function () { return category; }, set: function (value) { category = value; }, enumerable: false, configurable: true}); // get property descriptor and print var val = Object.getOwnPropertyDescriptor(TechBook,"experience"); alert(JSON.stringify(val)); // test configurable and enumerable props = "TechBook has "; for (var prop in TechBook) { props+=prop + " "; } alert(props); Object.defineProperty(TechBook, "experience", { enumerable: true}); props = "TechBook now has "; for (var prop in TechBook) { props+=prop + " "; } alert(props); // create TechBook instance var newBook = new TechBook("The JavaScript Cookbook", "Shelley Powers", "Programming"); // test new setter newBook.experience="intermediate"; // test data descriptor Object.defineProperty(newBook, "publisher", { value: "O'Reilly", writable: false, enumerable: true, configurable: true}); // test writable newBook.publisher="Some Other"; alert(newBook.publisher); } catch(e) { alert(e); } } </script> </head> <body> <p>some content</p> </body>
These methods are very new. At the time I wrote this recipe, they only work in nightly builds for WebKit and Firefox (Minefield), and in a very limited sense with IE8.
The IE8 limitation is that the new property methods only work with
DOM elements. The Object.defineProperty
method works with the Image
element, but not with the custom
objects. However, using defineProperty
on DOM elements causes an
exception in WebKit. None of the new property methods work with Opera.
The Firefox Minefield nightly and the Chrome beta were the only browsers
that currently work with both types of objects, as shown in Figure 16-2, which displays
the Image
object properties in
Firefox.
After printing out the Image
properties, a new property (experience
) and property descriptor are added
to the TechBook
custom object. The
Object.getOwnPropertyDescriptor
is called, passing in the TechBook
object and the property name,
experience
. The property descriptor
object is returned, and the JSON.stringify
method is used on the object to
print out the values:
{"enumerable":false,"configurable:true}
Next, the property descriptor values are tested. Currently,
because the experience
property is not enumerable, the use of the
for...in
loop can’t enumerate through
the properties, and the result is:
Techbook has prototype
The new experience
property’s
enumerable
attribute is then changed
to true
, because the property
descriptor for experience
allows
modification on descriptor values. Enumerating over the experience
property now yields the following
string for Firefox:
Techbook has prototype experience
However, Chrome does not pick up the prototype
property. The next two lines of code
create a new instance of the TechBook
object and adds an experience
, which
is then printed out to demonstrate the success of the property.
The last code in the example adds another new property (publisher
) and property descriptor. This is a
data property descriptor, which can also take a default value:
“O’Reilly”. The writable
property
descriptor is set to false
, and
configurable and enumerable descriptors are set to true
. The code then tries to change the
publisher value. However, the original publisher value of O'Reilly
prints out because the publisher
property writable
attribute is set to false
.
See Also
See Recipe 16.7
for more on property enumeration. Though not all browsers support
defineProperty
and defineProperties
yet, there are workarounds,
as detailed by John Resig in a nice
article describing the new object property
capabilities.
16.7. Enumerating an Object’s Properties
Solution
Use a specialized variation of the for
loop to iterate
through the properties:
for (var prop in obj) { alert(prop); // prints out the property name }
or use the new ECMAScript 5 Object.keys
method to return the names for all properties that can be
enumerated:
alert(Object.keys(obj).join(", "));
or use another new ECMAScript 5 method, Object.getOwnPropertyNames(obj)
, to get the
names of all properties, whether they can be enumerated or not:
var props = Object.getOwnPropertyNames(obj);
Discussion
For an object instance, newBook
, based on the following object
definitions:
function Book (title, author) { var title = title; this.author = author; this.getTitle=function() { return "Title: " + title; } this.getAuthor=function() { return "Author: " + author; } } function TechBook (title, author, category) { var category = category; this.getCategory = function() { return "Technical Category: " + category; } Book.apply(this,arguments); this.getBook=function() { return this.getTitle() + " " + author + " " + this.getCategory(); } } // chain the object constructors TechBook.prototype = new Book(); // create new tech book var newBook = new TechBook("The JavaScript Cookbook", "Shelley Powers", "Programming");
using the for...in
loop:
var str = ""; for (var prop in newBook) { str = str + prop + " "; } alert(str);
a message pops up with the following values:
getCategory author getTitle getAuthor getBook
Neither the category
property
in TechBook
nor the title
property in Book
are returned, as these are private data
members. When using WebKit nightly or Firefox Minefield, the same result
is returned when using the new Object.keys
method:
alert(Object.keys(newBook).join(" "));
The same result is also returned, again with WebKit nightly or
Firefox Minefield, when using the new Object.getOwnPropertyNames
method:
var props = Object.getOwnPropertyNames(newBook); alert(props.join(" "));
However, if I add a property descriptor for the title
property, making it enumerable:
// test data descriptor Object.defineProperty(newBook, "title", { writable: true, enumerable: true, configurable: true});
When I enumerate over the properties again, this time the title
displays among the properties, even
though it still can’t be accessed or reset directly on the
object.
We can also use these same property enumerators over the object
constructors (Book
or TechBook
) or any of the built-in objects.
However, the values we get back when we enumerate over Book
, as compared to the instance, newBook
, do vary among the methods. The
for...in
loop returns just one
property, prototype
, as does the
Object.keys
method. That’s because
prototype
is the only enumerable
property for the Function
object.
While newBook
is an instance of
Book
, Book
, itself is an instance of the Function
object.
The Object.getOwnPropertyNames
,
however, returns the following set of properties for Book
:
arguments callee caller length name prototype
Unlike Object.keys
and the
for...in
loop, Object.getOwnPropertyNames
returns a list of
all properties, not just those that are enumerable. This leads to a new
question: why did Object.getOwnPropertyNames
not return all of
the properties for the newBook
instance? It should have picked up title
before it was made enumerable, as well
as TechBook
’s private member,
category
.
I added another property descriptor to newBook
, this time for category
, and this time with enumerable
set to false
:
Object.defineProperty(newBook,"category", { writable: true, enumerable: false, configurable: true});
The category
property isn’t
listed when I use for...in
or
Object.keys
, but this time it is
picked up by Object.getOwnPropertyNames
.
It would seem that a property must either be publicly accessible,
or have a property descriptor for it to be picked up by Object.getOwnPropertyNames
, at least for these
earlier implementations of these new Object
methods. I imagine the reason is to
ensure consistent results between older ECMAScript implementations and
newer ones: defining a property in
older versions of ECMAScript is not the same as defining a property in
the newer version.
Speaking of ECMAScript 5 and new Object
methods, support for the older for...in
loop is broad, but support for
Object.keys
and Object.getOwnPropertyNames
, in addition to
support for property descriptors, is sparse at this time. Opera does not
support defineProperty
and the
associated new ECMAScript 5 functionality. WebKit nightly and the Chrome
beta support all of the new functionality, while the Firefox nightly
(Minefield), supports Object.keys
,
but not getOwnPropertyNames
. IE8’s
coverage is limited because it only supports the new methods on DOM
elements, such as Image
, and not on
custom objects.
See Also
For more on property descriptors, see Recipe 16.6.
16.8. Preventing Object Extensibility
Solution
Use the new ECMAScript 5 Object.preventExtensions
method to lock an object against future property
additions:
"use strict"; var Test = { value1 : "one", value2 : function() { return this.value1; } }; try { Object.preventExtensions(Test); // the following fails, and throws an exception in Strict mode Test.value3 = "test"; } catch(e) { alert(e); }
Discussion
Covering Object.preventExtensions
is a leap of faith on
my part, as no browser has implemented this new ECMAScript 5
functionality. However, by the time this book hits the streets, I expect
(hope) at least a couple of browsers will have implemented this new
feature.
The Object.preventExtensions
method prevents developers from extending the object with new
properties, though property values themselves are still writable. It
sets an internal property, Extensible
, to false
. You can check to see if an object is
extensible using Object.isExtensible
:
if (Object.isExtensible(obj)) { // extend the object }
Though you can’t extend the object, you can edit existing property values, as well as modify the object’s property descriptor.
See Also
Recipe 16.6 covers property descriptors.
16.9. Preventing Object Additions and Changes to Property Descriptors
Problem
You want to prevent extensions to an object, but you also want to disallow someone from changing the property descriptor for an object.
Solution
Use the new ECMAScript Object.seal
method to seal the object against additions and
modification of its property descriptor:
"use strict"; var Test = { value1 : "one", value2 : function() { return this.value1; } } try { // freeze the object Object.seal(Test); // the following would succeed Test.value2 = "two"; // the following would fail, throw an error in Strict Mode Test.newProp = "value3"; // so would the following Object.defineProperty(Title, "category", { get: function () { return category; }, set: function (value) { category = value; }, enumerable: true, configurable: true}); } catch(e) { alert(e); }
Discussion
Like Object.preventExtension
,
covered in Recipe 16.8, the
Object.seal
method is another new
ECMAScript 5 method that has no browser implementation yet, but should,
knock on wood, by the time you read this book. Look for a first
implementation in a Safari nightly build or a Firefox Minefield
build.
The Object.seal
method prevents
extensions to an object, like Object.preventExtensions
, but also prevents
any changes to the object’s property descriptor. To check if an object
is sealed, you would use the Object.isSealed
method:
if (Object.isSealed(obj)) ...
See Also
Property descriptors are described in Recipe 16.6, and Object.preventExtensions
is covered in Recipe 16.8.
16.10. Preventing Any Changes to an Object
Problem
You’ve defined your object, and now you want to make sure that its properties aren’t redefined or edited by other applications using the object.
Solution
Use the new ECMAScript 5 Object.freeze
method to freeze the object against any and all
changes:
"use strict"; var Test = { value1 : "one", value2 : function() { return this.value1; } } try { // freeze the object Object.freeze(Test); // the following would throw an error in Strict Mode Test.value2 = "two"; // so would the following Test.newProperty = "value"; // and so would the following Object.defineProperty(Title, "category", { get: function () { return category; }, set: function (value) { category = value; }, enumerable: true, configurable: true}); } catch(e) { alert(e); }
Discussion
There are several new Object
methods defined in ECMAScript 5 to provide better object management in
JavaScript. The least restrictive is Object.preventExtensions(obj)
, covered in Recipe 16.6, which disallows
adding new properties to an object, but you can change the object’s
property descriptor, or modify an existing property value.
The next more restrictive method is Object.seal
. The Object.seal(obj)
method prevents any modifications or new properties from being added to
the property descriptor, but you can modify an existing property
value.
The most restrictive new ECMAScript 5 Object
method is Object.freeze
. The Object.freeze(obj)
method disallows extensions
to the object, and restricts changes to the property descriptor.
However, Object.freeze
also prevents
any and all edits to existing object properties. Literally, once the
object is frozen, that’s it—no additions, no changes to existing
properties.
You can check to see if an object is frozen using the companion
method, Object.
is
Frozen
:
if (Object.isFrozen(obj)) ...
No browser currently implements Object.freeze
or Object.isFrozen
, but this state should change
relatively soon.
See Also
Recipe 16.6 covers
property descriptors, Recipe 16.8
covers Object.preventExtensions
, and
Recipe 16.9 covers
Object.seal
.
16.11. One-Off Objects and Namespacing Your JavaScript
Problem
You want to encapsulate your library functionality in such a way as to prevent clashes with other libraries.
Solution
Use an object literal, what I call a one-off object, to implement the JavaScript version of namespacing. An example is the following:
var jscbObject = { // return element getElem : function (identifier) { return document.getElementById(identifier); }, stripslashes : function(str) { return str.replace(/\\/g, ''); }, removeAngleBrackets: function(str) { return str.replace(/</g,'<').replace(/>/g,'>'); } }; var incoming = jscbObject.getElem("incoming"); var content = incoming.innerHTML; var result = jscbObject.stripslashes(content); result = jscbObject.removeAngleBrackets(result); jscbObject.getElem("result").innerHTML=result;
Discussion
As mentioned elsewhere in this book, all built-in objects in
JavaScript have a literal representation in addition to their more
formal object representation. For instance, an Array
can be created as follows:
var newArray = new Array('one','two','three');
or using the array literal notation:
var newArray = ['one','two','three'];
The same is true for objects. The notation for object literals is pairs of property names and associated values, separated by commas, and wrapped in curly brackets:
var newObj = { prop1 : "value", prop2 : function() { ... }, ... };
The property/value pairs are separated by colons. The properties can be scalar data values, or they can be functions. The object members can then be accessed using the object dot-notation:
var tmp = newObj.prop2();
Or:
var val = newObj.prop1 * 20;
Or:
getElem("result").innerHTML=result;
Using an object literal, we can wrap all of our library’s functionality in such a way that the functions and variables we need aren’t in the global space. The only global object is the actual object literal, and if we use a name that incorporates functionality, group, purpose, author, and so on, in a unique manner, we effectively namespace the functionality, preventing name clashes with other libraries.
This is the approach I use for every library, whether I create the library or use another, such as jQuery, Dojo, Prototype, and so on. As we’ll see later in the book, object literal notation is also the notation used by JSON, which is now formally a part of the ECMAScript 5 specification.
See Also
See Recipe 17.1 for a discussion related to packaging your code into a library for external distribution. Also check out Chapter 19 for recipes related to JSON.
16.12. Rediscovering “this” with Prototype.bind
Solution
Use the new ECMAScript 5 function bind
method:
window.onload=function() { window.name = "window"; var newObject = { name: "object", sayGreeting: function() { alert("Now this is easy, " + this.name); nestedGreeting = function(greeting) { alert(greeting + " " + this.name); }.bind(this); nestedGreeting("hello"); } } newObject.sayGreeting("hello"); }
If the method isn’t supported in your target browser(s), extend
the Function
object with the code
popularized by the Prototype.js JavaScript
library:
Function.prototype.bind = function(scope) { var _function = this; return function() { return _function.apply(scope, arguments); } }
Discussion
The this
keyword represents the
owner or scope of the function. The challenge associated with this
in current JavaScript libraries is that
we can’t guarantee which scope applies to a function.
In the solution, the literal object has a method, sayGreeting
, which prints a message out using
an alert, and then maps another nested function to its property,
nestedGreeting
.
Without the Function.bind
method, the first message printed out would say, “Now this is easy
object”, but the second would say, “hello window”. The reason the second
printout references a different name is that the nesting of the function
disassociates the inner function from the surrounding object, and all
unscoped functions automatically become the
property of the window object.
What the bind
method does is
use the apply
method to bind the
function to the object passed to the object. In the example, the
bind
method is invoked on the nested
function, binding it with the parent object using the apply
method.
bind
is particularly useful for
timers, such as setInterval
. Example 16-4 is a web page with
script that uses setTimeout
to
perform a countdown operation, from 10 to 0. As the numbers are counted
down, they’re inserted into the web page using the element’s innerHTML
property. Since most browsers have
not implemented Function.bind
as a
standard method yet, I added the Function.prototype.bind
functional
code.
<!DOCTYPE html>
<head>
<title>Using bind with timers</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
#item { font-size: 72pt; margin: 70px auto;
width: 100px;}
</style>
<script>
if (!Function.bind) {
Function.prototype.bind = function(scope) {
var _function = this;
return function() {
return _function.apply(scope, arguments);
}
}
}
window.onload=function() {
var theCounter = new Counter('item',10,0);
theCounter.countDown();
}
function Counter(id,start,finish) {
this.count = this.start = start;
this.finish = finish;
this.id = id;
this.countDown = function() {
if (this.count == this.finish) {
this.countDown=null;
return;
}
document.getElementById(this.id).innerHTML=this.count--;
setTimeout(this.countDown.bind(this),1000);
};
}
</script>
</head>
<body>
<div id="item">
10
</div>
</body>
If the line in bold text in the code sample had been the following:
setTimeout(this.countDown, 1000);
The application wouldn’t have worked, because the object scope and counter would have been lost when the method was invoked in the timer.
16.13. Chaining Your Object’s Methods
Problem
You wish to define your object’s methods in such a way that more than
one can be used at the same time, like the following, which retrieves a
reference to a page element and sets the element’s
style
property:
document.getElementById("elem").setAttribute("class","buttondiv");
Solution
The ability to directly call one function on the result of another in the same line of code is known as method chaining. It requires specialized code in whatever method you want to chain.
For instance, if you want to be able to chain the changeAuthor
method in the following object,
you must also return the object after you perform whatever other
functionality you need:
function Book (title, author) {
var title = title;
var author = author;
this.getTitle=function() {
return "Title: " + title;
}
this.getAuthor=function() {
return "Author: " + author;
}
this.replaceTitle = function (newTitle) {
var oldTitle = title;
title = newTitle;
}
this.replaceAuthor = function(newAuthor) {
var oldAuthor = author;
author = newAuthor;
}
}
function TechBook (title, author, category) {
var category = category;
this.getCategory = function() {
return "Technical Category: " + category;
}
Book.apply(this,arguments);
this.changeAuthor = function(newAuthor) {
this.replaceAuthor(newAuthor);
return this;
}
}
window.onload=function() {
try {
var newBook = new TechBook("I Know Things", "Shelley Powers", "tech");
alert(newBook.changeAuthor("Book K. Reader").getAuthor());
} catch(e) {
alert(e.message);
}
}
Discussion
The key to making method chaining work is to return a reference to
the object at the end of the method, as shown in the replaceAuthor
method in the solution:
this.changeAuthor = function(newAuthor) {
this.replaceAuthor(newAuthor);
return this;
}
In this example, the line return
this
returns the object reference.
Chaining is extensively used within the DOM methods, as shown throughout this book, when we see functionality such as:
var result = str.replace(/</g,'<').replace(/>/g,'>');
Libraries such as jQuery also make extensive use of method chaining:
$(document).ready(function() { $("#orderedlist").find("li").each(function(i) { $(this).append( " BAM! " + i ); }); });
If you examine the development version of jQuery (which is
uncompressed and very readable), you’ll see return this
sprinkled all throughout the
library’s methods:
// Force the current matched set of elements to become
// the specified array of elements
// (destroying the stack in the process)
// You should use pushStack() in order to do this,
// but maintain the stack
setArray: function( elems ) {
// Resetting the length to 0, then using the native Array push
// is a super-fast way to populate an object with
// array-like properties
this.length = 0;
push.apply( this, elems );
return this;
},
See Also
Chapter 17 provides an introduction to using jQuery in your JavaScript applications.
Get JavaScript Cookbook now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.