'this' vs $ scope dans les contrôleurs AngularJS

1027

Dans la section "Créer des composants" de la page d'accueil d'AngularJS , il y a cet exemple:

controller: function($scope, $element) {
  var panes = $scope.panes = [];
  $scope.select = function(pane) {
    angular.forEach(panes, function(pane) {
      pane.selected = false;
    });
    pane.selected = true;
  }
  this.addPane = function(pane) {
    if (panes.length == 0) $scope.select(pane);
    panes.push(pane);
  }
}

Remarquez comment la selectméthode est ajoutée $scope, mais la addPaneméthode est ajoutée à this. Si je le change en $scope.addPane, le code se casse.

La documentation indique qu'il y a en fait une différence, mais elle ne mentionne pas la différence:

Les versions précédentes d'Angular (pré 1.0 RC) vous permettaient d'utiliser de thismanière interchangeable avec la $scopeméthode, mais ce n'est plus le cas. À l'intérieur des méthodes définies sur la portée thiset $scopesont interchangeables (ensembles angulaires thisà $scope), mais pas autrement à l'intérieur du constructeur de votre contrôleur.

Comment fonctionne thiset $scopefonctionne dans les contrôleurs AngularJS?

Alexei Boronine
la source
Je trouve cela aussi déroutant. Lorsqu'une vue spécifie un contrôleur (par exemple, ng-controller = '...'), la portée $ associée à ce contrôleur semble l'accompagner, car la vue peut accéder aux propriétés de la portée $. Mais lorsqu'une directive 'requiert un autre contrôleur (et l'utilise ensuite dans sa fonction de liaison), la portée $ associée à cet autre contrôleur ne vient pas avec elle?
Mark Rajcok
Cette citation déroutante sur les "versions précédentes ..." a-t-elle été supprimée maintenant? Alors peut-être que la mise à jour serait en place?
Dmitri Zaitsev
Pour les tests unitaires, si vous utilisez «ceci» au lieu de «$ scope», vous ne pouvez pas injecter au contrôleur une portée simulée et vous ne pouvez donc pas effectuer de test unitaire. Je ne pense pas que ce soit une bonne pratique d'utiliser «ceci».
abentan

Réponses:

999

"Comment fonctionne thiset $scopefonctionne dans les contrôleurs AngularJS?"

Réponse courte :

  • this
    • Lorsque la fonction constructeur du contrôleur est appelée, thisest le contrôleur.
    • Lorsqu'une fonction définie sur un $scopeobjet est appelée, thisest la "portée en vigueur au moment de l'appel de la fonction". Il peut s'agir (ou non!) De la $scopedéfinition de la fonction. Donc, à l'intérieur de la fonction, thiset $scopepeut ne pas être le même.
  • $scope
    • Chaque contrôleur a un $scopeobjet associé .
    • Une fonction de contrôleur (constructeur) est chargée de définir les propriétés et les fonctions / le comportement du modèle sur son associé $scope.
    • Seules les méthodes définies sur cet $scopeobjet (et les objets de portée parent, si l'héritage prototypique est en jeu) sont accessibles à partir du HTML / de la vue. Par exemple, à partir de ng-click, filtres, etc.

Réponse longue :

Une fonction de contrôleur est une fonction constructeur JavaScript. Lorsque la fonction constructeur s'exécute (par exemple, lorsqu'une vue se charge), this(c'est-à-dire que le "contexte de fonction") est défini sur l'objet contrôleur. Donc, dans la fonction constructeur du contrôleur "tabs", lorsque la fonction addPane est créée

this.addPane = function(pane) { ... }

il est créé sur l'objet contrôleur, pas sur $ scope. Les vues ne peuvent pas voir la fonction addPane - elles n'ont accès qu'aux fonctions définies sur $ scope. En d'autres termes, dans le HTML, cela ne fonctionnera pas:

<a ng-click="addPane(newPane)">won't work</a>

Après l'exécution de la fonction de constructeur de contrôleur "tabs", nous avons ce qui suit:

après la fonction constructeur du contrôleur d'onglets

La ligne noire en pointillés indique l'héritage prototypique - un oscilloscope isolé hérite de Scope de manière prototypique . (Il n'hérite pas de manière prototypique de la portée en vigueur où la directive a été rencontrée dans le HTML.)

Maintenant, la fonction de lien de la directive volet veut communiquer avec la directive tabs (ce qui signifie vraiment qu'elle doit affecter les onglets isoler $ scope d'une certaine manière). Les événements peuvent être utilisés, mais un autre mécanisme consiste à avoir la directive de volet requirele contrôleur des onglets. (Il ne semble pas y avoir de mécanisme pour la directive de volet dans requireles onglets $ scope.)

Donc, cela pose la question: si nous n'avons accès qu'au contrôleur d'onglets, comment pouvons-nous accéder aux onglets isoler $ scope (ce que nous voulons vraiment)?

Eh bien, la ligne pointillée rouge est la réponse. La "portée" de la fonction addPane () (je fais référence ici à la portée / fermetures de la fonction JavaScript) donne à la fonction l'accès aux onglets isolant $ scope. C'est-à-dire, addPane () a accès aux "onglets IsolateScope" dans le diagramme ci-dessus en raison d'une fermeture qui a été créée lorsque addPane () a été défini. (Si nous définissions plutôt addPane () sur l'objet tabs $ scope, la directive pane n'aurait pas accès à cette fonction, et par conséquent, elle n'aurait aucun moyen de communiquer avec les tabs $ scope.)

