JavaScript function aliasing doesn't seem to w

2018-12-31 16:09发布

问题:

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.

回答1:

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();\">


回答2:

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



回答3:

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
    


回答4:

Another short answer, just for wrapping/aliasing console.log and similar logging methods. They all expect to be in the console context.

This is usable when wrapping console.log with some fallbacks, in case you or your users have run into trouble when using a browser that doesn\'t (always) support it. This is not a full solution to that problem though, as it needs to be expanded checks and a fallback - your mileage may vary.

Example using warnings

var warn = function(){ console.warn.apply(console, arguments); }

Then use it as usual

warn(\"I need to debug a number and an object\", 9999, { \"user\" : \"Joel\" });

If you prefer to see your logging arguments wrapped in an array (I do, most of the time), substitute .apply(...) with .call(...).

Should work with console.log(), console.debug(), console.info(), console.warn(), console.error(). See also console on MDN.

console firebug



回答5:

In addition to other great answers, there\'s simple jQuery method $.proxy.

You can alias like this:

myAlias = $.proxy(document, \'getElementById\');

Or

myAlias = $.proxy(document.getElementById, document);


回答6:

You actually can\'t \"pure alias\" a function on a predefined object. Therefore, the closest to aliasing you can get without wrapping is by staying within the same object:

>>> document.s = document.getElementById;
>>> document.s(\'myid\');
<div id=\"myid\">