53 votes

Puis-je remplacer l'objet Javascript Function pour enregistrer tous les appels de fonction ?

Est-il possible de remplacer le comportement de l'objet Function de façon à pouvoir injecter un comportement avant chaque appel de fonction, puis de continuer normalement ? Plus précisément, (bien que l'idée générale soit intrigante en soi) puis-je consigner à la console chaque appel de fonction sans avoir à insérer des instructions console.log partout ? Et puis le comportement normal continue ?

Je reconnais que cela aura probablement d'importants problèmes de performance ; je n'ai pas l'intention de faire fonctionner ce système de manière typique, même dans mon environnement de développement. Mais si cela fonctionne, cela semble être une solution élégante pour avoir une vue à 1000 mètres sur le code en cours d'exécution. Et je soupçonne que la réponse me montrera quelque chose de plus profond sur javascript.

22voto

etlovett Points 318

La réponse évidente est quelque chose comme ce qui suit :

var origCall = Function.prototype.call;
Function.prototype.call = function (thisArg) {
    console.log("calling a function");

    var args = Array.prototype.slice.call(arguments, 1);
    origCall.apply(thisArg, args);
};

Mais cela entre immédiatement dans une boucle infinie, car le fait même d'appeler console.log exécute un appel de fonction, qui appelle console.log qui exécute un appel de fonction, qui appelle console.log qui...

Le fait est que je ne suis pas sûr que ce soit possible.

15voto

Kenny Points 111

Intercepter les appels de fonction

Beaucoup ici ont essayé de passer outre l'appel .call. Certains ont échoué, d'autres ont réussi. Je réponds à cette vieille question, car elle a été soulevée sur mon lieu de travail, ce message étant utilisé comme référence.

Il n'y a que deux fonctions liées à l'appel de fonction que nous pouvons modifier : .call et .apply. Je vais vous montrer comment modifier ces deux fonctions avec succès.

TL;DR : Ce que le PO demande n'est pas possible. Certains des rapports de réussite dans les réponses sont dus au fait que la console appelle .call en interne juste avant l'évaluation, et non à l'appel que nous voulons intercepter.

Remplacement de Function.prototype.call

Il semble que ce soit la première idée qui vienne aux gens. Certaines ont eu plus de succès que d'autres, mais voici une mise en œuvre qui fonctionne :

// Store the original
var origCall = Function.prototype.call;
Function.prototype.call = function () {
    // If console.log is allowed to stringify by itself, it will
    // call .call 9 gajillion times. Therefore, lets do it by ourselves.
    console.log("Calling",
                Function.prototype.toString.apply(this, []),
                "with:",
                Array.prototype.slice.apply(arguments, [1]).toString()
               );

    // A trace, for fun
   console.trace.apply(console, []);

   // The call. Apply is the only way we can pass all arguments, so don't touch that!
   origCall.apply(this, arguments);
};

Cela intercepte avec succès Function.prototype.call.

Faisons un tour, d'accord ?

// Some tests
console.log("1"); // Does not show up
console.log.apply(console,["2"]); // Does not show up
console.log.call(console, "3"); // BINGO!

Il est important que cette opération ne soit pas exécutée à partir d'une console. Les différents navigateurs disposent de toutes sortes d'outils de console qui appellent .call eux-mêmes beaucoup y compris une fois pour chaque entrée, ce qui pourrait déconcerter l'utilisateur sur le moment. Une autre erreur consiste à n'utiliser que les arguments console.log, qui passent par l'API de la console pour la stringification, ce qui entraîne une boucle infinie.

Remplacer Function.prototype.apply également

Alors, pourquoi ne pas s'appliquer ? Ce sont les seules fonctions d'appel magique que nous ayons, alors essayons-les aussi. Voici une version qui prend les deux :

// Store apply and call
var origApply = Function.prototype.apply;
var origCall = Function.prototype.call;

// We need to be able to apply the original functions, so we need
// to restore the apply locally on both, including the apply itself.
origApply.apply = origApply;
origCall.apply = origApply;

// Some utility functions we want to work
Function.prototype.toString.apply = origApply;
Array.prototype.slice.apply = origApply;
console.trace.apply = origApply;

function logCall(t, a) {
    // If console.log is allowed to stringify by itself, it will
    // call .call 9 gajillion times. Therefore, do it ourselves.
    console.log("Calling",
                Function.prototype.toString.apply(t, []),
                "with:",
                Array.prototype.slice.apply(a, [1]).toString()
               );
    console.trace.apply(console, []);
}

