32

情况:

我在 Javascript 中发现了一些关于严格模式的奇怪之处。

  • 我正在使用一个外部的第三方 Javascript 库,它
    • 被缩小,
    • 有超过 4000 行代码,
    • 根本使用use strict,并且
    • 正在使用arguments.callee.
  • use strict在我自己的代码中使用,范围在一个函数内。

当我调用库提供的函数之一时,它会引发错误。然而,

  • 仅当我使用时才会引发错误use strict
  • 除 Chrome 之外的所有浏览器都会引发该错误

代码:

我已经删除了所有不相关的东西并将代码简化为这个(jsFiddle 上的在线演示):

// This comes from the minified external JS library.
// It creates a global object "foo".
(function () {
    foo = {};
    foo.bar = function (e) {
        return function () {
            var a5 = arguments.callee;
            while (a5) {
                a5 = a5.caller      // Error on this line in all browsers except Chrome
            }
        }
    }("any value here");
})();

// Here's my code.
(function() {
    "use strict";   // I enable strict mode in my own function only.

    foo.bar();
    alert("done");
})();


测试结果:

+-----------------------+-----+--------------------------------------------------------------+
| Browser               | OS  | Error                                                        |
+-----------------------+-----+--------------------------------------------------------------+
| Chrome 27.0.1453.94 m | Win | <<NO ERROR!>>                                                |
| Opera 12.15           | Win | Unhandled Error: Illegal property access                     |
| Firefox 21.0          | Win | TypeError: access to strict mode caller function is censored |
| Safari 5.1.7          | Win | TypeError: Type error                                        |
| IE 10                 | Win | SCRIPT5043: Accessing the 'caller' property of a function or |
|                       |     |             arguments object is not allowed in strict mode   |
| Chrome 27.0.1543.93   | Mac | <<NO ERROR!>>                                                |
| Opera 12.15           | Mac | Unhandled Error: Illegal property access                     |
| Firefox 21.0          | Mac | TypeError: access to strict mode caller function is censored |
| Safari 6.0.4          | Mac | TypeError: Function.caller used to retrieve strict caller    |
+-----------------------+-----+--------------------------------------------------------------+

注意:对于OS, Win= Windows 7, Mac= Mac OS 10.7.5


我的理解:

  • 所有现代桌面浏览器都支持use strict(请参阅我可以使用)。
  • use strict范围在我的函数内,因此在其范围之外定义的所有内容都不会受到影响(请参阅此 Stack Overflow 问题)。

问题:

那么,除了 Chrome 之外的所有浏览器都错了吗?还是相反?或者这是未定义的行为,所以浏览器可以选择以任何一种方式实现它?

4

2 回答 2

43

前言

在我们进入正文之前,有几个要点:

  • 所有现代桌面浏览器都支持use strict...

一点都不。IE8 是一个相当现代的浏览器 (不再是,在 2015 年),而 IE9 是一个相当现代的浏览器。它们都不支持严格模式(IE9 支持其中的一部分)。IE8 将伴随我们很长时间,因为它在 Windows XP 上已经达到了最高水平。即使 XP 现在已完全停产(好吧,您可以从 MS 购买特殊的“自定义支持”计划),但人们仍会继续使用它一段时间。

  • 在我的use strict函数范围内,因此在其范围之外定义的所有内容都不会受到影响

不完全的。该规范对非严格代码如何使用在严格模式下创建的函数施加了限制。所以严格模式可以超越它的框框。事实上,这是您正在使用的代码的一部分。

概述

那么,除了 Chrome 之外的所有浏览器都错了吗?还是相反?或者这是未定义的行为,所以浏览器可以选择以任何一种方式实现它?

稍微研究一下,它看起来像:

  1. Chrome 正在以一种方式做对,

  2. Firefox 以不同的方式让它变得正确,

  3. ...而且 IE10稍微有点错误。:-) (IE9 肯定弄错了,虽然不是以特别有害的方式。)

我没有看其他人,我想我们已经覆盖了地面。

从根本上导致问题的代码是这个循环

var a5 = arguments.callee;
while (a5) {
    a5 = a5.caller      // Error on this line in all browsers except Chrome
}

...这依赖于caller函数对象的属性。所以让我们从那里开始。

Function#caller

Function#caller属性从未在第 3 版规范中定义。一些实现提供了它,而另一些则没有。这是一个令人震惊的坏主意 (抱歉,这是主观的,不是吗?)一个实现问题(甚至不止一个arguments.caller),特别是在多线程环境中(并且有多线程 JavaScript 引擎),以及正如Bergi在对该问题的评论中指出的那样,递归代码。

所以在第 5 版中,他们明确地摆脱了它,通过指定caller在严格函数上引用属性会引发错误。(这在§13.2,创建函数对象,步骤 19中。)

这是一个严格的函数。但是,在非严格函数上,行为是未指定的并且依赖于实现。这就是为什么有这么多不同的方法可以做到这一点。

检测代码

引用检测代码比调试会话更容易,所以让我们使用这个

console.log("1. Getting a5 from arguments.callee");
var a5 = arguments.callee;
console.log("2. What did we get? " +
            Object.prototype.toString.call(a5));
while (a5) {
    console.log("3. Getting a5.caller");
    a5 = a5.caller;      // Error on this line in all browsers except Chrome
    console.log("4. What is a5 now? " +
                Object.prototype.toString.call(a5));
}

Chrome 如何做到正确

在 V8(Chrome 的 JavaScript 引擎)上,上面的代码告诉我们:

1.从arguments.callee中获取a5
2. 我们得到了什么?[对象功能]
3. 获取 a5.caller
4.现在什么是a5?[对象空]

