web-dev-qa-db-fra.com

Passage de la portée à la fonction/liaison de rappel

J'essaie de passer la portée de la fonction à une méthode de rappel. Le problème que je rencontre est que je reçois une étendue d'objet, ce qui ne me permet pas d'accéder aux paramètres et aux variables locales de la fonction d'origine. Ma compréhension de "ceci" signifie le contexte actuel (que ce soit une fenêtre ou un objet) en plus des variables et paramètres déclarés localement. [Citez l'excellent travail de Richard Cornford sur http://jibbering.com/faq/notes/closures/ dans la section "Contextes d'exécution"]. Je comprends également que les variables en JavaScript ont une portée de fonction (que si elles sont déclarées dans une fonction, elles ne sont accessibles que depuis cette fonction). 

Cela dit, dans un nouvel environnement, j'essaie de coder un modèle que j'ai beaucoup utilisé pour mon employeur précédent, en appelant une méthode asynchrone, en spécifiant un gestionnaire de rappel et en transmettant mon étendue actuelle, en espérant qu'il soit disponible dans la méthode de rappel. . Je ne trouve pas que cela soit le cas dans mon environnement actuel. (divulgation: j'utilisais ExtJS dans mon environnement précédent ... me faisant maintenant sentir comme si j'étais un peu trop à l'aise avec le cadre, émettant des hypothèses sur ce qui se passait).

Mon code de test simple montre ce que j'essaie de faire et ce qui ne fonctionne pas.

function myHandler(data, ctx) {
    console.log('myHandler():  bar: ' + bar);  // <- prob: bar undefined 
    console.log(JSON.stringify(data));
}
MySvcWrap = {
    doWork: function(p1, callback, scope) {
        var result = {colors: ['red', 'green'], name:'Jones', what: p1};
        if (callback) {
            callback.call(scope||this,result, scope);
        } 
    }
}
function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', myHandler, this); // scope object is this
}

lookup();

Le problème ici est que le "ceci" transmis à MySvcWrap.doWork est l'objet global Window dans ce cas. Mon intention est de transmettre le contexte d'exécution de la fonction à myHandler. 

Ce que j'ai essayé Si, au lieu de 'ceci', je passe un objet, cela fonctionne, par exemple:

function myHandler(data, ctx) {
    console.log('myHandler():  this.bar: ' + this.bar);  // <- no prob: this.bar 
    console.log(JSON.stringify(data));
}

function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', myHandler, {bar: bar}); // scope object is just object
}

J'ai besoin de quelqu'un pour me matraquer par ici ... quand je passe "ceci" dans mon premier cas, bien sûr, c'est la portée globale (je suis dans une fonction définie globalement) ... Mon problème est que j'ai pensé en passant portée que j’ai eu accès à des variables et paramètres définis localement dans ce contexte ... Suis-je en train de faire basculer ma compréhension de JS ?? Comment accomplir cette tâche?

16
Merl

btw, quelques mots sur les portées dans votre code:

function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', myHandler, this); // this here points to window object and not to the function scope
}

c'est donc comme écrire:

function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', myHandler, window); 
}

c'est parce que vous définissez la fonction de recherche globalement. dans la fonction doWork, lorsque vous écrivez this, il pointe sur un objet MySvcWrap (car cette fonction est définie à l'intérieur de cet objet). 

Si votre fonction de rappel doit voir la variable bar, elle doit être définie dans la même portée, comme celle-ci. 

MySvcWrap = {
    doWork: function(p1, callback, scope) {
        var result = {colors: ['red', 'green'], name:'Jones', what: p1};
        if (callback) {
            callback.call(scope||this,result, scope);
        } 
    }
}
function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', 
        function (data, ctx) {
            console.log('myHandler():  bar: ' + bar); 
            console.log(JSON.stringify(data));
        }, 
        this); // scope object is this
}

lookup();

dans ce cas, vous envoyez une fonction anonyme en tant que rappel, elle est définie dans la fonction de recherche de sorte qu'elle ait accès à ses variables locales; ma console me montre dans ce cae:

myHandler(): bar: food
{"colors":["red","green"],"name":"Jones","what":"thang"}

pour faciliter la prise en charge, vous pouvez définir la fonction de recherche myHandler dans la recherche:

function lookup() {
    var bar = 'food'; // local var
    var myHandler = function(data, ctx) {
        console.log('myHandler():  bar: ' + bar);
        console.log(JSON.stringify(data));
    };
    MySvcWrap.doWork('thang', myHandler, this); // scope object is this
}

D'autre part, pourquoi une fonction devrait avoir accès aux variables locales d'une autre fonction? Peut-être qu'il peut être redessiné ...

Une autre façon de faire fonctionner votre code consiste à utiliser une fonction anonyme, au lieu de rechercher (fonctionnera si vous déclarez et exécutez cette fonction une fois):

(function() {
   var bar = 'food';

   function myHandler(data, ctx) {
       console.log('myHandler():  bar: ' + bar);  
       console.log(JSON.stringify(data));
    } 
    MySvcWrap = {
       doWork: function(p1, callback, scope) {
           var result = {colors: ['red', 'green'], name:'Jones', what: p1};
           if (callback) {
               callback.call(scope||this,result, scope);
           } 
       }
    }
    MySvcWrap.doWork('thang', myHandler, this);
  }
)();