Pour répondre à l'autre partie de votre question how does $scope work in controllers?:

Dans les fonctions définies sur $ scope, thisest défini sur "le $ scope en vigueur où / quand la fonction a été appelée". Supposons que nous ayons le code HTML suivant:

<div ng-controller="ParentCtrl">
   <a ng-click="logThisAndScope()">log "this" and $scope</a> - parent scope
   <div ng-controller="ChildCtrl">
      <a ng-click="logThisAndScope()">log "this" and $scope</a> - child scope
   </div>
</div>

Et le ParentCtrl(uniquement) a

$scope.logThisAndScope = function() {
    console.log(this, $scope)
}

Cliquer sur le premier lien montrera que thiset $scopesont les mêmes, puisque " la portée en vigueur lorsque la fonction a été appelée " est la portée associée à la ParentCtrl.

Cliquer sur le deuxième lien révélera thiset ne$scope sera pas le même, car " la portée en vigueur lorsque la fonction a été appelée " est la portée associée à la ChildCtrl. Donc , ici, thisest réglé sur ChildCtrl« s $scope. À l'intérieur de la méthode, se $scopetrouve toujours la ParentCtrlportée de $.

Violon

J'essaie de ne pas utiliser à l' thisintérieur d'une fonction définie sur $ scope, car cela devient déroutant quelle $ scope est affectée, d'autant plus que ng-repeat, ng-include, ng-switch et les directives peuvent tous créer leurs propres étendues enfants.

Mark Rajcok
la source
6
@tamakisquare, je crois que le texte en gras que vous avez cité s'applique lorsque la fonction constructeur du contrôleur est appelée - c'est-à-dire lorsque le contrôleur est créé = associé à une portée $. Elle ne s'applique pas plus tard, lorsqu'un code JavaScript arbitraire appelle une méthode définie sur un objet $ scope.
Mark Rajcok
79
Notez qu'il est désormais possible d'appeler la fonction addPane () directement dans le modèle en nommant le contrôleur: "MyController as myctrl" puis myctrl.addPane (). Voir docs.angularjs.org/guide/concepts#controller
Christophe Augier
81
Trop de complexité inhérente.
Inanc Gumus
11
C'est une réponse très informative, mais quand je suis revenu avec un problème pratique ( comment invoquer $ scope. $ Apply () dans une méthode de contrôleur définie en utilisant 'this' ), je n'ai pas pu le résoudre . Donc, bien que ce soit encore une réponse utile, je trouve déconcertant la «complexité inhérente».
dumbledad
11
Javascript - beaucoup de corde [pour vous pendre].
AlikElzin-kilaka
55

La raison pour laquelle «addPane» est attribué à cela est à cause de la <pane>directive.

La panedirective fait require: '^tabs', qui place l'objet contrôleur tabs d'une directive parent, dans la fonction de lien.

addPane est affecté à this afin que la panefonction de liaison puisse le voir. Ensuite, dans la panefonction de lien, addPanen'est qu'une propriété du tabscontrôleur, et ce n'est que tabsControllerObject.addPane. Ainsi, la fonction de liaison de la directive pane peut accéder à l'objet contrôleur tabs et donc accéder à la méthode addPane.

