JavaScript函数别名似乎不起作用


85

我只是在阅读此问题,想尝试使用别名方法,而不是使用功能包装器方法,但是我似乎无法使其在Firefox 3或3.5beta4或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中使用。


1
在IE8中-可能是某种魔术功能或其他某种功能。
MaciejŁebkowski09年

我在stackoverflow.com/questions/954417的错误答案中添加了评论,并将其定向到此处。
格兰特·瓦格纳

1
对于支持Function.prototype.bind方法的浏览器:window.myAlias = document.getElementById.bind(document); 搜索MDN文档,将有一个绑定补片。
本·弗莱明

Answers:


39

您必须将该方法绑定到文档对象。看:

>>> $ = 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年,bindES5提供了一种新方法,可以让我们以一种更奇妙的方式做到这一点:

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

4
这实际上是一个包装器,因为它返回了一个新函数。我认为对Kev问题的答案是,由于您上述原因,这是不可能的。您在此处描述的方法可能是最佳选择。
吉吉

感谢您的评论,我没有足够仔细地意识到这一点。那么,另一个问题的答案中的语法完全是伪造的吗?有趣的...
Kev

188

我深入了解了这种特殊行为,并认为我找到了很好的解释。

在介绍为什么您不能使用别名之前document.getElementById,我将尝试解释JavaScript函数/对象的工作方式。

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

考虑以下功能:

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

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

该函数在Window范围内声明,当您调用它时this,sum函数内部的值将成为全局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函数,因此它将设置thisdave并调用该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.

在上一行中,您不是将JavaScript决定范围,而是手动将范围作为bob对象传递。getAge现在,200即使您已“调用”getAge了该dave对象,它也会返回。


以上所有的意思是什么?函数被“宽松地”附加到您的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没有birthDate属性,ageMethod执行将失败。

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

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

以上所有都有意义吗?如果是这样,那么您将能够解释为什么您不能使用别名document.getElementById

var $ = document.getElementById;

$('someElement'); 

$windowas一起调用,this并且如果getElementById期望this实现document,它将失败。

再次解决此问题,您可以执行

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

那么为什么它可以在Internet Explorer中工作呢?

我不知道getElementByIdIE中的内部实现,但是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对象asthis并输出100


var ageAlias = yogi.getAge;
console.log(ageAlias()); //Output: -1

JavaScript解释器将windowobject设置为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

您仍然可以将另一个Person对象传递给getAgeSmartest。:)


7
+1为何IE可以运作。其余的内容有点题外话,但总体上还是有用的。:)
Kev

44
极好的答案。我看不出有什么偏离主题的。
贾斯汀·约翰逊

确实,这是一个很好的答案。这里一个短一些的版本。
Marcel Korpel 2010年

8
我在stackoverflow上看到的最好的答案之一。
莺2011年

为什么它在IE中有效?因为:stackoverflow.com/questions/6994139/…-因此此方法的实现实际上可能是{return window [id]},所以它可以在任何上下文中使用
MaciejŁebkowski15年

3

这是一个简短的答案。

下面是该函数的副本(对其的引用)。问题在于,现在设计功能window时,该功能位于document对象上。

window.myAlias = document.getElementById

替代品是

  • 使用包装纸(FabienMénager已经提到过)
  • 或者您可以使用两个别名。

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

如果您希望将日志记录参数包装在一个数组中(大部分时间我都这样做),请替换.apply(...).call(...)

应与工作console.log()console.debug()console.info()console.warn()console.error()。另请参见consoleMDN


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”,就可以“纯别名”一个函数。因此,这取决于。
解决方案

抱歉,我的意思是在getElementById的上下文中。你是对的。我已略微更改了答案以反映这一点……
Kev
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.