web-dev-qa-db-fra.com

Portée incohérente de «utiliser strictement» sur différents navigateurs Web (concernant arguments.callee et l'appelant)

Situation:

J'ai trouvé quelque chose d'étrange concernant le mode strict en Javascript.

  • J'utilise une bibliothèque Javascript externe et tierce qui
    • a été minifié,
    • a plus de 4000 lignes de code,
    • n'est pas utilisant use strict du tout, et
    • utilise arguments.callee.
  • J'utilise use strict dans mon propre code, dans une fonction.

Lorsque j'appelle l'une des fonctions fournies par la bibliothèque, cela génère une erreur. cependant,

  • l'erreur est levée uniquement si j'utilise use strict
  • l'erreur est lancée dans tous les navigateurs sauf Chrome

Code:

J'ai supprimé tous les éléments non liés et réduit le code dans ce ( démo en ligne sur 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");
})();

Résultat du test:

+-----------------------+-----+--------------------------------------------------------------+
| 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    |
+-----------------------+-----+--------------------------------------------------------------+

Remarque: pour OS, Win = Windows 7, Mac = Mac OS 10.7.5


Ma compréhension:


Question:

Donc, tous les navigateurs sont-ils sauf Chrome faux? Ou est-ce l'inverse? Ou est-ce un comportement indéfini pour que les navigateurs puissent choisir de l'implémenter de l'une ou l'autre façon?

28
Pang

Préface

Quelques points rapides avant d'entrer dans le vif du sujet:

  • Tous les navigateurs de bureau modernes prennent en charge use strict ...

Non pas du tout. IE8 est un navigateur assez moderne (plus maintenant, en 2015), et IE9 est un assez navigateur assez moderne. Aucun d'eux ne prend en charge le mode strict (IE9 en prend en charge certaines parties). IE8 va être avec nous pendant longtemps, car il est aussi élevé que possible sur Windows XP. Même si XP est maintenant complètement en fin de vie (enfin, vous pouvez acheter un plan spécial "Support personnalisé" de MS), les gens continueront de l'utiliser pendant un certain temps.

  • Le use strict Est délimité dans ma fonction, donc tout ce qui est défini en dehors de sa portée n'est pas affecté

Pas assez. La spécification impose des restrictions sur la façon dont même le code non strict utilise des fonctions créées en mode strict. Le mode strict peut donc sortir de sa boîte. Et en fait, cela fait partie de ce qui se passe avec le code que vous utilisez.

Aperçu

Donc, tous les navigateurs sauf Chrome est-il faux? Ou est-ce l'inverse? Ou est-ce un comportement indéfini pour que les navigateurs puissent choisir de l'implémenter de l'une ou l'autre façon?

En y regardant un peu, on dirait:

  1. Chrome fait les choses dans un sens,

  2. Firefox fait les choses différemment,

  3. ... et IE10 l'obtient très légèrement faux. :-) (IE9 se trompe définitivement, mais pas de manière particulièrement nuisible.)

Je n'ai pas regardé les autres, j'ai pensé que nous avions recouvert le sol.

Le code qui cause fondamentalement le problème est cette boucle

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

... qui repose sur la propriété caller des objets fonction. Commençons donc par là.

Function#caller

La propriété Function#caller N'a jamais été définie dans la spécification de 3e édition. Certaines implémentations l'ont fourni, d'autres non. Ses une mauvaise idée choquante (désolé, c'était subjectif, n'est-ce pas?) un problème d'implémentation (encore plus d'un que arguments.caller), en particulier dans les environnements multithreads (et il existe plusieurs (moteurs JavaScript filetés), ainsi qu'avec du code récursif, comme l'a souligné Bergi dans les commentaires sur la question.

Donc, dans la 5e édition, ils s'en sont explicitement débarrassés, en spécifiant que référencer la propriété caller sur une fonction stricte déclencherait une erreur. (C'est dans §13.2, Création d'objets fonction, étape 19 .)

C'est sur une fonction stricte. Sur une fonction non stricte, cependant, le comportement n'est pas spécifié et dépend de l'implémentation. C'est pourquoi il existe tant de façons différentes de faire les choses correctement.

Code instrumenté

Il est plus facile de se référer au code instrumenté qu’une session de débogage, alors tilisez ceci :

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));
}

Comment Chrome fait les choses correctement

Sur V8 (moteur JavaScript de Chrome), le code ci-dessus nous donne ceci:

1. Obtenir a5 à partir d'arguments.callee 
 2. Qu'avons-nous obtenu? [Fonction d'objet] 
 3. Obtenir a5.caller 
 4. Qu'est-ce que l'A5 maintenant? [objet Null]

Nous avons donc obtenu une référence à la fonction foo.bar De arguments.callee, Mais accéder à caller sur cette fonction non stricte nous a donné null. La boucle se termine et nous n'obtenons aucune erreur.

