JavaScript 函数别名似乎不起作用

IT技术 javascript function closures alias
2021-01-12 14:19:46

我只是在阅读这个问题并想尝试别名方法而不是函数包装器方法,但我似乎无法让它在 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 中有效。

6个回答

我深入了解这种特殊行为,我想我找到了一个很好的解释。

在我开始讨论为什么不能使用 alias 之前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现在也会返回200getAgedave


以上所有内容的意义何在?函数“松散地”附加到您的 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方法是使用daveasthis对象调用的,而 JavaScript 无法确定ageMethod执行的“范围” 所以它通过全局“Window”作为“this”。现在由于window没有birthDate属性,ageMethod执行将失败。

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

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

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

var $ = document.getElementById;

$('someElement'); 

$windowas调用,this如果getElementById实现预期thisdocument,它将失败。

再次解决这个问题,你可以这样做

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

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

我不知道getElementById在 IE 中的内部实现,但是 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 方法获取yogiobject asthis并输出100


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

JavaScript 解释器将windowobject设置为 asthis并且我们的getAge方法将返回-1.


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

如果我们设置了正确的范围,则可以使用ageAlias方法。


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

如果我们传入其他 person 对象,它仍然会正确计算年龄。

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

我在 stackoverflow 上看到的最好的答案之一。
2021-03-18 14:19:46
+1 了解 IE 的工作原理。其余的有点离题,但总的来说还是有帮助的。:)
2021-03-19 14:19:46
优秀的答案。不过,我看不出其中有什么是题外话。
2021-03-22 14:19:46
非常好的答案,确实。这里一个较短的版本
2021-03-22 14:19:46
为什么它在 IE 中工作?因为:stackoverflow.com/questions/6994139/...——所以这个方法实现实际上可能是{return window[id]},所以它可以在任何上下文中工作
2021-03-25 14:19:46

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

>>> $ = 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 年,bind来自 ES5的新方法允许我们以一种更有趣的方式做到这一点:

>>> $ = document.getElementById.bind(document)
>>> $('bn_home')
<body id="bn_home" onload="init();">
这实际上是一个包装器,因为它返回一个新函数。我认为 Kev 问题的答案是,由于上述原因,这是不可能的。您在此处描述的方法可能是最佳选择。
2021-03-15 14:19:46
感谢您的评论,我没有仔细观察以意识到这一点。那么,另一个问题的答案中的语法完全是假的?有趣的...
2021-04-05 14:19:46

这是一个简短的答案。

下面复制(引用)该函数。问题是现在函数在window对象上,当它被设计为存在于document对象上时。

window.myAlias = document.getElementById

替代方案是

  • 使用包装器(Fabien Ménager 已经提到)
  • 或者您可以使用两个别名。

    window.d = document // A renamed reference to the object
    window.d.myAlias = window.d.getElementById
    

另一个简短的答案,仅用于包装/别名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

除了其他很棒的答案之外,还有简单的 jQuery 方法$.proxy

你可以这样别名:

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

或者

myAlias = $.proxy(document.getElementById, document);
+1 因为这是一个可行的解决方案。但是,严格来说,幕后$.proxy其实也是一个包装。
2021-03-20 14:19:46