JavaScript函数别名似乎无法正常工作

91

我刚刚在阅读这个问题,并想尝试别名方法而不是函数包装器方法,但无论是在Firefox 3或3.5beta4,还是Google Chrome中,都无法在它们的调试窗口和测试网页中使其正常工作。

Firebug:

>>> 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中可以正常工作。


1
在IE8中——可能是某种神奇的函数或其他什么东西。 - Maciej Łebkowski
我已经在https://dev59.com/6HNA5IYBdhLWcg3wdthd的错误答案下添加了评论,并将它们指向了这里。 - Grant Wagner
2
对于支持Function.prototype.bind方法的浏览器:window.myAlias = document.getElementById.bind(document);在MDN文档中搜索,那里会有一个绑定shim。 - Ben
6个回答

194

我认真研究了这种特定的行为,并认为我找到了一个很好的解释。

在深入讨论为什么你不能别名document.getElementById之前,我将尝试解释一下JavaScript函数/对象的工作原理。

每当调用JavaScript函数时,JavaScript解释器确定一个作用域并将其传递给该函数。

考虑以下函数:

function sum(a, b)
{
    return a + b;
}

sum(10, 20); // returns 30;

该函数在Window作用域中声明,当您调用它时,sum函数内部的this值将是全局Window对象。

对于'sum'函数而言,'this'的值并不重要,因为它并未使用它。


考虑以下函数:

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.

当你调用dave.getAge函数时,JavaScript解释器会看到你在调用dave对象上的getAge函数,所以它将this设置为dave并调用getAge函数。getAge()将正确返回100


你可能知道在JavaScript中可以使用apply方法指定作用域。让我们试一下。

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.

在上述代码行中,你手动传递了作用域 bob 对象,而不是让 JavaScript 自己决定。现在,即使你“认为”是在 dave 对象上调用了 getAge,它也将返回 200
以上内容的重点是什么?函数与 JavaScript 对象之间是“松散”连接的。例如,你可以这样做:
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

让我们迈出下一步。

var dave = new Person(new Date(1909, 1, 1));
var ageMethod = dave.getAge;

dave.getAge(); //returns 100;
ageMethod(); //returns ?????

ageMethod 方法执行时抛出了一个错误!发生了什么?

如果你仔细阅读我上面的内容,你会注意到 dave.getAge 方法被调用时使用 dave 作为 this 对象,而 JavaScript 无法确定 ageMethod 执行时的作用域。因此它将全局的 'Window' 作为 'this' 传递。由于 window 没有一个叫做 birthDate 的属性,所以 ageMethod 执行会失败。

如何解决这个问题?很简单,

ageMethod.apply(dave); //returns 100.

以上内容是否清晰易懂呢?如果是,接下来你就能够解释为什么不能给document.getElementById起一个别名:

var $ = document.getElementById;

$('someElement'); 

$ 作为函数被调用时,window 作为 this 参数传入。如果 getElementById 的实现期望 thisdocument,那么它将失败。

再次强调,为了解决这个问题,可以执行以下操作:

$.apply(document, ['someElement']);

为什么它在Internet Explorer中能够工作?
我不清楚IE中getElementById的内部实现,但是jQuery源代码(inArray方法实现)中的一条注释说,在IE中,window == document。如果是这样的话,那么给document.getElementById取一个别名在IE中应该可以工作。
为了进一步说明这一点,我创建了一个详细的示例,请查看下面的Person函数。
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();
    };

}

对于上述的Person函数,以下是各种getAge方法的行为。
我们使用Person函数创建两个对象。
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.

直截了当,getAge方法将yogi对象作为this参数,并输出100
var ageAlias = yogi.getAge;
console.log(ageAlias()); //Output: -1

JavaScript解释器将window对象设置为this,因此我们的getAge方法将返回-1


console.log(ageAlias.apply(yogi)); //Output: 100

如果我们设置了正确的作用域,您可以使用ageAlias方法。
console.log(ageAlias.apply(anotherYogi)); //Output: 200