Function.prototype.call = function () {
   logCall(this, arguments);
   origCall.apply(this, arguments);
};

Function.prototype.apply = function () {
    logCall(this, arguments);
    origApply.apply(this, arguments);
}

... Et essayons-le !

// Some tests
console.log("1"); // Passes by unseen
console.log.apply(console,["2"]); // Caught
console.log.call(console, "3"); // Caught

Comme vous pouvez le constater, les parenthèses d'appel passent inaperçues.

Conclusion

Heureusement, les parenthèses d'appel ne peuvent pas être interceptées depuis JavaScript. Mais même si .call interceptait l'opérateur de parenthèse sur les objets fonctionnels, comment pourrions-nous appeler l'original sans provoquer une boucle infinie ?

La seule chose que fait le remplacement de .call/.apply est d'intercepter les appels explicites à ces fonctions prototypes. Si la console est utilisée avec cette modification, il y aura beaucoup de spam. Il faut en outre être très prudent si on l'utilise, car l'utilisation de l'API de la console peut rapidement provoquer une boucle infinie (console.log utilisera .call en interne si on lui donne une chaîne autre qu'une chaîne).

4voto

HBP Points 6676

J'obtiens QUELQUES résultats et aucune page ne se bloque avec ce qui suit :

(function () {
  var 
    origCall = Function.prototype.call,
    log = document.getElementById ('call_log');  

  // Override call only if call_log element is present    
  log && (Function.prototype.call = function (self) {
    var r = (typeof self === 'string' ? '"' + self + '"' : self) + '.' + this + ' ('; 
    for (var i = 1; i < arguments.length; i++) r += (i > 1 ? ', ' : '') + arguments[i];  
    log.innerHTML += r + ')<br/>';

    this.apply (self, Array.prototype.slice.apply (arguments, [1]));
  });
}) ();

Testé uniquement dans la version 9.xxx de Chrome.

Il n'enregistre certainement pas tous les appels de fonction, mais il en enregistre certains ! Je soupçonne que seuls les appels à la fonction 'call' elle-même sont traités.

3voto

pentaphobe Points 143

Ce n'est qu'un test rapide, mais cela semble fonctionner pour moi. Ce n'est peut-être pas utile de cette façon, mais je restaure essentiellement le prototype pendant que je suis dans le corps de mon remplaçant, puis je le "dérestaure" avant de sortir.

Cet exemple enregistre simplement tous les appels de fonction - bien qu'il puisse y avoir une faille fatale que je n'ai pas encore détectée ; je fais cela pendant une pause café.

mise en œuvre

callLog = [];

/* set up an override for the Function call prototype
 * @param func the new function wrapper
 */
function registerOverride(func) {
   oldCall = Function.prototype.call;
   Function.prototype.call = func;
}

/* restore you to your regular programming 
 */
function removeOverride() {
   Function.prototype.call = oldCall;
}

/* a simple example override
 * nb: if you use this from the node.js REPL you'll get a lot of buffer spam
 *     as every keypress is processed through a function
 * Any useful logging would ideally compact these calls
 */
function myCall() { 
   // first restore the normal call functionality
   Function.prototype.call = oldCall;

   // gather the data we wish to log
   var entry = {this:this, name:this.name, args:{}};
   for (var key in arguments) {
     if (arguments.hasOwnProperty(key)) {
      entry.args[key] = arguments[key];
     }
   }
   callLog.push(entry);

   // call the original (I may be doing this part naughtily, not a js guru)
   this(arguments);

   // put our override back in power
   Function.prototype.call = myCall;
}

utilisation

J'ai eu quelques problèmes pour inclure les appels à cette fonction dans un seul et même collage, alors voici ce que j'ai tapé dans le REPL afin de tester les fonctions ci-dessus :

/* example usage
 * (only tested through the node.js REPL)
 */
registerOverride(myCall);
console.log("hello, world!");
removeOverride(myCall);
console.log(callLog);

1voto

colllin Points 1705

Vous pouvez passer outre Function.prototype.call assurez-vous seulement de apply dans votre contournement.

window.callLog = [];
Function.prototype.call = function() {
    Array.prototype.push.apply(window.callLog, [[this, arguments]]);
    return this.apply(arguments[0], Array.prototype.slice.apply(arguments,[1]));
};

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X