J'espère que mon explication est assez claire .. c'est un peu difficile à expliquer.

Andrew Joslin
la source
3
Merci pour l'explication. Les documents donnent à penser que le contrôleur n'est qu'une fonction qui définit la portée. Pourquoi le contrôleur est-il traité comme un objet si toutes les actions se produisent dans la portée? Pourquoi ne pas simplement passer la portée parent dans la fonction de liaison? Edit: Pour mieux formuler cette question, si les méthodes de contrôleur et les méthodes de portée fonctionnent toutes les deux sur la même structure de données (la portée), pourquoi ne pas les mettre toutes au même endroit?
Alexei Boronine
Il semble que la portée parent ne soit pas passée dans la fonction lnk en raison du désir de prendre en charge "les composants réutilisables, qui ne devraient pas accidentellement lire ou modifier les données dans la portée parent". Mais si une directive veut / doit vraiment lire ou modifier QUELQUES DONNÉES SPÉCIFIQUES dans la portée parent (comme le fait la directive 'pane'), cela demande un certain effort: 'exiger' le contrôleur où se trouve la portée parent souhaitée, puis définir un sur ce contrôleur (utilisez «this» et non $ scope) pour accéder à des données spécifiques. Étant donné que la portée parent souhaitée n'est pas injectée dans la fonction lnk, je suppose que c'est la seule façon de le faire.
Mark Rajcok
1
Hey Mark, il est en fait plus facile de modifier la portée de la directive. Vous pouvez simplement utiliser la fonction de lien jsfiddle.net/TuNyj
Andrew Joslin
3
Merci @Andy pour le violon. Dans votre violon, la directive ne crée pas de nouvelle portée, donc je peux voir comment la fonction de lien peut accéder directement à la portée du contrôleur ici (car il n'y a qu'une seule portée). Les onglets et les directives de volet utilisent des étendues isolées (c'est-à-dire que de nouvelles étendues enfants sont créées qui n'héritent pas de manière prototypique de la portée parent). Pour le cas de portée isolée, il semble que la définition d'une méthode sur un contrôleur (en utilisant «ceci») soit la seule façon de permettre à une autre directive d'obtenir un accès (indirect) à l'autre portée (isolée).
Mark Rajcok
27

Je viens de lire une explication assez intéressante sur la différence entre les deux, et une préférence croissante pour attacher des modèles au contrôleur et alias le contrôleur pour lier les modèles à la vue. http://toddmotto.com/digging-into-angulars-controller-as-syntax/ est l'article.
Il ne le mentionne pas mais lors de la définition des directives, si vous avez besoin de partager quelque chose entre plusieurs directives et que vous ne voulez pas de service (il y a des cas légitimes où les services sont un problème), attachez les données au contrôleur de la directive parent.

Le $scopeservice fournit de nombreuses choses utiles, $watchétant les plus évidentes, mais si tout ce dont vous avez besoin pour lier des données à la vue, utiliser le contrôleur ordinaire et le `` contrôleur comme '' dans le modèle est correct et sans doute préférable.

Derek
la source
20

Je vous recommande de lire le post suivant: AngularJS: "Controller as" ou "$ scope"?

Il décrit très bien les avantages d'utiliser "Controller as" pour exposer des variables sur "$ scope".

Je sais que vous avez posé des questions précises sur les méthodes et non sur les variables, mais je pense qu'il vaut mieux s'en tenir à une technique et y être cohérent.

Donc, à mon avis, en raison du problème des variables discuté dans le post, il est préférable d'utiliser simplement la technique "Controller as" et de l'appliquer également aux méthodes.

Liran Brimer
la source
16

