Remplacer une fonction JavaScript lors du référencement de l'original

160

J'ai une fonction,, a()que je veux remplacer, mais aussi que l'original a()soit exécuté dans un ordre dépendant du contexte. Par exemple, parfois, lorsque je génère une page, je souhaite remplacer comme ceci:

function a() {
    new_code();
    original_a();
}

et parfois comme ceci:

function a() {
    original_a();
    other_new_code();
}

Comment puis-je obtenir cela original_a()de la circonscription principale a()? Est-ce même possible?

S'il vous plaît, ne suggérez pas d'alternatives à la surcharge de cette façon, j'en connais beaucoup. Je pose des questions sur cette façon spécifiquement.

Kev
la source

Réponses:

179

Vous pouvez faire quelque chose comme ceci:

var a = (function() {
    var original_a = a;

    if (condition) {
        return function() {
            new_code();
            original_a();
        }
    } else {
        return function() {
            original_a();
            other_new_code();
        }
    }
})();

La déclaration à l' original_aintérieur d'une fonction anonyme l'empêche d'encombrer l'espace de noms global, mais elle est disponible dans les fonctions internes.

Comme Nerdmaster mentionné dans les commentaires, assurez-vous d'inclure le ()à la fin. Vous souhaitez appeler la fonction externe et stocker le résultat (l'une des deux fonctions internes) dans a, pas stocker la fonction externe elle-même a.

Matthew Crumley
la source
25
Pour tous les idiots comme moi - faites très attention au "()" à la fin - sans cela, il renvoie la fonction externe, pas les fonctions internes :)
Nerdmaster
@Nerdmaster Merci de l'avoir signalé. J'ai ajouté une note pour aider les gens à remarquer.
Matthew Crumley
5
Wow, j'ai essayé de le faire sans espace de noms et j'ai eu un débordement de pile, lol.
gosukiwi
Je pense que le résultat serait le même si la première et la dernière parenthèses étaient supprimées de la fonction. Comme d'ici (functi..et }). Faire juste }();produirait le même résultat, car le (function{})()premier jeu de parenthèses est utilisé parce que vous ne pouvez pas simplement déclarer, anonyme, l'expression de fonction que vous devez l'assigner. Mais en faisant () vous ne définissez rien simplement en retournant une fonction.
Muhammad Umer
@MuhammadUmer Vous avez raison de dire que les parenthèses autour de l'expression de la fonction ne sont pas nécessaires. Je les ai inclus parce que sans eux, si vous regardez simplement les deux premières lignes, il semble (du moins pour moi) que vous attribuez directement la fonction externe a, ne l'appelez pas et stockez la valeur de retour. Les parenthèses me font savoir qu'il se passe autre chose.
Matthew Crumley
88

Le modèle Proxy peut vous aider:

(function() {
    // log all calls to setArray
    var proxied = jQuery.fn.setArray;
    jQuery.fn.setArray = function() {
        console.log( this, arguments );
        return proxied.apply( this, arguments );
    };
})();

Ce qui précède enveloppe son code dans une fonction pour masquer la variable «mandatée». Il enregistre la méthode setArray de jQuery dans une fermeture et l'écrase. Le proxy enregistre ensuite tous les appels à la méthode et délègue l'appel à l'original. L'utilisation de apply (this, arguments) garantit que l'appelant ne pourra pas remarquer la différence entre la méthode d'origine et la méthode mandatée.

PhilHoy
la source
10
En effet. l'utilisation de 'appliquer' et 'arguments' rend cela beaucoup plus robuste que les autres réponses
Robert Levy
Notez que ce modèle ne nécessite pas jQuery - ils utilisent simplement l'une des fonctions de jQuery comme exemple.
Kev
28

Merci les gars, le modèle de proxy a vraiment aidé ..... En fait, je voulais appeler une fonction globale foo .. Dans certaines pages, j'ai besoin de faire quelques vérifications. J'ai donc fait ce qui suit.

//Saving the original func
var org_foo = window.foo;

//Assigning proxy fucnc
window.foo = function(args){
    //Performing checks
    if(checkCondition(args)){
        //Calling original funcs
        org_foo(args);
    }
};

Thnx cela m'a vraiment aidé

Naresh S
la source
3
Lorsque vous suivez le modèle de proxy , il sera important dans certains cas d'implémenter ce détail qui n'est pas présent dans le code de cette réponse: Au lieu de org_foo(args), appelez org_foo.call(this, args). Cela se maintient thiscomme il l'aurait été lorsque window.foo appelle normalement (sans proxy). Voir cette réponse
cellepo
10

Vous pouvez remplacer une fonction en utilisant une construction comme:

function override(f, g) {
    return function() {
        return g(f);
    };
}

Par exemple:

 a = override(a, function(original_a) {
      if (condition) { new_code(); original_a(); }
      else { original_a(); other_new_code(); }
 });

Edit: Correction d'une faute de frappe.

Hai Phan
la source
+1 C'est tellement plus lisible que les autres exemples de modèles de proxy!
Mu Mind
1
... mais si je ne me trompe pas, cela est limité aux fonctions à zéro argument, ou au moins à un nombre prédéterminé d'arguments. Existe-t-il un moyen de le généraliser à un nombre arbitraire d'arguments sans perdre la lisibilité?
Mu Mind
@MuMind: oui, mais en utilisant correctement le modèle de proxy - voir ma réponse .
Dan Dascalescu
4

Passer des arguments arbitraires:

a = override(a, function(original_a) {
    if (condition) { new_code(); original_a.apply(this, arguments) ; }
    else { original_a.apply(this, arguments); other_new_code(); }
});
Edric Garran
la source
1
les arguments ne feront-ils pas référence original_a?
Jonathan.
2