所以我们从 中获得了对该foo.bar函数的引用arguments.callee,但随后访问caller该非严格函数给了我们null. 循环终止,我们没有收到任何错误。

由于Function#caller未为非严格函数指定,因此允许 V8 为访问calleron做任何它想做的事情foo.bar。返回null是完全合理的(虽然我很惊讶看到null而不是undefined)。(我们将在下面的结论中回到这一点null......)

Firefox 如何做到正确

SpiderMonkey(Firefox 的 JavaScript 引擎)这样做:

1.从arguments.callee中获取a5
2. 我们得到了什么?[对象功能]
3. 获取 a5.caller
TypeError:对严格模式调用函数的访问被审查

我们从 开始foo.bararguments.callee但随后访问caller该非严格函数失败并出现错误。

同样,由于caller对非严格函数的访问是未指定的行为,SpiderMonkey 的人可以做他们想做的事。如果要返回的函数是严格函数,他们决定抛出错误。一条很好的路线,但由于未指定,因此允许他们走。

IE10 是如何弄错的

JScript(IE10 的 JavaScript 引擎)这样做:

1.从arguments.callee中获取a5
 2. 我们得到了什么?[对象功能]
 3. 获取 a5.caller
SCRIPT5043:在严格模式下不允许访问函数或参数对象的“调用者”属性

和其他人一样,我们foo.bararguments.callee. 然后尝试访问该非严格函数caller给我们一个错误,说我们不能在严格模式下这样做。

我称之为“错误”(但使用非常小写的“w”),因为它表示我们不能在严格模式下做我们正在做的事情,但我们不是严格模式下。

但是您可能会争辩说这与 Chrome 和 Firefox 所做的一样并没有错,因为(再次)caller访问是未指定的行为。所以 IE10 的人决定他们对这种未指定行为的实现会引发严格模式错误。我认为它具有误导性,但同样,如果它是“错误的”,那肯定不是错误。

顺便说一句,IE9 肯定会出错:

1.从arguments.callee中获取a5
2. 我们得到了什么?[对象功能]
3. 获取 a5.caller
4.现在什么是a5?[对象功能]
3. 获取 a5.caller
4.现在什么是a5?[对象空]

它允许Function#caller在非严格函数上,然后在严格函数上允许它,返回null. 规范很清楚,第二次访问应该引发错误,因为它是caller在严格函数上访问的。

结论和观察

以上所有内容的有趣之处在于,除了明确指定的在您尝试访问caller严格功能时抛出错误的行为之外,Chrome、Firefox 和 IE10 都(以各种方式)阻止您使用caller来获取对一个严格的函数,即使在访问caller非严格函数时也是如此。Firefox 通过抛出错误来做到这一点。Chrome 和 IE10 通过返回null. 它们都支持通过(在非严格函数上)获取对非严格函数的引用,而不是严格函数caller

我在任何地方都找不到指定的行为(但是,caller在非严格函数上完全未指定......)。这可能是正确的事情(tm),我只是没有看到它指定。

这段代码玩起来也很有趣:Live Copy | 直播源

<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Strict and Loose Function#caller</title>
  <style>
    p {
      font-family: sans-serif;
      margin: 0.1em;
    }
    .err {
      color: #d00;
    }
  </style>
</head>
<body>
  <script>
    function display(msg, cls) {
        var p = document.createElement('p');
        if (cls) {
            p.className = cls;
        }
        p.innerHTML = String(msg);
        document.body.appendChild(p);
    }

    // The loose functions
    (function () {
      function loose1() {
        display("loose1 calling loose2");
        loose2();
      }
      loose1.id = "loose1"; // Since name isn't standard yet

      function loose2() {
        var c;

        try {
          display("loose2: looping through callers:");
          c = loose2;
          while (c) {
            display("loose2: getting " + c.id + ".caller");
            c = c.caller;
            display("loose2: got " +
                    ((c && c.id) || Object.prototype.toString.call(c)));
          }
          display("loose2: done");
        }
        catch (e) {
          display("loose2: exception: " +
                  (e.message || String(e)),
                  "err");
        }
      }
      loose2.id = "loose2";

      window.loose1 = loose1;

      window.loose2 = loose2;
    })();

    // The strict ones
    (function() {
      "use strict";

      function strict1() {
        display("strict1: calling strict2");
        strict2();
      }
      strict1.id = "strict1";

      function strict2() {
        display("strict2: calling loose1");
        loose1();
      }
      strict2.id = "strict2";

      function strict3() {
        display("strict3: calling strict4");
        strict4();
      }
      strict3.id = "strict3";

      function strict4() {
        var c;

        try {
          display("strict4: getting strict4.caller");
          c = strict4.caller;
        }
        catch (e) {
          display("strict4: exception: " +
                  (e.message || String(e)),
                 "err");
        }
      }
      strict4.id = "strict4";

      strict1();      
      strict3();
    })();
  </script>
</body>
</html>
于 2013-06-01T09:18:20.260 回答
3

我必须使用一个较旧的 Telerik JS 库,我不能轻易更新,今天早上遇到了这个错误。对于某些人来说,一种可能的解决方法可能是在调用松散模式函数之前使用“setTimeout”JS 函数离开严格模式。

例如改变这个:

function functionInStrictMode(){
  looseModeFunction();
}

对于这样的事情:

function functionInStrictMode(){
    setTimeout(looseModeFunction);      
}

我猜这可能有效,因为 setTimeout 将上下文恢复到全局命名空间和/或离开 functionInStrictMode 的范围。我不完全了解所有细节。可能有更好的方法;我没有彻底研究这个,但我想我会把它贴在这里进行讨论。

于 2019-05-29T15:10:56.980 回答