JavaScript function aliasing doesn't seem to work
I was just reading this question and wanted to try the alias method rather than the function-wrapper method, but I couldn't seem to get it to work in either Firefox 3 or 3.5beta4, or Google Chrome, both in their debug windows and in a test web page.
Firebug:
>>> window.myAlias = document.getElementById
function()
>>> myAlias('item1')
>>> window.myAlias('item1')
>>> document.getElementById('item1')
<div id="item1">
If I put it in a web page, the call to myAlias gives me this error:
uncaught exception: [Exception... "Illegal operation on WrappedNative prototype object" nsresult: "0x8057000c (NS_ERROR_XPC_BAD_OP_ON_WN_PROTO)" location: "JS frame :: file:///[...snip...]/test.html :: <TOP_LEVEL> :: line 7" data: no]
Chrome (with >>>'s inserted for clarity):
>>> window.myAlias = document.getElementById
function getElementById() { [native code] }
>>> window.myAlias('item1')
TypeError: Illegal invocation
>>> document.getElementById('item1')
<div id=?"item1">?
And in the test page, I get the same "Illegal invocation".
Am I doing something wrong? Can anyone else reproduce this?
Also, oddly enough, I just tried and it works in IE8.
I dug deep to understand this particular behavior and I think I have found a good explanation.
Before I get in to why you are not able to alias document.getElementById
, I will try to explain how JavaScript functions/objects work.
Whenever you invoke a JavaScript function, the JavaScript interpreter determines a scope and passes it to the function.
Consider following function:
function sum(a, b)
{
return a + b;
}
sum(10, 20); // returns 30;
This function is declared in the Window scope and when you invoke it the value of this
inside the sum function will be the global Window
object.
For the 'sum' function, it doesn't matter what the value of 'this' is as it is not using it.
Consider following function:
function Person(birthDate)
{
this.birthDate = birthDate;
this.getAge = function() { return new Date().getFullYear() - this.birthDate.getFullYear(); };
}
var dave = new Person(new Date(1909, 1, 1));
dave.getAge(); //returns 100.
When you call dave.getAge function, the JavaScript interpreter sees that you are calling getAge function on the dave
object, so it sets this
to dave
and calls the getAge
function. getAge()
will correctly return 100
.
You may know that in JavaScript you can specify the scope using the apply
method. Let's try that.
var dave = new Person(new Date(1909, 1, 1)); //Age 100 in 2009
var bob = new Person(new Date(1809, 1, 1)); //Age 200 in 2009
dave.getAge.apply(bob); //returns 200.
In the above line, instead of letting JavaScript decide the scope, you are passing the scope manually as the bob
object. getAge
will now return 200
even though you 'thought' you called getAge
on the dave
object.
What's the point of all of the above? Functions are 'loosely' attached to your JavaScript objects. E.g. you can do
var dave = new Person(new Date(1909, 1, 1));
var bob = new Person(new Date(1809, 1, 1));
bob.getAge = function() { return -1; };
bob.getAge(); //returns -1
dave.getAge(); //returns 100
Let's take the next step.
var dave = new Person(new Date(1909, 1, 1));
var ageMethod = dave.getAge;
dave.getAge(); //returns 100;
ageMethod(); //returns ?????
ageMethod
execution throws an error! What happened?
If you read my above points carefully, you would note that dave.getAge
method was called with dave
as this
object whereas JavaScript could not determine the 'scope' for ageMethod
execution. So it passed global 'Window' as 'this'. Now as window
doesn't have a birthDate
property, ageMethod
execution will fail.
How to fix this? Simple,
ageMethod.apply(dave); //returns 100.
Did all of the above make sense? If it does, then you will be able to explain why you are not able to alias document.getElementById
:
var $ = document.getElementById;
$('someElement');
$
is called with window
as this
and if getElementById
implementation is expecting this
to be document
, it will fail.
Again to fix this, you can do
$.apply(document, ['someElement']);
So why does it work in Internet Explorer?
I don't know the internal implementation of getElementById
in IE, but a comment in jQuery source (inArray
method implementation) says that in IE, window == document
. If that's the case, then aliasing document.getElementById
should work in IE.
To illustrate this further, I have created an elaborate example. Have a look at the Person
function below.
function Person(birthDate)
{
var self = this;
this.birthDate = birthDate;
this.getAge = function()
{
//Let's make sure that getAge method was invoked
//with an object which was constructed from our Person function.
if(this.constructor == Person)
return new Date().getFullYear() - this.birthDate.getFullYear();
else
return -1;
};
//Smarter version of getAge function, it will always refer to the object
//it was created with.
this.getAgeSmarter = function()
{
return self.getAge();
};
//Smartest version of getAge function.
//It will try to use the most appropriate scope.
this.getAgeSmartest = function()
{
var scope = this.constructor == Person ? this : self;
return scope.getAge();
};
}
For the Person
function above, here's how the various getAge
methods will behave.
Let's create two objects using Person
function.
var yogi = new Person(new Date(1909, 1,1)); //Age is 100
var anotherYogi = new Person(new Date(1809, 1, 1)); //Age is 200
console.log(yogi.getAge()); //Output: 100.
Straight forward, getAge method gets yogi
object as this
and outputs 100
.
var ageAlias = yogi.getAge;
console.log(ageAlias()); //Output: -1
JavaScript interepreter sets window
object as this
and our getAge
method will return -1
.
console.log(ageAlias.apply(yogi)); //Output: 100
If we set the correct scope, you can use ageAlias
method.
console.log(ageAlias.apply(anotherYogi)); //Output: 200
If we pass in some other person object, it will still calculate age correctly.
var ageSmarterAlias = yogi.getAgeSmarter;
console.log(ageSmarterAlias()); //Output: 100
The ageSmarter
function captured the original this
object so now you don't have to worry about supplying correct scope.
console.log(ageSmarterAlias.apply(anotherYogi)); //Output: 100 !!!
The problem with ageSmarter
is that you can never set the scope to some other object.
var ageSmartestAlias = yogi.getAgeSmartest;
console.log(ageSmartestAlias()); //Output: 100
console.log(ageSmartestAlias.apply(document)); //Output: 100
The ageSmartest
function will use the original scope if an invalid scope is supplied.
console.log(ageSmartestAlias.apply(anotherYogi)); //Output: 200
You will still be able to pass another Person
object to getAgeSmartest
. :)
You have to bind that method to the document object. Look:
>>> $ = document.getElementById
getElementById()
>>> $('bn_home')
[Exception... "Cannot modify properties of a WrappedNative" ... anonymous :: line 72 data: no]
>>> $.call(document, 'bn_home')
<body id="bn_home" onload="init();">
When you’re doing a simple alias, the function is called on the global object, not on the document object. Use a technique called closures to fix this:
function makeAlias(object, name) {
var fn = object ? object[name] : null;
if (typeof fn == 'undefined') return function () {}
return function () {
return fn.apply(object, arguments)
}
}
$ = makeAlias(document, 'getElementById');
>>> $('bn_home')
<body id="bn_home" onload="init();">
This way you don’t loose the reference to the original object.
In 2012, there is the new bind
method from ES5 that allows us to do this in a fancier way:
>>> $ = document.getElementById.bind(document)
>>> $('bn_home')
<body id="bn_home" onload="init();">
This is a short answer.
The following makes a copy of (a reference to) the function. The problem is that now the function is on the window
object when it was designed to live on the document
object.
window.myAlias = document.getElementById
The alternatives are
- to use a wrapper (already mentioned by Fabien Ménager)
-
or you can use two aliases.
window.d = document // A renamed reference to the object window.d.myAlias = window.d.getElementById