A Re-Introduction to JavaScript, Part II
|
|
|
| 5.0/5.0 (2 votes total) |
|
|
|
Simon Willison April 27, 2006
|
The beginning of this article can be found at A Re-Introduction to JavaScript, Part I.
Custom objects
In classic
Object Oriented Programming, objects are collections of data and
methods that operate on that data. Let's consider a person object
with first and last name fields. There are two ways in which their
name might be displayed: as "first last" or as "last,
first". Using the functions and objects that we've discussed
previously, here's one way of doing it:
function
makePerson(first, last) {
return
{
first:
first,
last:
last
}
}
function
personFullName(person) {
return
person.first + ' ' + person.last;
}
function
personFullNameReversed(person) {
return
person.last + ', ' + person.first
}
>
s = makePerson("Simon", "Willison");
>
personFullName(s)
Simon
Willison
>
personFullNameReversed(s)
Willison,
Simon
This works, but
it's pretty ugly. You end up with dozens of functions in your global
namespace. What we really need is a way to attach a function to an
object. Since functions are objects, this is easy:
function
makePerson(first, last) {
return
{
first:
first,
last:
last,
fullName:
function() {
return
this.first + ' ' + this.last;
},
fullNameReversed:
function() {
return
this.last + ', ' + this.first;
}
}
}
>
s = makePerson("Simon", "Willison")
>
s.fullName()
Simon
Willison
>
s.fullNameReversed()
Willison,
Simon
There's
something here we haven't seen before: the 'this'
keyword. Used inside a function, 'this'
refers to the current object. What that actually means is specified
by the way in which you called that function. If you called it using
dot
notation or bracket notation on an object, that object
becomes 'this'.
If dot notation wasn't used for the call, 'this'
refers to the global object. This is a frequent cause of mistakes.
For example:
>
s = makePerson("Simon", "Willison")
>
var fullName = s.fullName;
>
fullName()
undefined
undefined
When we call
fullName(),
'this'
is bound to the global object. Since there are no global variables
called first
or last
we get undefined
for each one.
We can take
advantage of the 'this'
keyword to improve our makePerson
function:
function
Person(first, last) {
this.first
= first;
this.last
= last;
this.fullName
= function() {
return
this.first + ' ' + this.last;
}
this.fullNameReversed
= function() {
return
this.last + ', ' + this.first;
}
}
var
s = new Person("Simon", "Willison");
We've introduced
another keyword: 'new'.
new is
strongly related to 'this'.
What it does is it creates a brand new empty object, and then calls
the function specified, with 'this'
set to that new object. Functions that are designed to be called by
'new'
are called constructor functions. Common practise is to capitalise
these functions as a reminder to call them with new.
Our person
objects are getting better, but there are still some ugly edges to
them. Every time we create a person object we are creating two brand
new function objects within it - wouldn't it be better if this code
was shared?
function
personFullName() {
return
this.first + ' ' + this.last;
}
function
personFullNameReversed() {
return
this.last + ', ' + this.first;
}
function
Person(first, last) {
this.first
= first;
this.last
= last;
this.fullName
= personFullName;
this.fullNameReversed
= personFullNameReversed;
}
That's better:
we are creating the method functions only once, and assigning
references to them inside the constructor. Can we do any better than
that? The answer is yes:
function
Person(first, last) {
this.first
= first;
this.last
= last;
}
Person.prototype.fullName
= function() {
return
this.first + ' ' + this.last;
}
Person.prototype.fullNameReversed
= function() {
return
this.last + ', ' + this.first;
}
Person.prototype
is an object shared by all instances of Person.
It forms part of a lookup chain (that has a special name, "prototype
chain"): any time you attempt to access a property of Person
that isn't set, JavaScript will check Person.prototype
to see if that property exists there instead. As a result, anything
assigned to Person.prototype
becomes available to all instances of that constructor via the this
object.
This is an
incredibly powerful tool. JavaScript lets you modify something's
prototype at any time in your program, which means you can add extra
methods to existing objects at runtime:
>
s = new Person("Simon", "Willison");
>
s.firstNameCaps();
TypeError
on line 1: s.firstNameCaps is not a function
>
Person.prototype.firstNameCaps = function() {
return
this.first.toUpperCase()
}
>
s.firstNameCaps()
SIMON
Interestingly,
you can also add things to the prototype of built-in JavaScript
objects. Let's add a method to String
that returns that string in reverse:
>
var s = "Simon";
>
s.reversed()
TypeError
on line 1: s.reversed is not a function
>
String.prototype.reversed = function() {
var
r = ;
for
(var i = this.length - 1; i >= 0; i--) {
r
+= this[i];
}
return
r;
}
>
s.reversed()
nomiS
Our new method
even works on string literals!
>
"This can now be reversed".reversed()
desrever
eb won nac sihT
As I mentioned
before, the prototype forms part of a chain. The root of that chain
is Object.prototype,
whose methods include toString()
- it is this method that is called when you try to represent an
object as a string. This is useful for debugging our Person
objects:
>
var s = new Person("Simon", "Willison");
>
s
[object
Object]
>
Person.prototype.toString = function() {
return
'<Person: ' + this.fullName() + '>';
}
>
s
<Person:
Simon Willison>
Remember how
avg.apply()
had a null first argument? We can revisit that now. The first
argument to apply()
is the object that should be treated as 'this'.
For example, here's a trivial implementation of 'new':
function
trivialNew(constructor) {
var
o = {}; // Create an object
constructor.apply(o,
arguments);
return
o;
}
This isn't an
exact replica of new
as it doesn't set up the prototype chain. apply()
is difficult to illustrate - it's not something you use very often,
but it's useful to know about.
apply()
has a sister function named call,
which again lets you set 'this'
but takes an expanded argument list as opposed to an array.
function
lastNameCaps() {
return
this.last.toUpperCase();
}
var
s = new Person("Simon", "Willison");
lastNameCaps.call(s);
//
Is the same as:
s.lastNameCaps
= lastNameCaps;
s.lastNameCaps();
Inner functions
JavaScript
function declarations are allowed inside other functions. We've seen
this once before, with an earlier makePerson()
function. An important detail of nested functions in JavaScript is
that they can access variables in their parent function's scope:
function
betterExampleNeeded() {
var
a = 1;
function
oneMoreThanA() {
return
a + 1;
}
return
oneMoreThanA();
}
This provides a
great deal of utility in writing more maintainable code. If a
function relies on one or two other functions that are not useful to
any other part of your code, you can nest those utility functions
inside the function that will be called from elsewhere. This keeps
the number of functions that are in the global scope down, which is
always a good thing.
This is also a
great counter to the lure of global variables. When writing complex
code it is often tempting to use global variables to share values
between multiple functions - which leads to code that is hard to
maintain. Nested functions can share variables in their parent, so
you can use that mechanism to couple functions together when it makes
sense without polluting your global namespace - 'local globals' if
you like. This technique should be used with caution, but it's a
useful ability to have.
Closures
This leads us to
one of the most powerful abstractions that JavaScript has to offer -
but also the most potentially confusing. What does this do?
function
makeAdder(a) {
return
function(b) {
return
a + b;
}
}
x
= makeAdder(5);
y
= makeAdder(20);
x(6)
?
y(7)
?
The name of the
makeAdder
function should give it away: it creates new 'adder' functions, which
when called with one argument add it to the argument that they were
created with.
What's happening
here is pretty much the same as was happening with the inner
functions earlier on: a function defined inside another function has
access to the outer function's variables. The only difference here is
that the outer function has returned, and hence common sense would
seem to dictate that its local variables no longer exist. But they do
still exist - otherwise the adder functions would be unable to work.
What's more, there are two different "copies" of
makeAdder's
local variables - one in which a
is 5 and one in which a
is 20.
Here's what's
actually happening. Whenever JavaScript executes a function, a
'scope' object is created to hold the local variables created within
that function. It is initialised with any variables passed in as
function parameters. This is similar to the global object that all
global variables and functions live in, but with a couple of
important differences: firstly, a brand new scope object is created
every time a function starts executing, and secondly, unlike the
global object (which in browsers is accessible as window) these scope
objects cannot be directly accessed from your JavaScript code. There
is no mechanism for iterating over the properties of the current
scope object for example.
So when
makeAdder
is called, a scope object is created with one property: a,
which is the argument passed to the makeAdder
function. makeAdder
then returns a newly created function. Normally JavaScript's garbage
collector would clean up the scope object created for makeAdder
at this point, but the returned function maintains a reference back
to that scope object. As a result, the scope object will not be
garbage collected until there are no more references to the function
object that makeAdder
returned.
Scope objects
form a chain called the scope chain, similar to the prototype chain
used by JavaScript's object system.
A closure is the
combination of a function and the scope object in which it was
created.
Closures let you
save state - as such, they can often be used in place of objects.
Memory leaks
An unfortunate
side effect of closures is that they make it trivially easy to leak
memory in Internet Explorer. JavaScript is a garbage collected
language - objects are allocated memory upon their creation and that
memory is reclaimed by the browser when no references to an object
remain. Objects provided by the host environment are handled by that
environment.
Browser hosts
need to manage a large number of objects representing the HTML page
being presented - the objects of the DOM.
It is up to the browser to manage the allocation and recovery of
these.
Internet
Explorer uses its own garbage collection scheme for this, separate
from the mechanism used by JavaScript. It is the interaction between
the two that can cause memory leaks.
A memory leak in
IE occurs any time a circular reference is formed between a
JavaScript object and a native object. Consider the following:
function
leakMemory() {
var
el = document.getElementById('el');
var
o = { 'el': el };
el.o
= o;
}
The circular
reference formed above creates a memory leak; IE will not free the
memory used by el
and o
until the browser is completely restarted.
The above case
is likely to go unnoticed; memory leaks only become a real concern in
long running applications or applications that leak large amounts of
memory due to large data structures or leak patterns within loops.
Leaks are rarely
this obvious - often the leaked data structure can have many layers
of references, obscuring the circular reference.
Closures make it
easy to create a memory leak without meaning to. Consider this:
function
addHandler() {
var
el = document.getElementById('el');
el.onclick
= function() {
this.style.backgroundColor
= 'red';
}
}
The above code
sets up the element to turn red when it is clicked. It also creates a
memory leak. Why? Because the reference to el
is inadvertently caught in the closure created for the anonymous
inner function. This creates a circular reference between a
JavaScript object (the function) and a native object (el).
There are a
number of workarounds for this problem. The simplest is this:
function
addHandler() {
var
el = document.getElementById('el');
el.onclick
= function() {
this.style.backgroundColor
= 'red';
}
el
= null;
}
This works by
breaking the circular reference.
Surprisingly,
one trick for breaking circular references introduced by a closure is
to add another closure:
function
addHandler() {
var
clickHandler = function() {
this.style.backgroundColor
= 'red';
}
(function()
{
var
el = document.getElementById('el');
el.onclick
= clickHandler;
})();
}
The inner
function is executed straight away, and hides its contents from the
closure created with clickHandler.
Another good
trick for avoiding closures is breaking circular references during
the window.onunload
event. Many event libraries will do this for you. Note that doing so
disables bfcache
in Firefox 1.5, so you should not register an unload
listener in Firefox, unless you have other reasons to do so. __
* First published on developer.mozilla.org * Copyright: © 2006 Simon Willison, contributed under the Creative Commons: Attribute-Sharealike 2.0 license.
|