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

225
为什么JavaScript中的arguments.callee.caller属性被弃用?
它在JavaScript中被添加后又被弃用,但在ECMAScript中完全被省略。一些浏览器(Mozilla,IE)一直支持它,并且没有计划删除支持。其他浏览器(Safari,Opera)已经采用了对其的支持,但是旧版浏览器上的支持不可靠。
有没有一个很好的理由将这个有用的功能置于不稳定状态?
(或者说,是否有更好的方法来获取调用函数的句柄?)

2
它被其他浏览器支持,因为任何得到广泛使用的功能都会成为其他浏览器的兼容性问题。如果一个网站使用了只存在于一个浏览器中的功能,那么该网站在所有其他浏览器中都会出现故障,并且通常用户认为是浏览器出了问题。 - olliej
4
几乎所有浏览器都曾经这样做过,例如这个功能(和JS本身)来自Netscape,XHR起源于IE,Canvas在Safari中等等。其中一些是有用的并随着时间被其他浏览器采纳(JS、Canvas、XHR都是例子),而一些则不是(.callee)。 - olliej
@olliej 你关于支持它是因为它被使用而不是因为它是标准(甚至尽管它已经在标准中被弃用)的评论非常正确!这就是为什么每当我感觉标准没有帮助我的时候,我开始忽略它们。我们作为开发人员可以通过使用有效的方法来塑造标准的方向,而不是按照规范所说的去做。这就是我们如何重新得到<b>和<i>标签的(是的,这些标签曾经被弃用过)。 - Stijn de Witt
5个回答

264

早期版本的 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通过允许命名函数表达式来解决了这些问题,例如:

无论如何,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; }

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

现在在这种特殊情况下,一个聪明的解释器应该能够重新排列检查顺序,使其更优化,并且不检查任何不会使用的值。 然而,在许多情况下这是不可能的,因此无法内联。


12
你是说它被淘汰了只是因为很难优化?那有点儿荒谬。 - Thomas Eding
11
不,我列举了一些原因,除了使优化变得困难之外(尽管通常情况下历史表明,那些难以优化的事物也具有人们难以理解的语义)。 - olliej
19
“this”参数有点牵强,如果它很重要,它的值可以通过调用来设置。通常它不会被使用(至少我在递归函数中从未遇到过问题)。通过名称调用函数与“this”具有相同的问题,因此我认为这与“callee”是好还是坏无关。另外,“callee”和“caller”仅在严格模式(ECMAScript ed 5,2009年12月)中被“弃用”,但我猜这在2008年olliej发布时还不为人所知。 - RobG
9
我还是不理解这个逻辑。在任何支持一等函数的语言中,定义一个函数体并能够在其内部引用自身而无需知道其身份信息是非常有用的。 - Mark Reed
8
RobG指出了这一点,但我认为并不是很清楚:使用命名函数进行递归只会在this是全局作用域的情况下保留this的值。在所有其他情况下,this的值将在第一次递归调用后发生改变,因此我认为你回答中提到this保留的部分不太正确。 - JLRishe
显示剩余12条评论

90

arguments.callee.caller并没有被弃用,尽管它使用了Function.caller属性(arguments.callee只是给您当前函数的引用)。

  • Function.caller虽然不符合ECMA3标准,但在所有目前主流浏览器中都被实现。
  • arguments.caller已经过时,推荐使用Function.caller,并且一些主流浏览器(如Firefox 3)未实现该方法。

因此,尽管情况不太理想,但如果想要在Javascript中跨所有主流浏览器访问调用函数,则可以使用Function.caller属性,在命名函数引用上直接访问或通过arguments.callee属性在匿名函数中访问。


5
这是最好的解释什么是已弃用(deprecated)和未弃用的内容,十分有用。如果你想看Function.caller做不到什么(获取递归函数的堆栈跟踪),可以参考https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/caller这个链接。 - Ruan Mendes
2
虽然在严格模式下禁止使用arguments.callee,这让我也很难过,但最好不再使用它。 - Gras Double
1
你在MDN上看到的arguments.callee超链接说它在严格模式下被移除了。这不就是被弃用了吗? - styfle
1
请注意,在ES5严格模式下,arguments.callee.caller已被弃用:“另一个被弃用的功能是arguments.callee.caller,更具体地说是Function.caller。”(来源 - thdoan
1
Function.caller现在也已经被弃用了。 - Ooker
显示剩余2条评论

29

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

 function foo () {
     ... foo() ...
 }
比...更好
 function () {
     ... arguments.callee() ...
 }

通过caller属性,这个命名函数将能够访问到它的调用者:

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

比...更好

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

此项废弃是由于当前 ECMAScript 设计原则


2
你能描述一下为什么使用命名函数更好吗?在匿名函数中从未需要使用callee吗? - AnthonyWJones
28
如果在匿名函数中使用了callee,那么你应该将该函数更改为非匿名函数。 - Prestaul
3
有时候,使用.caller()是调试最简单的方法。在这种情况下,命名函数无法帮助 - 你需要确定哪个函数进行了调用。 - SamGoody
6
定义“更好”。例如,IE6-8浏览器中有命名函数怪异性(详见http://kangax.github.com/nfe/#jscript-bugs),而arguments.callee函数可用。 - cmc
1
除了IE6-8的怪癖外,它还会使代码紧密耦合。如果对象和/或函数的名称是硬编码的,那么像ardsasd和rsk82提到的那样,存在重大的重构危险,随着代码库规模的增加而不断增加。单元测试是一道防线,我使用它们,但对于这个硬编码问题,它们仍然不能真正满足我的需求。 - Jasmine Hegman
我真的希望在函数体中有一种一致的方式来引用函数对象...而不依赖于名称。保持代码的简洁和重复使用。 - vaughan

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

0

在使用new Function的情况下,它仍然可以在js strict mode / type="module"中工作。但是被kapersky反病毒软件检测到有恶意软件。

<script type="module">
let fn = new Function(`e`,`
   new Function('console.log(arguments.callee.caller)')()
`)
fn(5)
</script>


1
new Functioneval() 基本上是一个新的上下文。非模块化,非严格模式。 - Evert

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