JavaScript function aliasing doesn't seem to work

Kev picture Kev · Jun 17, 2009 · Viewed 26.8k times · Source

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.

Answer

SolutionYogi picture SolutionYogi · Jul 22, 2009

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. :)