Dans ce cours ( https://www.codeschool.com/courses/shaping-up-with-angular-js ), ils expliquent comment utiliser "ceci" et bien d'autres choses.

Si vous ajoutez une méthode au contrôleur via «cette» méthode, vous devez l'appeler dans la vue avec le nom du contrôleur «dot» votre propriété ou méthode.

Par exemple, en utilisant votre contrôleur dans la vue, vous pouvez avoir un code comme celui-ci:

    <div data-ng-controller="YourController as aliasOfYourController">

       Your first pane is {{aliasOfYourController.panes[0]}}

    </div>
Sandro
la source
6
Après avoir suivi le cours, j'ai été immédiatement confondu par l'utilisation de code $scope, alors merci de l'avoir mentionné.
Matt Montag
16
Ce cours ne mentionne pas du tout $ scope, ils utilisent simplement aset thisalors comment peut-il aider à expliquer la différence?
dumbledad
10
Mon premier contact avec Angular a été du cours mentionné, et comme cela $scopen'a jamais été mentionné, j'ai appris à utiliser uniquement thisdans les contrôleurs. Le problème est que lorsque vous commencez à gérer des promesses dans votre contrôleur, vous avez beaucoup de problèmes de références thiset devez commencer à faire des choses comme var me = thisréférencer le modèle à thispartir de la fonction de retour de promesse. Donc à cause de cela, je suis toujours très confus quant à la méthode à utiliser, $scopeou this.
Bruno Finger
@BrunoFinger Malheureusement, vous aurez besoin var me = thisou .bind(this)chaque fois que vous ferez des promesses, ou d'autres choses lourdes de fermeture. Cela n'a rien à voir avec Angular.
Dzmitry Lazerka
1
L'important est de savoir que cela ng-controller="MyCtrl as MC"équivaut à mettre $scope.MC = thisle contrôleur lui-même - il définit une instance (ceci) de MyCtrl sur la portée à utiliser dans le modèle via{{ MC.foo }}
William B
3

Les versions précédentes d'Angular (pré 1.0 RC) vous permettaient de l'utiliser de manière interchangeable avec la méthode $ scope, mais ce n'est plus le cas. À l'intérieur des méthodes définies sur l'étendue this et $ scope sont interchangeables (angular définit ceci sur $ scope), mais pas autrement à l'intérieur du constructeur de votre contrôleur.

Pour ramener ce comportement (quelqu'un sait-il pourquoi il a été changé?), Vous pouvez ajouter:

return angular.extend($scope, this);

à la fin de votre fonction de contrôleur (à condition que $ scope ait été injecté dans cette fonction de contrôleur).

Cela a un bel effet d'avoir accès à la portée parent via un objet contrôleur que vous pouvez obtenir avec l'enfant require: '^myParentDirective'

Kamil Szot
la source
7
Cet article explique clairement pourquoi ceci et $ scope sont différents.
Robert Martin
1

$ scope a un `` ceci '' différent du contrôleur `` ceci '' .Ainsi, si vous mettez un console.log (ceci) dans le contrôleur, il vous donne un objet (contrôleur) et this.addPane () ajoute la méthode addPane à l'objet contrôleur. Mais la portée $ a une portée différente et toutes les méthodes de sa portée doivent être accédées par $ scope.methodName (). this.methodName()à l'intérieur du contrôleur signifie ajouter des méthos à l'intérieur de l'objet contrôleur. $scope.functionName()est en HTML et à l'intérieur

$scope.functionName(){
    this.name="Name";
    //or
    $scope.myname="myname"//are same}

Collez ce code dans votre éditeur et ouvrez la console pour voir ...

 <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>this $sope vs controller</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.7/angular.min.js"></script>
    <script>
        var app=angular.module("myApp",[]);
app.controller("ctrlExample",function($scope){
          console.log("ctrl 'this'",this);
          //this(object) of controller different then $scope
          $scope.firstName="Andy";
          $scope.lastName="Bot";
          this.nickName="ABot";
          this.controllerMethod=function(){

            console.log("controllerMethod ",this);
          }
          $scope.show=function(){
              console.log("$scope 'this",this);
              //this of $scope
              $scope.message="Welcome User";
          }

        });
</script>
</head>
<body ng-app="myApp" >
<div ng-controller="ctrlExample">
       Comming From $SCOPE :{{firstName}}
       <br><br>
       Comming from $SCOPE:{{lastName}}
       <br><br>
       Should Come From Controller:{{nickName}}
       <p>
            Blank nickName is because nickName is attached to 
           'this' of controller.
       </p>

       <br><br>
       <button ng-click="controllerMethod()">Controller Method</button>

       <br><br>
       <button ng-click="show()">Show</button>
       <p>{{message}}</p>

   </div>

</body>
</html>
Aniket Jha
la source