Réponse rapide :
une portée enfant hérite normalement de sa portée parent de manière prototypique, mais pas toujours. Une exception à cette règle est une directive avec scope: { ... }
- cela crée une portée "isolée" qui n'hérite pas de manière prototypique. Cette construction est souvent utilisée lors de la création d'une directive "composant réutilisable".
En ce qui concerne les nuances, l'héritage de portée est normalement simple ... jusqu'à ce que vous ayez besoin d' une liaison de données bidirectionnelle (c'est-à-dire, des éléments de formulaire, un modèle ng) dans la portée enfant. Ng-repeat, ng-switch et ng-include peuvent vous déclencher si vous essayez de vous lier à une primitive (par exemple, nombre, chaîne, booléen) dans la portée parent depuis l'intérieur de la portée enfant. Cela ne fonctionne pas comme la plupart des gens pensent que cela devrait fonctionner. La portée enfant obtient sa propre propriété qui masque / masque la propriété parent du même nom. Vos solutions de contournement sont
- définir des objets dans le parent de votre modèle, puis référencer une propriété de cet objet dans l'enfant: parentObj.someProp
- utilisez $ parent.parentScopeProperty (pas toujours possible, mais plus facile que 1. si possible)
- définir une fonction sur la portée parent et l'appeler depuis l'enfant (pas toujours possible)
Les développeurs nouveaux AngularJS souvent ne réalisent pas que ng-repeat
, ng-switch
, ng-view
, ng-include
et ng-if
tout créer de nouveaux champs d' application de l' enfant, de sorte que le problème se manifeste souvent lorsque ces directives sont impliqués. (Voir cet exemple pour une illustration rapide du problème.)
Ce problème avec les primitives peut être facilement évité en suivant la "meilleure pratique" de toujours avoir un '.' dans vos modèles ng - regardez 3 minutes. Misko démontre le problème de liaison primitif avec ng-switch
.
Avoir un '.' dans vos modèles garantira que l'héritage prototypique est en jeu. Alors, utilisez
<input type="text" ng-model="someObj.prop1">
<!--rather than
<input type="text" ng-model="prop1">`
-->
Réponse longue :
Héritage prototypique JavaScript
Également placé sur le wiki AngularJS: https://github.com/angular/angular.js/wiki/Understanding-Scopes
Il est important d'avoir d'abord une solide compréhension de l'héritage prototypique, surtout si vous venez d'un arrière-plan côté serveur et que vous êtes plus familier avec l'héritage classique. Examinons donc cela en premier.
Supposons que parentScope possède les propriétés aString, aNumber, anArray, anObject et aFunction. Si childScope hérite de manière prototype de parentScope, nous avons:
(Notez que pour économiser de l'espace, je montre l' anArray
objet comme un seul objet bleu avec ses trois valeurs, plutôt que comme un seul objet bleu avec trois littéraux gris distincts.)
Si nous essayons d'accéder à une propriété définie sur le parentScope à partir de la portée enfant, JavaScript cherchera d'abord dans la portée enfant, ne trouvera pas la propriété, puis cherchera dans la portée héritée et trouvera la propriété. (S'il ne trouvait pas la propriété dans le parentScope, il continuerait la chaîne du prototype ... jusqu'à la portée racine). Donc, tout cela est vrai:
childScope.aString === 'parent string'
childScope.anArray[1] === 20
childScope.anObject.property1 === 'parent prop1'
childScope.aFunction() === 'parent output'
Supposons que nous fassions ensuite ceci:
childScope.aString = 'child string'
La chaîne prototype n'est pas consultée et une nouvelle propriété aString est ajoutée à childScope. Cette nouvelle propriété masque / masque la propriété parentScope du même nom. Cela deviendra très important lorsque nous discuterons de ng-repeat et ng-include ci-dessous.
Supposons que nous fassions ensuite ceci:
childScope.anArray[1] = '22'
childScope.anObject.property1 = 'child prop1'
La chaîne prototype est consultée car les objets (anArray et anObject) ne sont pas trouvés dans childScope. Les objets se trouvent dans le parentScope et les valeurs des propriétés sont mises à jour sur les objets d'origine. Aucune nouvelle propriété n'est ajoutée à childScope; aucun nouvel objet n'est créé. (Notez que dans les tableaux et fonctions JavaScript sont également des objets.)
Supposons que nous fassions ensuite ceci:
childScope.anArray = [100, 555]
childScope.anObject = { name: 'Mark', country: 'USA' }
La chaîne de prototype n'est pas consultée et la portée enfant obtient deux nouvelles propriétés d'objet qui masquent / masquent les propriétés d'objet parentScope avec les mêmes noms.
Points à retenir:
- Si nous lisons childScope.propertyX et que childScope a propertyX, alors la chaîne de prototype n'est pas consultée.
- Si nous définissons childScope.propertyX, la chaîne prototype n'est pas consultée.
Un dernier scénario:
delete childScope.anArray
childScope.anArray[1] === 22 // true
Nous avons d'abord supprimé la propriété childScope, puis lorsque nous essayons d'accéder à nouveau à la propriété, la chaîne de prototype est consultée.
Héritage de portée angulaire
Les prétendants:
- Les éléments suivants créent de nouvelles étendues et héritent de manière prototypique: ng-repeat, ng-include, ng-switch, ng-controller, directive with
scope: true
, directive with transclude: true
.
- Ce qui suit crée une nouvelle portée qui n'hérite pas de manière prototypique: directive avec
scope: { ... }
. Cela crée une portée "isoler" à la place.
Notez que, par défaut, les directives ne créent pas de nouvelle portée - c'est-à-dire que la valeur par défaut est scope: false
.
ng-include
Supposons que nous ayons dans notre contrôleur:
$scope.myPrimitive = 50;
$scope.myObject = {aNumber: 11};
Et dans notre HTML:
<script type="text/ng-template" id="/tpl1.html">
<input ng-model="myPrimitive">
</script>
<div ng-include src="'/tpl1.html'"></div>
<script type="text/ng-template" id="/tpl2.html">
<input ng-model="myObject.aNumber">
</script>
<div ng-include src="'/tpl2.html'"></div>
Chaque ng-include génère une nouvelle portée enfant, qui hérite de manière prototypique de la portée parent.
En tapant (par exemple, "77") dans la première zone de texte d'entrée, la portée enfant obtient une nouvelle myPrimitive
propriété de portée qui masque / masque la propriété de portée parent du même nom. Ce n'est probablement pas ce que vous voulez / attendez.
Taper (par exemple, "99") dans la deuxième zone de texte d'entrée n'entraîne pas une nouvelle propriété enfant. Étant donné que tpl2.html lie le modèle à une propriété d'objet, l'héritage prototypique intervient lorsque le ngModel recherche l'objet myObject - il le trouve dans la portée parent.
Nous pouvons réécrire le premier modèle à utiliser $ parent, si nous ne voulons pas changer notre modèle d'une primitive en un objet:
<input ng-model="$parent.myPrimitive">
Taper (par exemple, "22") dans cette zone de texte d'entrée n'entraîne pas de nouvelle propriété enfant. Le modèle est désormais lié à une propriété de la portée parent (car $ parent est une propriété de portée enfant qui fait référence à la portée parent).
Pour toutes les étendues (prototypiques ou non), Angular suit toujours une relation parent-enfant (c'est-à-dire une hiérarchie), via les propriétés d'étendue $ parent, $$ childHead et $$ childTail. Normalement, je ne montre pas ces propriétés de portée dans les diagrammes.
Pour les scénarios où les éléments de formulaire ne sont pas impliqués, une autre solution consiste à définir une fonction sur la portée parent pour modifier la primitive. Assurez-vous ensuite que l'enfant appelle toujours cette fonction, qui sera disponible pour la portée enfant en raison de l'héritage prototypique. Par exemple,
// in the parent scope
$scope.setMyPrimitive = function(value) {
$scope.myPrimitive = value;
}
Voici un exemple de violon qui utilise cette approche de «fonction parent». (Le violon a été écrit dans le cadre de cette réponse: https://stackoverflow.com/a/14104318/215945 .)
Voir également https://stackoverflow.com/a/13782671/215945 et https://github.com/angular/angular.js/issues/1267 .
ng-switch
L'héritage de la portée de ng-switch fonctionne exactement comme ng-include. Donc, si vous avez besoin d'une liaison de données bidirectionnelle à une primitive dans la portée parent, utilisez $ parent ou modifiez le modèle pour qu'il soit un objet, puis liez-le à une propriété de cet objet. Cela évitera que la portée enfant ne masque / masque les propriétés de la portée parent.
Voir aussi AngularJS, bind scope of a switch-case?
ng-repeat
Ng-repeat fonctionne un peu différemment. Supposons que nous ayons dans notre contrôleur:
$scope.myArrayOfPrimitives = [ 11, 22 ];
$scope.myArrayOfObjects = [{num: 101}, {num: 202}]
Et dans notre HTML:
<ul><li ng-repeat="num in myArrayOfPrimitives">
<input ng-model="num">
</li>
<ul>
<ul><li ng-repeat="obj in myArrayOfObjects">
<input ng-model="obj.num">
</li>
<ul>
Pour chaque élément / itération, ng-repeat crée une nouvelle étendue, qui hérite de manière prototypique de l'étendue parent, mais il affecte également la valeur de l'élément à une nouvelle propriété sur la nouvelle étendue enfant . (Le nom de la nouvelle propriété est le nom de la variable de boucle.) Voici ce qu'est réellement le code source angulaire pour ng-repeat:
childScope = scope.$new(); // child scope prototypically inherits from parent scope
...
childScope[valueIdent] = value; // creates a new childScope property
Si item est une primitive (comme dans myArrayOfPrimitives), une copie de la valeur est essentiellement affectée à la nouvelle propriété de portée enfant. La modification de la valeur de la propriété de la portée enfant (c'est-à-dire en utilisant ng-model, donc la portée enfant num
) ne change pas le tableau auquel la portée parent fait référence. Ainsi, dans la première répétition ng ci-dessus, chaque portée enfant obtient une num
propriété indépendante du tableau myArrayOfPrimitives:
Cette répétition ng ne fonctionnera pas (comme vous le souhaitez / attendez). La saisie dans les zones de texte modifie les valeurs dans les zones grises, qui ne sont visibles que dans les étendues enfants. Ce que nous voulons, c'est que les entrées affectent le tableau myArrayOfPrimitives, et non une propriété primitive de portée enfant. Pour ce faire, nous devons changer le modèle pour qu'il soit un tableau d'objets.
Ainsi, si l'élément est un objet, une référence à l'objet d'origine (pas une copie) est affectée à la nouvelle propriété de portée enfant. Modification de la valeur de la propriété portée des enfants (à l'aide ng-modèle, donc obj.num
) fait changer l'objet les références de la portée des parents. Donc, dans la deuxième répétition ng ci-dessus, nous avons:
(J'ai coloré une ligne en gris juste pour qu'il soit clair où il va.)
Cela fonctionne comme prévu. La saisie dans les zones de texte modifie les valeurs dans les zones grises, qui sont visibles pour les étendues enfant et parent.
Voir aussi Difficulté avec ng-model, ng-repeat et entrées et
https://stackoverflow.com/a/13782671/215945
ng-controller
L'imbrication de contrôleurs utilisant ng-controller entraîne un héritage prototypique normal, tout comme ng-include et ng-switch, donc les mêmes techniques s'appliquent. Cependant, "il est considéré comme une mauvaise forme pour deux contrôleurs de partager des informations via l'héritage $ scope" - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/
Un service doit être utilisé pour partager des données entre contrôleurs à la place.
(Si vous voulez vraiment partager des données via l'héritage de l'étendue des contrôleurs, vous n'avez rien à faire. L'étendue enfant aura accès à toutes les propriétés de l'étendue parent. Voir aussi L'ordre de chargement du contrôleur diffère lors du chargement ou de la navigation )
directives
- default (
scope: false
) - la directive ne crée pas de nouvelle portée, il n'y a donc pas d'héritage ici. C'est facile, mais aussi dangereux parce que, par exemple, une directive peut penser qu'elle crée une nouvelle propriété sur la portée, alors qu'en fait elle détruit une propriété existante. Ce n'est pas un bon choix pour écrire des directives qui sont conçues comme des composants réutilisables.
scope: true
- la directive crée une nouvelle portée enfant qui hérite de manière prototypique de la portée parent. Si plusieurs directives (sur le même élément DOM) demandent une nouvelle portée, une seule nouvelle portée enfant est créée. Étant donné que nous avons un héritage prototypique "normal", cela ressemble à ng-include et ng-switch, alors méfiez-vous de la liaison de données bidirectionnelle aux primitives de portée parent, et du masquage / duplication de la portée enfant des propriétés de la portée parent.
scope: { ... }
- la directive crée une nouvelle portée isolée / isolée. Il n'hérite pas de manière prototypique. C'est généralement votre meilleur choix lors de la création de composants réutilisables, car la directive ne peut pas lire ou modifier accidentellement la portée parent. Cependant, ces directives ont souvent besoin d'accéder à quelques propriétés de portée parent. Le hachage d'objet est utilisé pour configurer la liaison bidirectionnelle (en utilisant '=') ou la liaison unidirectionnelle (en utilisant '@') entre la portée parent et la portée isolée. Il y a aussi '&' pour se lier aux expressions de portée parent. Ainsi, ils créent tous des propriétés de portée locale dérivées de la portée parent. Notez que les attributs sont utilisés pour aider à configurer la liaison - vous ne pouvez pas simplement référencer les noms de propriété de portée parent dans le hachage d'objet, vous devez utiliser un attribut. Par exemple, cela ne fonctionnera pas si vous souhaitez vous lier à la propriété parentparentProp
dans le domaine isolé: <div my-directive>
et scope: { localProp: '@parentProp' }
. Un attribut doit être utilisé pour spécifier chaque propriété parent à laquelle la directive veut se lier: <div my-directive the-Parent-Prop=parentProp>
et scope: { localProp: '@theParentProp' }
.
Isoler les __proto__
références de la portée Objet. Le parent $ de la portée d'isolat fait référence à la portée parent, donc bien qu'il soit isolé et n'hérite pas de manière prototypique de la portée parent, il s'agit toujours d'une portée enfant.
Pour l'image ci-dessous, nous avons
<my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">
et
scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
supposons également que la directive le fait dans sa fonction de liaison: scope.someIsolateProp = "I'm isolated"
Pour plus d'informations sur les étendues d'isolement, voir http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
transclude: true
- la directive crée une nouvelle portée enfant "transclue", qui hérite de manière prototypique de la portée parent. La portée transclue et la portée isolée (le cas échéant) sont des frères et sœurs - la propriété $ parent de chaque portée fait référence à la même portée parent. Quand une portée transclude et une portée isolate existent toutes les deux, la propriété de portée isolate $$ nextSibling fera référence à la portée transclue. Je ne connais aucune nuance avec la portée transclue.
Pour l'image ci-dessous, supposez la même directive que ci-dessus avec cet ajout:transclude: true
Ce violon a une showScope()
fonction qui peut être utilisée pour examiner une portée isolée et transclue. Voir les instructions dans les commentaires au violon.
Sommaire
Il existe quatre types de portées:
- héritage de portée prototypique normal - ng-include, ng-switch, ng-controller, directive with
scope: true
- héritage de portée prototypique normal avec une copie / affectation - ng-repeat. Chaque itération de ng-repeat crée une nouvelle portée enfant, et cette nouvelle portée enfant obtient toujours une nouvelle propriété.
- isoler la portée - directive avec
scope: {...}
. Celui-ci n'est pas prototypique, mais '=', '@' et '&' fournissent un mécanisme pour accéder aux propriétés de portée parent, via des attributs.
- champ d'application exclu - directive avec
transclude: true
. Celui-ci est également un héritage de portée prototypique normal, mais il est également un frère de toute portée d'isolat.
Pour toutes les étendues (prototypiques ou non), Angular suit toujours une relation parent-enfant (c'est-à-dire une hiérarchie), via les propriétés $ parent et $$ childHead et $$ childTail.
Des diagrammes ont été générés avec graphvizFichiers "* .dot", qui sont sur github . " Learning JavaScript with Object Graphs " de Tim Caswell a été l'inspiration pour utiliser GraphViz pour les diagrammes.