Note: As of ES2015, JavaScript arguably got methods (and that is the term the spec uses) via a new notation on object initializers and via the
class syntax. This article predates ES2015 and refers to non-method properties on prototypes, created with the
function keyword (
the new method syntax doesn't use the
function keyword).
We frequently talk about JavaScript objects having
methods. This is just a convenient myth. JavaScript has functions, but it doesn't have
methods. It doesn't need them. Its functions, combined with some syntactic sugar, are more than up to the job.
What is a "method"? I'd have to say that the
definition given on Wikipedia is pretty good. As of this writing, it says a method is
...a subroutine that is exclusively associated either with a class...or with an object...
Yeah, JavaScript doesn't have any of those. Granted it
seems to have them. For example:
function Guess(killer, location, weapon)
{
this.killer = killer;
this.location = location;
this.weapon = weapon;
}
Guess.prototype.accuse = function()
{
alert('It was ' + this.killer +
' in the ' + this.location +
' with the ' + this.weapon +
'!');
};
function testGuess()
{
var mustardStudyLeadPipe;
var plumHallRope;
mustardStudyLeadPipe = new Guess(
'Colonel Mustard',
'study',
'lead pipe');
mustardStudyLeadPipe.accuse();
plumHallRope = new Guess(
'Professor Plum',
'hall',
'rope');
plumHallRope.accuse();
}
Running
testGuess
does indeed show us
"It was Colonel Mustard in study with the lead pipe!"
and then
"It was Professor Plum in hall with the rope!"
so that looks an awful lot like
accuse
is a method of
Guess
objects.
Except it isn't. Let's add a bit to our
testGuess
function (new bits in bold):
function testGuess()
{
var mustardStudyLeadPipe;
var plumHallRope;
var accuse;
mustardStudyLeadPipe = new Guess(
'Colonel Mustard',
'study',
'lead pipe');
mustardStudyLeadPipe.accuse();
plumHallRope = new Guess(
'Professor Plum',
'hall',
'rope');
plumHallRope.accuse();
accuse = mustardStudyLeadPipe.accuse;
accuse();
}
What does the final call to
accuse
show? Does it accuse Colonel Mustard? No, all we've done is get a reference to the
accuse
function into our local variable, there's nothing there (or in the function definition) that refers to the
mustardStudyLeadPipe
instance or indeed anything related to
Guess
. No, what this shows will depend on the HTML document in which it's found, but it'll be something along these lines:
"It was undefined in the http://blog.niftysnippets.org with the undefined!"
Why? Because we didn't do anything to define "this" within the function. (I'll come back to details on why we'd get seemingly-odd alert that includes a URL in a bit.)
There are three main things about JavaScript that make it seem to have methods (leaving aside prototypes and "classes" for the moment): The "this" keyword, the fact that object properties can refer to functions (since they are, after all, just objects), and the fact that when you call a function using an expression that gets the function reference from an object property (e.g.,
object.functionName()
or
object['functionName']()
), the object is set automatically as "this" within the function call.
Let's look at each of those.
The "this" keyword: This keyword looks familiar if you're coming to JavaScript from a background in C++, Java, C#, and the like, where "this" within a method is guaranteed to refer to an instance of the class defining the method (or a subclass). And in JavaScript, "this" does refer to an object instance, but that's where the similarity ends. Other than the name being the same and it referencing an object, it bears no relation to the "this" keyword in class-based languages like C++, Java, or C#. In fact, in many ways, "this" is actually just a function argument that is supplied in a non-obvious (but convenient!) way.
Functions as properties: Let's look again at one line from the code above:
mustardStudyLeadPipe.accuse();
If we were talking about that line of code, we'd probably say "...the
accuse
method of the
mustardStudyLeadPipe
object...", which is a useful and convenient way to put it. A more painstakingly-geeky-accurate way of putting it, though, would be "...the function referenced by the
accuse
property of the
mustardStudyLeadPipe
object..." Not that anyone's going to say that, but that's really what's going on. Objects don't have methods, they have properties; it's just that a property can refer to a function, since functions are objects like everything else.
Functions called via property references get "this" set for them: This is the part that really makes it seem like JavaScript has methods: The "this" reference gets set automagically to the object instance if you call a function via a property reference. Let's look at that call again:
mustardStudyLeadPipe.accuse();
This does three
completely distinct things: Firstly, it identifies a function to call by getting the function reference from an object property; secondly, it says to call the function and return its result rather than just get a reference to it (the parentheses do that); and thirdly, it says that within that call, use the object in question as "this". These completely distinct things are combined in that notation for our convenience. Getting the function reference from an object property doesn't link it in any way to the object the property came from (as our
accuse
test above confirmed), it just gets a reference the function; the JavaScript engine treats calls of functions just retrieved from object properties as special and sets up "this" accordingly, but it has nothing to do with the function being called.
Lets prove that another way:
function testGuess2()
{
var plumHallRope;
var fakeGuess;
plumHallRope = new Guess(
'Professor Plum',
'hall',
'rope');
plumHallRope.accuse();
fakeGuess = {};
fakeGuess.location = 'library';
fakeGuess.killer = 'Mrs. Peacock';
fakeGuess.weapon = 'revolver';
fakeGuess.demo = plumHallRope.accuse;
fakeGuess.demo();
}
This accuses Professor Plum as before, and then:
"It was Mrs. Peacock in the library with the revolver!"
The exact same function produces the alerts in both cases, it's purely the way it was called that defined "this". The
fakeGuess
object isn't even really a
Guess
-- "
fakeGuess instanceof Guess
" returns false -- but it has all of the properties the function expects (killer, location, weapon), and so it works just fine (this is something we'll come back to in a later post). Essentially, "this" is just a function argument that's passed into the function as an implicit feature of calling a function via a property reference.
We can also do it explicitly. JavaScript gives us the
call and
apply methods on function instances, with which we can say explicitly what we want "this" to be. (They do the same thing, they just provide different ways to specify the function's arguments.) If you say
myfunction.call(myobject), you're saying "call
myfunction and use
myobject as 'this'", which is what the property-retrieval stuff does implicitly for you. In fact, this:
plumHallRope.accuse();
equates to
plumHallRope.accuse.call(plumHallRope);
Now all three of the parts we identified above are shown distinctly: Getting the function reference from the property (
plumHallRope.accuse
) is distinct from the fact we're calling it (
call
) is distinct from what "this" should be (
(plumHallRope)
). The fact that these are distinct can be made clearer:
var f;
f = plumHallRope.accuse;
f.call(plumHallRope);
Alternately, here's a more dramatic example:
function testGuess3()
{
var plumHallRope;
var fakeGuess;
plumHallRope = new Guess(
'Professor Plum',
'hall',
'rope');
plumHallRope.accuse();
fakeGuess = {};
fakeGuess.location = 'library';
fakeGuess.killer = 'Mrs. Peacock';
fakeGuess.weapon = 'revolver';
plumHallRope.accuse.call(fakeGuess);
}
Not to bang on about it, but the call at the end accuses Mrs. Peacock, not Professor Plum. The
plumHallRope
instance was only used to get the function reference, it wasn't used within the function call. In our original
testGuess
function, we could even do this if we wanted to:
mustardStudyLeadPipe.accuse.call(plumHallRope);
Which accuses Professor Plum. But it's more convenient, isn't it, to say
plumHallRope.accuse
?
So if "this" is really just a sort of obscure function argument, why have it? Why not just write everything as global functions and pass in the object to act on as the first argument? We could do that (and in fact I did it for years, as a C programmer), but it's more cumbersome. You have to know what functions are meant to be used with what kinds of objects, there's all sorts of stuff littering up the global namespace, etc., etc. By passing around object references, which have properties on them (usually inherited from their prototype; again the topic of an upcoming post) that reference functions intended for use with them, it's just so much more convenient.
Okay, but what was that about that URL showing up earlier when we called
accuse
? You'll remember earlier when we ran this code:
accuse = mustardStudyLeadPipe.accuse;
accuse();
we got the alert with the URL in it. So why did that happen, and where did the URL come from? It was because we didn't give an object to use for "this", and so the function call got the default, which is the JavaScript global object. In browser implementations, the global object is the
window
object -- so we were showing the values of window.killer, window.location, and window.weapon. In a typical situation,
window.killer
and
window.weapon
will be undefined, and of course
window.location
is the URL of the document in the window.
I chose that example intentionally, because it's a pitfall people fall into a lot: Losing "this". Usually, people lose "this" in the context of an event handler -- e.g., they have an instance (
plumHallRope
, perhaps), and want to hook up a "method" on it (say,
accuse
) to an event (a click handler for a button, maybe?), and so understandably they do something like this (I'm again using convenience syntax, as I describe
here):
Event.observe('theButton', plumHallRope.accuse); // WRONG
The problem being, that just references the
function, nothing about its context. Its context will be determined by how it's called. Earlier when we called
accuse
directly, because we didn't do that via property retrieval or
call
or
apply
, "this" was the global object; event handlers like this one will usually get "this" set to the element (although not if you're using old DOM0 -- onclick attribute -- events or IE's
attachEvent
function). To maintain its context, you have to do something like this:
Event.observe('theButton', function() { plumHallRope.accuse(); });
The function that gets called by the event handler then turns around and calls the
accuse
function such that
plumHallRope
is "this", so you've preserved "this" using a wrapper (which is also a closure;
details). (Note that thanks to a bug in Internet Explorer, that may well cause a memory leak for your IE users.
Prototype's
Event.observe()
does some things to minimize the issue; other frameworks will have other helpers along those lines.)
In Conclusion:
I've seen people deride JavaScript for having "fake" methods, but I think that's missing the point. It doesn't have fake methods, it just doesn't have methods at all. What it
does have is very powerful, flexible functions and some convenient syntactic sugar that lets us express the 90% case -- calling a function related to an object instance passing in that object instance as an argument -- in a compact and expressive way, but without limiting our use of the functions involved. By not limiting our use, we can use just about anything as a
mixin, we can use
duck typing, easily create wrapper objects to enforce or verify
contracts, etc.
Like so many things about the language, it's confusing until you grok just how simple it is, and then you start appreciating that it's simple but powerful.