La réponse que @Matthew Crumley fournit est d'utiliser les expressions de fonction immédiatement appelées, pour fermer l'ancienne fonction «a» dans le contexte d'exécution de la fonction retournée. Je pense que c'était la meilleure réponse, mais personnellement, je préférerais passer la fonction «a» comme argument à l'IIFE. Je pense que c'est plus compréhensible.

   var a = (function(original_a) {
        if (condition) {
            return function() {
                new_code();
                original_a();
            }
        } else {
            return function() {
                original_a();
                other_new_code();
            }
        }
    })(a);
Rodrigo Hernandez
la source
1

Les exemples ci-dessus ne s'appliquent pas correctement thisou ne passent pas argumentscorrectement au remplacement de fonction. Underscore _.wrap () encapsule les fonctions existantes, s'applique thiset passe argumentscorrectement. Voir: http://underscorejs.org/#wrap

nevf
la source
0

J'avais du code écrit par quelqu'un d'autre et je voulais ajouter une ligne à une fonction que je ne pouvais pas trouver dans le code. Donc, comme solution de contournement, je voulais le remplacer.

Aucune des solutions n'a fonctionné pour moi.

Voici ce qui a fonctionné dans mon cas:

if (typeof originalFunction === "undefined") {
    originalFunction = targetFunction;
    targetFunction = function(x, y) {
        //Your code
        originalFunction(a, b);
        //Your Code
    };  
}
Sagar Sodah
la source
0

J'ai créé un petit assistant pour un scénario similaire car j'avais souvent besoin de remplacer les fonctions de plusieurs bibliothèques. Cet assistant accepte un «espace de noms» (le conteneur de fonction), le nom de la fonction et la fonction de remplacement. Il remplacera la fonction d'origine dans l'espace de noms référencé par la nouvelle.

La nouvelle fonction accepte la fonction d'origine comme premier argument et les arguments de fonctions d'origine comme le reste. Cela préservera le contexte à chaque fois. Il prend également en charge les fonctions vides et non vides.

function overrideFunction(namespace, baseFuncName, func) {
    var originalFn = namespace[baseFuncName];
    namespace[baseFuncName] = function () {
        return func.apply(this, [originalFn.bind(this)].concat(Array.prototype.slice.call(arguments, 0)));
    };
}

Utilisation par exemple avec Bootstrap:

overrideFunction($.fn.popover.Constructor.prototype, 'leave', function(baseFn, obj) {
    // ... do stuff before base call
    baseFn(obj);
    // ... do stuff after base call
});

Je n'ai cependant créé aucun test de performance. Cela peut éventuellement ajouter des frais généraux indésirables qui peuvent ou ne peuvent pas être un gros problème, selon les scénarios.

Zoltán Tamási
la source
0

À mon avis, les principales réponses ne sont pas lisibles / maintenables, et les autres réponses ne lient pas correctement le contexte. Voici une solution lisible utilisant la syntaxe ES6 pour résoudre ces deux problèmes.

const orginial = someObject.foo;
someObject.foo = function() {
  if (condition) orginial.bind(this)(...arguments);
};
James L.
la source
Mais l'utilisation de la syntaxe ES6 rend son utilisation plutôt restreinte. Avez-vous une version qui fonctionne dans ES5?
eitch
0

Ma réponse a donc fini par être une solution qui me permet d'utiliser la variable _this pointant vers l'objet d'origine. Je crée une nouvelle instance d'un "carré" mais je détestais la façon dont le "carré" générait sa taille. J'ai pensé qu'il devrait suivre mes besoins spécifiques. Cependant, pour ce faire, j'avais besoin que le carré ait une fonction "GetSize" mise à jour avec les éléments internes de cette fonction appelant d'autres fonctions déjà existantes dans le carré telles que this.height, this.GetVolume (). Mais pour ce faire, je devais le faire sans aucun piratage fou. Voici donc ma solution.

Une autre fonction d'initialisation ou d'assistance d'objet.

this.viewer = new Autodesk.Viewing.Private.GuiViewer3D(
  this.viewerContainer)
var viewer = this.viewer;
viewer.updateToolbarButtons =  this.updateToolbarButtons(viewer);

Fonction dans l'autre objet.

updateToolbarButtons = function(viewer) {
  var _viewer = viewer;
  return function(width, height){ 
blah blah black sheep I can refer to this.anything();
}
};
SteckDEV
la source
0

Je ne sais pas si cela fonctionnera dans toutes les circonstances, mais dans notre cas, nous essayions de remplacer la describefonction dans Jest afin de pouvoir analyser le nom et ignorer tout le describebloc s'il répondait à certains critères.

Voici ce qui a fonctionné pour nous:

function describe( name, callback ) {
  if ( name.includes( "skip" ) )
    return this.describe.skip( name, callback );
  else
    return this.describe( name, callback );
}

Deux choses qui sont essentielles ici:

  1. Nous n'utilisons pas de fonction fléchée () =>.

    Les fonctions fléchées changent la référence à thiset nous avons besoin que ce soit le fichier this.

  2. L'utilisation de this.describeet this.describe.skipau lieu de juste describeet describe.skip.

Encore une fois, je ne suis pas sûr que cela ait de la valeur pour quiconque, mais nous avons initialement essayé de nous en sortir avec l' excellente réponse de Matthew Crumley, mais nous devions faire de notre méthode une fonction et accepter les paramètres afin de les analyser au conditionnel.

Joshua Pinter
la source