为什么在JavaScript中弃用了arguments.callee.caller属性?

在JavaScript中添加并弃用了该属性,但ECMAScript完全省略了该属性。某些浏览器(Mozilla,IE)一直都支持该浏览器,并且在地图上没有删除支持的计划。其他(Safari,Opera)已采用了对此功能的支持,但对较旧浏览器的支持并不可靠。

是否有充分的理由将这种有价值的功能置于困境?

(或者,是否有更好的方法来抓住调用函数的句柄?)

评论

它受其他浏览器支持,因为任何获得广泛使用的功能都将成为其他浏览器的兼容性错误。如果某个网站使用的功能仅在一个浏览器中存在,则该网站在所有其他浏览器中都已损坏,并且通常用户认为浏览器已损坏。

(几乎所有浏览器都一次或多次执行此操作,例如,此功能(和JS本身)来自Netscape,源自IE的XHR,来自Safari的Canvas等。其中一些很有用,并被其他浏览器采用随着时间的流逝(js,canvas,xhr都是示例),有些(.callee)则不是。

@olliej您对支持它的评论是正确的,因为它被使用而不是因为它是一个标准(或者即使它已在标准中弃用)。因此,每当我觉得标准没有帮助我时,我便开始几乎不理会这些标准。作为开发人员,我们可以通过使用有效的方法来规范标准的方向,而不是使用规范说明应该做什么。这就是我们取回的方式(是的,它们在某一时刻已被弃用)。

#1 楼

早期版本的JavaScript不允许使用命名函数表达式,因此,我们无法创建递归函数表达式:

 // This snippet will work:
 function factorial(n) {
     return (!(n>1))? 1 : factorial(n-1)*n;
 }
 [1,2,3,4,5].map(factorial);


 // But this snippet will not:
 [1,2,3,4,5].map(function(n) {
     return (!(n>1))? 1 : /* what goes here? */ (n-1)*n;
 });


为了解决这个问题,添加了arguments.callee我们可以做到:

 [1,2,3,4,5].map(function(n) {
     return (!(n>1))? 1 : arguments.callee(n-1)*n;
 });


然而,这实际上是一个非常糟糕的解决方案,因为它(与其他参数,被调用方和调用方问题一起使用)使内联和尾递归变得不可能在一般情况下(您可以通过跟踪等方式在特定情况下实现此目标,但由于检查本来就不需要最佳代码,因此即使是最佳代码也不太理想)。另一个主要问题是递归调用将获得不同的this值,例如:

var global = this;
var sillyFunction = function (recursed) {
    if (!recursed)
        return arguments.callee(true);
    if (this !== global)
        alert("This is: " + this);
    else
        alert("This is the global");
}
sillyFunction();


无论如何,EcmaScript 3通过允许命名函数表达式解决了这些问题,例如:

 [1,2,3,4,5].map(function factorial(n) {
     return (!(n>1))? 1 : factorial(n-1)*n;
 });


这有很多好处:


可以像在代码内部一样调用该函数。
它不会污染名称空间。
this的值不会改变。
性能更高(访问参数对象很昂贵)。


>
刚刚意识到,除了其他所有问题之外,问题还与arguments.callee.caller有关,或更确切地说与Function.caller有关。

在任何时间点,您都可以找到堆栈中任何函数的最深层调用者,就像我在上面说的,查看调用栈有一个主要的影响:它使得不可能进行大量优化,甚至变得更加困难。

例如。如果我们不能保证函数f不会调用未知函数,则无法内联f。基本上,这意味着任何可能微不足道的呼叫站点都会聚集大量警卫,请采取以下措施:

 function f(a, b, c, d, e) { return a ? b * c : d * e; }


如果js解释器不能保证在调用时提供的所有参数都是数字,则它需要在内联代码之前插入对所有参数的检查,或者不能内联函数。

现在,在这种特殊情况下,智能解释器应该能够重新排列检查以使其更优化,而不检查任何不使用的值。但是,在很多情况下这是不可能的,因此无法内联。

评论


您是说它仅仅因为难以优化而被贬低了吗?真是愚蠢。

–托马斯·埃丁
2010年8月17日在23:57



不,我列举了许多原因,除了使其难以优化之外(尽管从历史上看,难以优化的事物还具有人们难以理解的语义)

– olliej
2010年8月27日在9:23

此参数有点虚假,如果重要,则可以通过调用设置其值。通常不使用它(至少,我在递归函数中从未遇到过问题)。通过名称调用函数与此有相同的问题,因此我认为与被调用者的好坏无关。而且,仅在严格模式下“弃用”被呼叫者和呼叫者(ECMAscript编辑,2009年12月5日),但是我猜这在olliej于2008年发布时并不为人所知。

–RobG
2011年5月27日,3:24



我仍然看不到逻辑。在具有一流功能的任何语言中,能够定义一个可以引用自身而不需要知道自己的功能体就具有明显的价值。

–马克·里德(Mark Reed)
2011-10-20 19:41

RobG指出了这一点,但我认为还不是很清楚:如果是全局作用域,则使用命名函数进行递归将仅保留其值。在所有其他情况下,此值将在第一次递归调用后发生更改,因此我认为您的答案中涉及到此内容保留部分实际上并不是有效的。

– JLRishe
2014年1月22日下午13:31

#2 楼

尽管不使用arguments.callee.caller,但它确实使用了Function.caller属性。 (arguments.callee只会为您提供当前功能的参考)



Function.caller尽管根据ECMA3是非标准的,但已在所有当前的主要浏览器中实现。 >
不赞成使用arguments.caller,而建议使用Function.caller,并且在当前的某些主要浏览器(例如Firefox 3)中未实现。

所以情况不太理想,但是如果要在所有主要浏览器上访问Java调用函数时,都可以使用Function.caller属性,该属性可以直接在命名函数引用上访问,也可以通过arguments.callee属性从匿名函数内部访问。

评论


这是关于什么是不推荐使用的最佳解释,非常有用。有关Function.caller无法执行的一个很好的示例(获取递归函数的堆栈跟踪),请参阅developer.mozilla.org/en/JavaScript/Reference/Global_Objects/…

–胡安·门德斯(Juan Mendes)
2011年11月9日19:18



但是,严格模式下禁止arguments.callee。这让我伤心过,但最好不再使用它。

–Gras Double
2015年4月14日23:25



您必须指向MDN的arguments.callee超链接表示已在严格模式下将其删除。那与弃用不一样吗?

–样式
17年4月11日在20:19



请注意,在ES5严格模式下不推荐使用arguments.callee.caller:“不推荐使用的另一个功能是arguments.callee.caller,或更具体地说是Function.caller。” (资源)

– thdoan
18年5月16日在10:42



#3 楼

与参数相比,使用命名函数更好。callee:

 function foo () {
     ... foo() ...
 }




 function () {
     ... arguments.callee() ...
 }


命名函数将通过调用者属性访问其调用者:

 function foo () {
     alert(foo.caller);
 }


,它比

 function foo () {
     alert(arguments.callee.caller);
 }


>弃用是由于当前的ECMAScript设计原则。

评论


您能描述为什么使用命名函数更好。永远不需要在匿名函数中使用被调用者吗?

–AnthonyWJones
08年9月19日在18:49

如果您在匿名函数中使用被调用方,则您所拥有的函数不应为匿名函数。

– Prestaul
08-09-19在19:42

有时,最简单的调试方法是使用.caller()。在这种情况下,命名函数将无济于事-您试图确定哪个函数正在执行调用。

– SamGoody
2010-12-13 11:02



定义更好。例如,当arguments.callee工作时,IE6-8命名了函数怪癖。

– cmc
2012年1月21日,下午2:27

除了IE6-8的怪癖之外,它还使代码紧密耦合。如果对象和/或函数的名称是硬编码的,则正如ardsasd和rsk82所述,存在重大的重构危险,这种危险仅随着代码库大小的增加而增加。单元测试是一道防线,我使用了它们,但它们仍然不是一个真正使我个人满意的硬编码问题答案。

–茉莉·黑格曼(Jasmine Hegman)
13年5月1日18:43

#4 楼

只是一个扩展。递归期间“ this”的值会更改。在以下(经过修改的)示例中,阶乘获取{foo:true}对象。

[1,2,3,4,5].map(function factorial(n) {
  console.log(this);
  return (!(n>1))? 1 : factorial(n-1)*n;
},     {foo:true}     );


首次调用的阶乘获取对象,但是对于递归而言并非如此电话。

评论


因为您做错了。如果需要保持这种状态,请写我在写递归代码时发现的factorial.call(this,n-1),通常没有,或者它指向树中的某个节点,实际上很好变化。

– Stijn de Witt
17年4月17日在20:33