le résultat est le même, mais plus de fonction de recherche ...

Et encore une idée pour que cela fonctionne ... En fait, vous devez définir le gestionnaire de callback dans le même champ d'application que la variable bar, de sorte que cela peut être un peu délicat, mais tout aussi alternatif:

function myHandler(bar) { // actually in this case better to call it createHandler 
    return function(data, ctx) {
        console.log('myHandler():  bar: ' + bar); 
        console.log(JSON.stringify(data));
    } 
}
MySvcWrap = {
    doWork: function(p1, callback, scope) {
        var result = {colors: ['red', 'green'], name:'Jones', what: p1};
        if (callback) {
            callback.call(scope||this,result, scope);
        } 
    }
}
function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', myHandler(bar), this); // scope object is this
}

Et quelques ressources à lire sur la portée et la fermeture de JavaScript:

  1. Explication de la portée et des fermetures de JavaScript
  2. Picking up Javascript - Fermetures et périmètres lexicaux
  3. JavaScript: Advanced Scoping & Other Puzzles - Très belle présentation sur le sujet
16
Maxym

Le code fonctionnerait s'il était structuré comme ceci.

MySvcWrap = {
    doWork: function(p1, callback, scope) {
        var result = {colors: ['red', 'green'], name:'Jones', what: p1};
        if (callback) {
            callback.call(scope||this,result, scope);
        } 
    }
}
function lookup() {
    var bar = 'food'; // local var

    function myHandler(data, ctx) {
        console.log('myHandler():  bar: ' + bar);  // <- prob: bar undefined 
        console.log(JSON.stringify(data));
    }

    MySvcWrap.doWork('thang', myHandler, this); // scope object is this
}

lookup();

La fonction myHandler peut accéder aux variables locales de la recherche car elle est entourée par celle-ci, donc à une fermeture.

J'essaierais probablement d'obtenir le même résultat, en structurant le code de la manière suivante.

function myHandler(data, bar, ctx) {
    console.log('myHandler():  bar: ' + bar);  // <- prob: bar undefined 
    console.log(JSON.stringify(data));
}
MySvcWrap = {
    doWork: function(p1, callback) {
        var result = {colors: ['red', 'green'], name:'Jones', what: p1};
        if (callback) {
            callback(result);
        } 
    }
}
function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', myHandler.bind(this, bar)); // callback function is bound to the scope
}

lookup();

Plutôt que de transmettre la portée, j'utilise bind dans la méthode de recherche. En utilisant bind, je peux également ajouter les variables locales que je souhaite transmettre à partir de cette portée.

Bien sûr, comme bind n'est pas disponible dans les anciens navigateurs, vous devez soit l'ajouter via un framework, soit utiliser l'un des nombreux fragments de code qui ajoutent la méthode si elle n'existe pas.

2
leebriggs

_ {J'ai été un peu rapide lors de ma première réponse et laissé quelques trous comme Maxym dans les commentaires ci-dessous, merci de m'avoir informé :) C'est ce que je reçois pour avoir essayé de poster rapidement avant de retourner au travail: P

Ce que j'essayais de faire dans ma première réponse est que si vous voulez utiliser une liaison pour accéder aux variables de la fonction lookup à partir de la fonction myHandler (ce qui est en dehors de la portée de lookup), vous devrez ne pas utiliser var mais this.

Utiliser var le rendrait accessible uniquement à lookup's private scope et à toutes les fonctions qui y sont imbriquées, ce qui est la façon dont les autres réponses ont été démontrées (et constituent probablement la meilleure route).

Vous trouverez ci-dessous un exemple actualisé de la procédure à suivre pour passer la portée de la recherche à myHandler, laissant myHandler complètement hors de la portée de la recherche. Espérons que cela aidera à faire la lumière sur:

"Le problème que je rencontre est que je reçois une étendue d'objet, ce qui ne me permet pas d'accéder aux paramètres et aux variables locales de la fonction d'origine."

Comme indiqué dans les commentaires sur ma réponse précédente, this peut être compliqué et vous pouvez finir par ajouter des éléments à la portée de global si vous ne faites pas attention, comme je l'avais fait dans mon premier exemple. :( J'ai donc ajouté un peu de vérification hack'ish pour voir quelle portée this s'assure que ce n'est pas window à des fins de démonstration ... 

Exemple en direct

function myHandler(data, ctx) {
    console.log('myHandler():  bar: ' + this.bar); // <- must use `this`
    console.log('myHandler():  privateBar: ' + this.privateBar); // <- undefined because it's privately scoped
    console.log(JSON.stringify(data));
}
MySvcWrap = {
    doWork: function(p1, callback, scope) {
        var result = {
            colors: ['red', 'green'],
            name: 'Jones',
            what: p1
        };
        if (callback) {
            callback.call(scope || this, result, scope);
        }
    }
}

function lookup() {
    if(this == window) return lookup.call(lookup); // <- my hack'ish check

    var privateBar = 'private food'; // private local var
    this.bar = 'food'; // public local var
    MySvcWrap.doWork('thang', myHandler, this); // scope object is this
}

lookup();

console.log('global space - bar: ' + this.bar);
0
subhaze