Chapter 4. The Malleable JavaScript Object
With the increased interest in functional programming, you might think there’s less interest in JavaScript’s object-based capability. However, JavaScript is a flexible, adaptable language, and is just as happy to embrace both functional programming and object-oriented development.
There is a caveat related to JavaScript’s object-oriented capabilities: unlike languages such as Java or C++, which are based on classes and class instances, JavaScript is based on prototypical inheritance. What prototypical 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, prototypical 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.
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
;
this
.
concat
=
function
()
{
return
title
+
" "
+
artist
;
}
}
var
happySongs
=
[];
happySongs
[
0
]
=
new
Tune
(
"Putting on the Ritz"
,
"Ella Fitzgerald"
);
console
.
log
(
happySongs
[
0
].
title
);
// undefined
// prints out correct title and artist
console
.
log
(
happySongs
[
0
].
concat
());
Discussion
Variables in the object constructor (the function body), are not accessible outside the object unless they’re attached to that object using this
. If they’re redefined using the var
keyword or passed in as parameters 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:
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.
Be aware, though, that the privacy of the variable is somewhat illusory. One can easily assign a value to that property outside the constructor function, and overwrite the private data:
happySongs
[
0
].
title
=
'testing'
;
console
.
log
(
happySongs
[
0
].
title
);
// testing
However, the “privacy” of the data isn’t meant to ensure security of the object. It’s a contract with the developer, a way of saying, “This data isn’t meant to be accessed directly, and doing so will probably mess up your application.” As such, developers also typically use a naming convention where private data members begin with an underscore, to highlight that they aren’t meant to be accessed or set directly:
function
Tune
(
song
,
artist
)
{
var
_title
=
song
;
this
.
concat
=
function
()
{
return
_title
+
" "
+
artist
;
}
}
See Also
See Creating a Function That Remembers Its State for more on function closures. See Using Prototype to Create Objects for more on adding public members after the object has been defined.
Using Prototype to Create Objects
You want to create a new object, but you don’t want to add all the properties and methods into the constructor function.
Solution
Use the object’s prototype
to add the new properties:
Tune
.
prototype
.
addCategory
=
function
(
categoryName
)
{
this
.
category
=
categoryName
;
}
Discussion
Object is the ancestor for every object in JavaScript; objects inherit methods and properties from the Object via the Object prototype
. It’s through the prototype
that we can add new methods to existing objects:
var
str
=
'one'
;
String
.
prototype
.
exclaim
=
function
()
{
if
(
this
.
length
==
0
)
return
this
;
return
this
+
'!'
;
}
var
str2
=
'two'
;
console
.
log
(
str
.
exclaim
());
// one!
console
.
log
(
str2
.
exclaim
());
// two!
Before ECMAScript 5 added trim()
to 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. 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. To avoid this, libraries test to see if the method already exists before adding their own.
We can also use prototype
to add properties to our own objects. In Example 4-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, concatTitleArtist()
, 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 (addCategory()
) and data member (category
) the new method is used to update the existing object instance.
function
Tune
(
title
,
artist
)
{
this
.
concatTitleArtist
=
function
()
{
return
title
+
" "
+
artist
;
}
}
// 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
.
concatTitleArtist
()
+
" Category: "
+
happySong
.
category
;
console
.
log
(
song
);
The result of running the code is the following line printed out to the console:
"Title and artist: Putting on the Ritz Ella Fitzgerald Category: Swing"
One major advantage to extending an object using prototype
is increased efficiency. When you add a method directly to a function constructor, such as the concat
TitleArtist()
method in Tune
, every single instance of the object then has a copy of this function. Unlike the data members, the function isn’t unique to each object instance. When you extend the object using prototype
, as the code did with addCategory()
, the method is created on the object itself, and then shared equally between all instances of the objects.
Of course, using prototype
also has disadvantages. Consider again the concat
TitleArtist()
method. It’s dependent on access to data members that are not accessible outside the object. If the concatTitleArtist()
method was defined using prototype
and then tried to access these data members, an error occurs.
If you define the method using prototype
directly in the constructor function, it is created in the scope of the function and does have access to the private data, but the data is overridden each time a new object instance is created:
function
Tune
(
title
,
artist
)
{
var
title
=
title
;
var
artist
=
artist
;
Tune
.
prototype
.
concatTitleArtist
=
function
()
{
return
title
+
" "
+
artist
;
}
}
var
sad
=
new
Tune
(
'Sad Song'
,
'Sad Singer'
)
var
happy
=
new
Tune
(
'Happy'
,
'Happy Singer'
);
console
.
log
(
sad
.
concatTitleArtist
());
// Happy Happy Singer
The only data unique to the prototype function is what’s available via this
. There are twisty ways around this, but they not only add to the complexity of the application, they tend to undermine whatever efficiency we get using prototype
.
Generally, if your function must deal with private data, it should be defined within the function constructor, and without using prototype
. Otherwise, the data should be available via this
, or static and never changing once the object is created.
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 Object.create()
to implement the inheritance:
function
origObject
()
{
this
.
val1
=
'a'
;
this
.
val2
=
'b'
;
}
origObject
.
prototype
.
returnVal1
=
function
()
{
return
this
.
val1
;
};
origObject
.
prototype
.
returnVal2
=
function
()
{
return
this
.
val2
;
};
function
newObject
()
{
this
.
val3
=
'c'
;
origObject
.
call
(
this
);
}
newObject
.
prototype
=
Object
.
create
(
origObject
.
prototype
);
newObject
.
prototype
.
constructor
=
newObject
;
newObject
.
prototype
.
getValues
=
function
()
{
return
this
.
val1
+
" "
+
this
.
val2
+
" "
+
this
.
val3
;
};
var
obj
=
new
newObject
();
console
.
log
(
obj
instanceof
newObject
);
// true
console
.
log
(
obj
instanceof
origObject
);
// true
console
.
log
(
obj
.
getValues
());
"a b c"
Discussion
The Object.create()
method introduced with ECMAScript 5 provides classical inheritance in JavaScript. The first parameter is the object that serves as prototype for the newly created object, and the second optional parameter is a set of properties defined for the object, and equivalent to the second argument in Object.defineProperties()
.
In the solution for this recipe, the prototype for the original object is passed in the Object.create()
call, assigned to the new object’s own prototype
. The new object’s constructor
property is set to the new object’s constructor function. The new object’s prototype is then extended with a new method, getValues()
, which returns a string consisting of concatenated properties from both objects. Note the use of instanceof
demonstrating how both the old and new object prototypes are in the new object’s prototype chain.
In the constructor function for the new object, you need to use call()
to chain the constructors for both objects. If you want to pass the argument list between the two objects, use apply()
instead, as demonstrated in Example 4-2.
function
Book
(
title
,
author
)
{
this
.
getTitle
=
function
()
{
return
"Title: "
+
title
;
};
this
.
getAuthor
=
function
()
{
return
"Author: "
+
author
;
};
}
function
TechBook
(
title
,
author
,
category
)
{
this
.
getCategory
=
function
()
{
return
"Technical Category: "
+
category
;
};
this
.
getBook
=
function
()
{
return
this
.
getTitle
()
+
" "
+
author
+
" "
+
this
.
getCategory
();
};
Book
.
apply
(
this
,
arguments
);
}
TechBook
.
prototype
=
Object
.
create
(
Book
.
prototype
);
TechBook
.
prototype
.
constructor
=
TechBook
;
// get all values
var
newBook
=
new
TechBook
(
"The JavaScript Cookbook"
,
"Shelley Powers"
,
"Programming"
);
console
.
log
(
newBook
.
getBook
());
// now, individually
console
.
log
(
newBook
.
getTitle
());
console
.
log
(
newBook
.
getAuthor
());
console
.
log
(
newBook
.
getCategory
());
In jsBin, the output for the application is:
"Title: The JavaScript Cookbook Shelley Powers Technical Category: Programming"
"Title: The JavaScript Cookbook"
"Author: Shelley Powers"
"Technical Category: Programming"
Extending an Object by Defining a New Property
Problem
You can easily slap a new property onto an object, but you want to do so in such a way that you have more control of how it’s used.
Solution
Use the defineProperty()
method to add the property.
Given the following object:
var
data
=
{}
If you want to add the following two properties with the given characteristics:
-
type
: Initial value set and can’t be changed, can’t be deleted or modified, but can be enumerated -
id
: Initial value set, but can be changed, can’t be deleted or modified, and can’t be enumerated
Use the following JavaScript:
var
data
=
{};
Object
.
defineProperty
(
data
,
'type'
,
{
value
:
'primary'
,
enumerable
:
true
});
console
.
log
(
data
.
type
);
// primary
data
.
type
=
'secondary'
;
console
.
log
(
data
.
type
);
// nope, still primary
Object
.
defineProperty
(
data
,
'id'
,
{
value
:
1
,
writable
:
true
});
console
.
log
(
data
.
id
);
// 1
data
.
id
=
300
;
console
.
log
(
data
.
id
);
// 300
for
(
prop
in
data
)
{
console
.
log
(
prop
);
// only type displays
}
Discussion
The defineProperty()
is a way of adding a property to an object other than direct assignment that gives us some control over its behavior and state. There are two variations of property you can create with defineProperty()
: a data descriptor, as demonstrated in the solution, and an accessor descriptor, defined with a getter-setter function pair.
Note
The defineProperty()
Object method for accessor descriptors replaces the now deprecated __defineGetter
and __defineSetter
.
An example of an accessor descriptor is the following:
var
data
=
{};
var
group
=
'history'
;
Object
.
defineProperty
(
data
,
"category"
,
{
get
:
function
()
{
return
group
;
},
set
:
function
(
value
)
{
group
=
value
;
},
enumerable
:
true
,
configurable
:
true
});
console
.
log
(
data
.
category
);
// history
group
=
'math'
;
console
.
log
(
data
.
category
);
// math
data
.
category
=
'spanish'
;
console
.
log
(
data
.
category
);
// spanish
console
.
log
(
group
);
// spanish
Changes to the value for data.category
and group
are now interconnected.
The Object.defineProperty()
supports three parameters: the object, the property, and a descriptor object. The latter consists of the following options:
-
configurable:
false
by default; controls whether the property descriptor can be changed -
enumerable:
false
by default; controls whether the property can be enumerated -
writable:
false
by default; controls whether the property value can be changed through assignment - value: The initial value for the property
-
get:
undefined
by default; property getter -
set:
undefined
by default; property setter
The defineProperty()
method has wide support in all modern browsers, but with caveats. Safari does not allow its use on a DOM object, while IE8 only supports it on a DOM object (IE9 and later support it on all objects).
See Also
Preventing Object Extensibility details how to prevent the addition of new properties to an object, and Preventing Any Changes to an Object covers freezing an object against any further change.
Preventing Object Extensibility
Solution
Use the 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 a TypeError in Strict mode
Test
.
value3
=
"test"
;
}
catch
(
e
)
{
console
.
log
(
e
);
}
Discussion
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
}
If you attempt to add a property to an object that can’t be extended, the effort will either fail silently, or, if strict mode
is in effect, will throw a TypeError
exception:
TypeError
:
Can
'
t
add
property
value3
,
object
is
not
extensible
Though you can’t extend the object, you can edit existing property values, as well as modify the object’s property descriptor.
See Also
Extending an Object by Defining a New Property covers property descriptors. strict mode
was covered in Extra: Speaking of Strict Mode.
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 Object.freeze()
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 throws an error in Strict Mode
test
.
value2
=
'two'
;
// so does the following
test
.
newProperty
=
'value'
;
var
val
=
'test'
;
// and the following
Object
.
defineProperty
(
test
,
'category'
,
{
get
:
function
()
{
return
test
;
},
set
:
function
(
value
)
{
test
=
value
;
},
enumerable
:
true
,
configurable
:
true
});
}
catch
(
e
)
{
console
.
log
(
e
);
}
Discussion
ECMAScript 5 brought us several Object methods for better object management. The least restrictive is Object.preventExtensions(obj)
, covered in Preventing Object Extensibility, which disallows adding new properties to an object, but you can still change the object’s property descriptor or modify an existing property value.
The next, more restrictive method is Object.seal()
, which prevents any modifications or new properties from being added to the property descriptor, but you can modify an existing property value.
The most restrictive method is Object.freeze()
. This method disallows extensions to the object and restricts changes to the property descriptor. In addition, 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.
The first property modification in the solution code:
test
.
value2
=
"two"
;
results in the following error (in Chrome):
TypeError
:
Cannot
assign
to
read
only
property
'value2'
of
#
<
Object
>
If we comment out the line, the next object adjustment:
test
.
newProperty
=
"value"
;
throws the following error:
TypeError
:
Can
'
t
add
property
newProperty
,
object
is
not
extensible
Commenting out this line leaves the use of defineProperty()
:
var
val
=
'test'
;
// and the following
Object
.
defineProperty
(
test
,
"category"
,
{
get
:
function
()
{
return
test
;
},
set
:
function
(
value
)
{
test
=
value
;
},
enumerable
:
true
,
configurable
:
true
});
We get the final exception, for the use of defineProperty()
on the object:
TypeError
:
Cannot
define
property
:
category
,
object
is
not
extensible
.
If we’re not using strict mode, the first two assignments fail silently, but the use of defineProperty()
still triggers an exception (this mixed result is another good reason for using strict mode).
Check if an object is frozen using the companion method, Object.isFrozen()
:
if
(
Object
.
isFrozen
(
obj
))
...
Namespacing Your JavaScript Objects
Problem
You want to encapsulate your data and functions 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
sample
=
"<div>testing\changes</div>"
;
var
result
=
jscbObject
.
stripslashes
(
sample
);
result
=
jscbObject
.
removeAngleBrackets
(
result
);
console
.
log
(
result
);
//<div>testingchanges</div>
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 individually 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.
Advanced
I use the term one-off with the object literal rather than the more commonly known singleton because, technically, the object literal doesn’t fit the singleton pattern.
A singleton pattern is one where only one instance of an object can be created. We can say this is true of our object literal, but there’s one big difference: a singleton can be instantiated at a specific time rather than exist as a static construct, which is what the solution defines.
I went to Addy Osmani’s JavaScript Design Patterns (O’Reilly) to get an example of a good implementation of a singleton:
var
mySingleton
=
(
function
()
{
// Instance stores a reference to the Singleton
var
instance
;
function
init
()
{
// Singleton
// Private methods and variables
function
privateMethod
(){
console
.
log
(
"I am private"
);
}
var
privateVariable
=
"Im also private"
;
var
privateRandomNumber
=
Math
.
random
();
return
{
// Public methods and variables
publicMethod
:
function
()
{
console
.
log
(
"The public can see me!"
);
},
publicProperty
:
"I am also public"
,
getRandomNumber
:
function
()
{
return
privateRandomNumber
;
}
};
};
return
{
// Get the Singleton instance if one exists
// or create one if it doesn't
getInstance
:
function
()
{
if
(
!
instance
)
{
instance
=
init
();
}
return
instance
;
}
};
})();
singleA
=
mySingleton
.
getInstance
();
var
singleB
=
mySingleton
.
getInstance
();
console
.
log
(
singleA
.
getRandomNumber
()
===
singleB
.
getRandomNumber
()
);
The singleton uses an Immediately-Invoked Function Expression (IIFE) to wrap the object, which immediately returns an instance of the object. But not just any instance—if an instance already exists, it’s returned rather than a new instance. The latter is demonstrated by the object’s getRandomNumber()
function, which returns a random number that is generated when the object is created, and returns the same random number regardless of which “instance” is accessed.
Note
Access Addy Osmani’s Learning JavaScript Design Patterns online, or you can purchase a digital and/or paper copy directly at O’Reilly, or from your favorite book seller.
See Also
Chapter 7 covers external libraries and packaging your code into a library for external distribution. Chapter 12 covers another important pattern, the module pattern, and how modularization works with JavaScript.
Rediscovering this with Prototype.bind
Solution
Use the 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"
);
};
Discussion
this
represents the owner or scope of the function. The challenge associated with this
in JavaScript libraries is that we can’t guarantee which scope applies to a function.
In the solution, the object has a method, sayGreeting()
, which outputs a message and maps another nested function to its property, nestedGreeting
.
Without the Function’s 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 4-3 is a web page with a 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.
<!DOCTYPE html>
<head>
<html>
<title>
Using bind with timers</title>
<meta
charset=
utf-8"
/>
<style
type=
"text/css"
>
#item
{
font-size
:
72pt
;
margin
:
70px
auto
;
width
:
100px
;
}
</style>
</head>
<body>
<div
id=
"item"
>
10</div>
<script>
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>
</body>
</html>
If the setTimeout()
function 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.
Extra: self = this
An alternative to using bind()
, and one that is still in popular use, is to assign this
to a variable in the outer function, which is then accessible to the inner. Typically this
is assigned to a variable named that
or self
:
window
.
onload
=
function
()
{
window
.
name
=
"window"
;
var
newObject
=
{
name
:
"object"
,
sayGreeting
:
function
()
{
var
self
=
this
;
alert
(
"Now this is easy, "
+
this
.
name
);
nestedGreeting
=
function
(
greeting
)
{
alert
(
greeting
+
" "
+
self
.
name
);
};
nestedGreeting
(
"hello"
);
}
};
newObject
.
sayGreeting
(
"hello"
);
};
Without the assignment, the second message would reference “window”, not “object”.
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, similar to 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 TechBook.changeAuthor()
method in the following code snippet, you must also return the object after you perform whatever other functionality you need:
function
Book
(
title
,
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
)
{
this
.
getCategory
=
function
()
{
return
"Technical Category: "
+
category
;
};
Book
.
apply
(
this
,
arguments
);
this
.
changeAuthor
=
function
(
newAuthor
)
{
this
.
replaceAuthor
(
newAuthor
);
return
this
;
// necessary to enable method chaining
};
}
var
newBook
=
new
TechBook
(
"I Know Things"
,
"Smart Author"
,
"tech"
);
console
.
log
(
newBook
.
changeAuthor
(
"Book K. Reader"
).
getAuthor
());
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 changeAuthor()
method in the solution:
this
.
changeAuthor
=
function
(
newAuthor
)
{
this
.
replaceAuthor
(
newAuthor
);
return
this
;
// necessary to enable method chaining
};
Chaining is used extensively in JavaScript objects, and demonstrated 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, as we’ll see later in the book.
Get JavaScript Cookbook, 2nd Edition 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.