Puisque Function#caller N'est pas spécifié pour les fonctions non strictes, V8 est autorisé à faire tout ce qu'il veut pour cet accès à caller sur foo.bar. Renvoyer null est parfaitement raisonnable (même si j'ai été surpris de voir null plutôt que undefined). (Nous y reviendrons null dans les conclusions ci-dessous ...)

Comment Firefox réussit

SpiderMonkey (le moteur JavaScript de Firefox) fait ceci:

1. Obtenir a5 à partir d'arguments.callee 
 2. Qu'avons-nous obtenu? [Fonction d'objet] 
 3. Obtenir a5.caller 
 TypeError: l'accès à la fonction d'appel en mode strict est censuré

Nous commençons par obtenir foo.bar De arguments.callee, Mais l'accès à caller sur cette fonction non stricte échoue avec une erreur.

Puisque, encore une fois, l'accès à caller sur une fonction non stricte est un comportement non spécifié, les gens de SpiderMonkey peuvent faire ce qu'ils veulent. Ils ont décidé de lancer une erreur si la fonction qui serait retournée est une fonction stricte. Une ligne fine, mais comme cela n'est pas spécifié, ils sont autorisés à marcher.

Comment IE10 l'obtient très légèrement faux

JScript (moteur JavaScript d'IE10) fait ceci:

 1. Obtenir a5 de arguments.callee 
 2. Qu'avons-nous obtenu? [Fonction objet] 
 3. Obtenir a5.caller 
 SCRIPT5043: L'accès à la propriété 'caller' d'une fonction ou d'un objet arguments n'est pas autorisé en mode strict

Comme pour les autres, nous obtenons la fonction foo.bar De arguments.callee. Ensuite, essayer d'accéder à caller de cette fonction non stricte nous donne une erreur disant que nous ne pouvons pas le faire en mode strict.

J'appelle cela "mauvais" (mais avec un très minuscule "w") parce qu'il dit que nous ne pouvons pas faire ce que nous faisons en mode strict, mais nous ne sommes pas in mode strict.

Mais vous pourriez dire que ce n'est pas plus faux que ce que Chrome et Firefox font, car (encore) l'accès caller est un comportement non spécifié. Les gens d'IE10 ont donc décidé que leur implémentation de ce comportement non spécifié jetterait une erreur en mode strict. Je pense que c'est trompeur, mais encore une fois, si c'est "faux", ce n'est certainement pas très faux.

BTW, IE9 se trompe définitivement:

1. Obtenir a5 à partir d'arguments.callee 
 2. Qu'avons-nous obtenu? [Fonction d'objet] 
 3. Obtenir a5.caller 
 4. Qu'est-ce que l'A5 maintenant? [Fonction d'objet] 
 3. Obtenir a5.caller 
 4. Qu'est-ce que l'A5 maintenant? [objet Null]

Il autorise Function#caller Sur la fonction non stricte, puis l'autorise sur une fonction stricte, renvoyant null. La spécification est claire que ce deuxième accès aurait dû générer une erreur, car il accédait à caller sur une fonction stricte.

Conclusions et observations

Ce qui est intéressant à propos de tout ce qui précède, c'est qu'en plus du comportement clairement spécifié de lancer une erreur si vous essayez d'accéder à caller sur des fonctions strictes, Chrome, Firefox et IE10, tous (de diverses manières) empêchent votre en utilisant caller pour obtenir une référence à une fonction stricte, même lorsque vous accédez à caller sur une fonction non stricte. Firefox le fait en lançant une erreur. Chrome et IE10 le font en renvoyant null. Ils prennent tous en charge l'obtention d'une référence à un non -strict fonction via caller (sur une fonction non stricte), mais pas une fonction stricte.

Je ne peux trouver ce comportement spécifié nulle part (mais alors, caller sur les fonctions non strictes est entièrement non spécifié ...). C'est probablement la bonne chose(tm), Je ne le vois pas spécifié.

Ce code est également amusant à jouer avec: Live Copy | Source en direct

<!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>
41
T.J. Crowder

Je dois utiliser une ancienne bibliothèque Telerik JS que je ne peux pas facilement mettre à jour et j'ai rencontré cette erreur ce matin. Une solution de contournement possible pour certaines personnes pourrait être d'utiliser la fonction JS 'setTimeout' pour quitter le mode strict avant d'appeler la fonction de mode lâche.

par exemple. Change ça:

function functionInStrictMode(){
  looseModeFunction();
}

Pour quelque chose comme ça:

function functionInStrictMode(){
    setTimeout(looseModeFunction);      
}

Je suppose que cela fonctionne probablement parce que setTimeout rétablit le contexte dans l'espace de noms global et/ou laisse la portée de functionInStrictMode. Je ne comprends pas bien tous les détails. Il pourrait y avoir de meilleures façons; Je n'ai pas fait de recherche approfondie sur ce sujet, mais j'ai pensé le publier ici pour discussion.

0
scradam