为什么在JavaScript中不赞成arguments.callee.caller属性?


214

为什么arguments.callee.caller在JavaScript中不推荐使用该属性?

它是在JavaScript中添加并弃用的,但ECMAScript完全省略了它。某些浏览器(Mozilla,IE)一直都支持它,并且在地图上也没有任何计划删除支持。其他浏览器(Safari,Opera)已经支持它,但是较旧的浏览器支持不可靠。

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

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


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

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

@olliej您对支持它的评论是正确的,因为使用它而不是因为它是一个标准(甚至即使它已在标准中弃用)也是如此!因此,每当我觉得标准没有帮助我时,我便开始几乎不理会这些标准。作为开发人员,我们可以通过使用有效的方法来规范标准的方向,而不是使用规范说明应该做什么。这就是我们如何得到<b><i>背部(是的,那些在一个点就被抛弃了)。
Stijn de Witt

Answers:


252

早期版本的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对象的成本很高)。

哎呀,

刚刚意识到,除了其他所有问题之外,这个问题还是关于arguments.callee.caller或更具体的问题Function.caller

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

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

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

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

现在,在这种特殊情况下,智能解释器应该能够将检查重新排列为最佳状态,而不检查任何不会使用的值。但是,在许多情况下这是不可能的,因此无法内联。


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

11
不,我列举了许多原因,除了使其难以优化之外(尽管从历史
上看,

17
说法有点假,其值可以通过调用来设置,如果它是很重要的。通常不使用它(至少,我在递归函数中从未遇到过问题)。通过名称调用函数存在相同的问题,this因此我认为与被调用方的好坏无关。此外,仅在严格模式下“弃用” 被叫呼叫方(ECMAscript ed 5,Dec 2009),但是我猜这在olliej于2008年发布时并不为人所知。
RobG

8
我仍然看不到逻辑。在拥有一流的功能的任何一种语言,我们在能够定义一个函数体可以引用本身,而不必知道我是明确的价值
马克·里德

8
RobG指出了这一点,但我认为还不是很清楚:使用命名函数进行递归只会保留thisif this是全局范围的值。在其他所有情况下,第一次递归调用后的值this 都会改变,因此我认为您的答案中涉及保留的this部分并不是真正有效的。
JLRishe 2014年

89

arguments.callee.caller不是过时了,但它确实利用的特性。(只会为您提供当前功能的参考)Function.callerarguments.callee

  • Function.caller,尽管根据ECMA3是非标准的,但已在所有当前的主流浏览器中实现
  • arguments.caller 弃用,取而代之的,而不是在一些当前主要的浏览器(如Firefox 3中)来实现。Function.caller

因此情况并不理想,但是如果要跨所有主要浏览器访问Java中的调用函数,则可以使用该属性,该属性可以直接在命名函数引用上访问,也可以通过该属性从匿名函数内部访问。Function.callerarguments.callee


5
这是什么是不推荐使用的最好的解释,非常有用。对于什么Function.caller不能做(让递归函数的堆栈跟踪)一个很好的例子,看到developer.mozilla.org/en/JavaScript/Reference/Global_Objects/...
胡安·门德斯

1
虽然,arguments.callee在严格模式下是禁止的。这让我伤心过,但最好不再使用它。
Gras Double

1
您必须具有MDN 的arguments.callee超链接表示它已在严格模式下删除。那与弃用不一样吗?
styfle

1
请注意,arguments.callee.caller在ES5严格模式下已弃用:“已弃用的另一个功能是arguments.callee.caller,或更具体地说是Function.caller。” (来源
thdoan

29

使用命名函数比使用arguments.callee 更好:

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

胜过

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

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

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

哪个比

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

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


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

27
如果您在匿名函数中使用被调用者,则您所拥有的函数不应为匿名函数。
Prestaul's

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

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

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

0

只是一个扩展。递归期间“ 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}     );

第一次调用的阶乘会获取对象,但对于递归调用而言并非如此。


1
因为您做错了。如果this需要维护,请写factorial.call(this, n-1)我实际上在编写递归代码时发现,通常没有this,或者this引用树中的某个节点,并且更改它实际上是一件好事。
Stijn de Witt
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.