如果我们传入其他人的对象,它仍然能正确计算年龄。
var ageSmarterAlias = yogi.getAgeSmarter;    
console.log(ageSmarterAlias()); //Output: 100

ageSmarter函数捕获了原始的this对象,因此现在您无需担心提供正确的作用域。


console.log(ageSmarterAlias.apply(anotherYogi)); //Output: 100 !!!
ageSmarter 的问题在于您无法将其作用域设置为其他对象。
var ageSmartestAlias = yogi.getAgeSmartest;
console.log(ageSmartestAlias()); //Output: 100
console.log(ageSmartestAlias.apply(document)); //Output: 100

ageSmartest 函数如果提供了无效的作用域,将使用原始的作用域。


console.log(ageSmartestAlias.apply(anotherYogi)); //Output: 200

你仍然能够向 getAgeSmartest 方法传递另一个 Person 对象。 :)

7
为什么IE有效,给个赞。其他内容有些离题但总体还是有帮助的。 :) - Kev
44
很好的回答。但我不知道它怎么会与主题无关。 - Justin Johnson
确实是非常好的答案。这里有一个稍微简短一些的版本:链接 - Marcel Korpel
9
在stackoverflow上看到的最好的答案之一。 - warbaker
为什么它在IE中能够工作?因为:https://dev59.com/q1rUa4cB1Zd3GeqPgRJ7 — 因此,这种方法的实现实际上可能是{ return window[id] },因此它可以在任何上下文中工作。 - Maciej Łebkowski
在阅读这篇文章后,我对软件工程的热情被重新点燃了。 - mn.

43
你需要将该方法绑定到文档对象。看这里:

你需要将该方法绑定到文档对象。看这里:

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

当您进行简单的别名时,函数会在全局对象上调用,而不是在文档对象上调用。使用一种称为闭包的技术来解决这个问题:

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

这样你就不会失去对原始对象的引用。

在2012年,ES5中新增了bind方法,使我们能够以更加高级的方式实现此操作:

>>> $ = document.getElementById.bind(document)
>>> $('bn_home')
<body id="bn_home" onload="init();">

4
这个"wrapper"实际上是返回一个新函数。我认为对于Kev的问题,由于你之前描述的原因,这是不可能的。你在这里描述的方法可能是最佳选择。 - jiggy
谢谢您的评论,我没有仔细看出来。那么,其他问题答案中的语法完全是虚假的吗?有趣... - Kev

3

这是一个简短的回答。

以下代码复制了(引用)该函数。问题在于,现在该函数位于window对象上,而它原本设计为存在于document对象上。

window.myAlias = document.getElementById

替代方案有:
  • 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
    

2
另一个简短的答案,仅用于包装/别名console.log和类似的日志记录方法。它们都期望在console上下文中。
当使用一些回退选项来包装console.log时,这是可用的,以防您或您的用户在使用不支持它(总是)的浏览器时遇到问题。然而,这不是该问题的完整解决方案,因为需要扩展检查和回退 - 您的行驶里程可能会有所不同。
示例使用警告
var warn = function(){ console.warn.apply(console, arguments); }

然后像平常一样使用它。
warn("I need to debug a number and an object", 9999, { "user" : "Joel" });

如果您更喜欢将日志参数包装在数组中(我大多数时候都是这样做的),请使用.call(...)替换.apply(...)
应该适用于console.log()console.debug()console.info()console.warn()console.error()。另请参见MDN上的console

1
除了其他很好的答案之外,还有一个简单的jQuery方法$.proxy
你可以像这样设置别名:
myAlias = $.proxy(document, 'getElementById');

或者

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

+1 是因为这是一个可行的解决方案。但严格来说,$.proxy 在幕后也是一个包装器。 - pimvdb

-6

实际上,您无法在预定义对象上“纯粹别名”函数。因此,如果不进行包装,最接近别名的方法是保持在同一对象内:

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

5
这个说法稍有不准确。只要函数实现中没有用到'this'关键字,就可以进行“纯别名”操作。因此这取决于具体情况。 - SolutionYogi
抱歉,我是指在getElementById的上下文中。你是对的。我稍微修改了答案以反映这一点... - Kev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接