JavaScript 函数别名似乎不起作用

我正在阅读 这个问题,想尝试别名方法而不是函数包装器方法,但我似乎无法让它在 Firefox 3或3.5 beta 4或 Google Chrome 中工作,无论是在调试窗口还是在测试网页中。

萤火虫:

>>> window.myAlias = document.getElementById
function()
>>> myAlias('item1')
>>> window.myAlias('item1')
>>> document.getElementById('item1')
<div id="item1">

如果我把它放在一个网页上,对 myAlias 的调用会给我这个错误:

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 (为清晰起见,插入 > > >) :

>>> window.myAlias = document.getElementById
function getElementById() { [native code] }
>>> window.myAlias('item1')
TypeError: Illegal invocation
>>> document.getElementById('item1')
<div id=?"item1">?

在测试页面中,我得到了相同的“非法调用”。

我做错什么了吗? 还有人能复制这个吗?

另外,奇怪的是,我刚刚尝试了,它在 IE8中工作。

28090 次浏览

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

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">

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

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
    

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.

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);