Quelles sont les nuances de la portée prototypal / prototypical hérédity dans AngularJS?

Le Référence de l'API de la Portée de la page dit:

Un champ peut hériter d'un parent.

Le Guide du Développeur de la Portée de la page dit:

Un champ d'application (fait) hérite des propriétés de son parent.

donc, est-ce qu'un enfant scope hérite toujours prototypiquement à partir de son parent? Sont t-il des exceptions? Quand il hérite, est-ce toujours un héritage prototypique JavaScript normal?

981
demandé sur Peter Mortensen 2012-12-27 08:48:31

3 réponses

réponse Rapide :

Un enfant normalement fait hérite de son parent, mais pas toujours. Une exception à cette règle est une directive avec scope: { ... } -- cela crée une portée "isolante" qui n'hérite pas prototypiquement. Cette construction est souvent utilisée lors de la création d'une directive "composant réutilisable".

quant aux nuances, la portée héréditaire est normalement en ligne droite... jusqu'à ce que vous avez besoin liaison bidirectionnelle de données (c.-à-d. éléments de forme, modèle ng) dans le champ d'application enfant. Ng-repeat, ng-switch, et ng-include peuvent vous faire trébucher si vous essayez de vous lier à un primitif (e.g., nombre, chaîne, booléen) dans le champ parent à partir de l'intérieur du champ enfant. Ça ne marche pas comme la plupart des gens s'y attendent. L'enfant scope obtient sa propre propriété qui cache / cache la propriété parent du même nom. Vos solutions de rechange sont

  1. définir des objets dans le parent pour votre modèle, puis référencer une propriété de cet objet dans l'enfant: parentObj.someProp
  2. utilisez $ parent.parentScopeProperty (pas toujours possible, mais plus facile que 1. si possible)
  3. définir une fonction sur le champ parent, et l'appeler de l'enfant (pas toujours possible)

les nouveaux développeurs AngularJS souvent ne se rendent pas compte que ng-repeat , ng-switch , ng-view , ng-include et ng-if tous créent de nouvelles portées pour enfants, de sorte que le problème apparaît souvent lorsque ces directives sont impliquées. (Voir cet exemple pour une illustration rapide du problème.)

ce problème avec les primitifs peut être facilement évité en suivant la" meilleure pratique "de toujours avoir un".'dans vos modèles ng -montre de 3 minutes. Misko démontre le problème de liaison primitif avec ng-switch .

ayant un"."dans vos modèles s'assurera que l'héritage prototypique est en jeu. Ainsi, utilisez

<input type="text" ng-model="someObj.prop1">

<!--rather than
<input type="text" ng-model="prop1">`
-->


L-g réponse :

Héritage Prototypal JavaScript

également placé sur le wiki AngularJS: https://github.com/angular/angular.js/wiki/Understanding-Scopes

il est important d'avoir d'abord une bonne compréhension de l'héritage prototypique, surtout si vous venez d'un fond côté serveur et que vous êtes plus familier avec l'héritage classique. Donc, examinons cela d'abord.

supposons que parentScope possède des propriétés aString, aNumber, anArray, anObject, et aFunction. Si childScope prototypically hérite de parentScope, nous avons:

prototypal inheritance

(notez que pour économiser de l'espace, je montre l'objet anArray comme un seul objet bleu avec ses trois valeurs, plutôt qu'un seul objet bleu avec trois littérales grises séparées.)

si nous essayons d'accéder à une propriété définie sur le parentScope à partir du child scope, JavaScript va d'abord chercher dans le child scope, non pas trouver la propriété, puis regarder dans le champ hérité, et trouver la propriété. (S'il n'a pas trouvé la propriété dans le parentScope, il continuerait jusqu'à la chaîne du prototype... tout le chemin jusqu'à la racine de la portée). Donc, tout cela est vrai:

childScope.aString === 'parent string'
childScope.anArray[1] === 20
childScope.anObject.property1 === 'parent prop1'
childScope.aFunction() === 'parent output'

supposons alors que nous fassions ceci:

childScope.aString = 'child string'

la chaîne prototype n'est pas consultée, et une nouvelle propriété aString est ajoutée au childScope. cette nouvelle propriété cache la propriété parentScope du même nom. cela deviendra très important quand nous discutons ng-répéter et ng-inclure ci-dessous.

property hiding

supposons alors que nous fassions ceci:

childScope.anArray[1] = '22'
childScope.anObject.property1 = 'child prop1'

la chaîne prototype est consultée car les objets (anArray et anObject) ne se trouvent pas dans le child-scope. Les objets se trouvent dans le paramètre parentScope, et les valeurs des propriétés sont mises à jour sur les objets originaux. Aucune nouvelle propriété n'est ajoutée à childScope; aucun nouvel objet n'est créé. (Notez que dans les tableaux JavaScript et les fonctions sont aussi des objets.)

follow the prototype chain

supposons alors que nous fassions ceci:

childScope.anArray = [100, 555]
childScope.anObject = { name: 'Mark', country: 'USA' }

la chaîne prototype n'est pas consultée, et child scope obtient deux nouvelles propriétés d'objet qui masquent/ombragent les propriétés d'objet parentScope avec les mêmes noms.

more property hiding

plats à Emporter:

  • si nous lisez childScope.propertyX, et childScope a propertyX, alors la chaîne prototype n'est pas consultée.
  • si on met 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 supprimé la propriété childScope en premier, puis quand nous essayons d'accéder à nouveau à la propriété, la chaîne prototype est consultée.

after removing a child property


Angulaire De La Portée De L'Héritage

Les prétendants:

  • ce qui suit crée de nouvelles portées, et hérite prototypiquement: ng-repeat, ng-include, ng-switch, ng-controller, directive avec scope: true , directive avec transclude: true .
  • ce qui suit crée une nouvelle portée qui n'hérite pas prototypiquement: directive avec scope: { ... } . Cela crée un "isoler" la portée plutôt.

Notez, par défaut, que les directives ne créent pas de nouvelle portée, c'est-à-dire que la valeur par défaut est scope: false .

ng-inclure

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 un nouveau child scope, qui hérite prototypiquement du parent scope.

ng-include child scopes

en tapant (say," 77") dans la première boîte de texte d'entrée, l'enfant scope obtient une nouvelle propriété myPrimitive scope qui cache la propriété parent scope du même nom. Ce n'est probablement pas ce que vous voulez/attendre.

ng-include with a primitive

taper (say," 99") dans la deuxième zone de saisie n'entraîne pas la création d'un nouveau bien pour enfant. Parce que tpl2.html lie le modèle d'un objet propriété, héritage prototypique s'active lorsque le ngModel recherche un objet myObject -- il le trouve dans le champ parent.

ng-include with an object

nous pouvons réécrire le premier modèle pour utiliser $parent, si nous ne voulons pas changer notre modèle d'une primitive à un objet:

<input ng-model="$parent.myPrimitive">

taper (disons," 22") dans cette zone de saisie n'entraîne pas la création d'une nouvelle propriété d'enfant. Le modèle est maintenant lié à une propriété de la portée pour le parent (parce que $parent est une propriété de la portée pour l'enfant qui renvoie à la portée pour le parent).

ng-include with $parent

pour tous les scopes (prototypes ou non), Angular suit toujours une relation parent-enfant (c.-à-d. une hiérarchie), via les propriétés scope $parent, $$childHead et $$childTail. D'habitude, je ne montre pas ces propriétés dans les diagrammes.

pour les scénarios dans lesquels les éléments de forme ne sont pas impliqués, une autre solution est de définir une fonction sur le scope parent pour modifier la primitive. Ensuite, s'assurer que l'enfant appelle toujours cette fonction, qui sera disponible pour l'enfant en raison de prototypes à l'héritage. Par exemple,

// in the parent scope
$scope.setMyPrimitive = function(value) {
     $scope.myPrimitive = value;
}

voici un sample fiddle qui utilise cette approche de" fonction mère". (Le violon a été écrit dans le cadre de cette réponse: https://stackoverflow.com/a/14104318/215945 .)

Voir aussi https://stackoverflow.com/a/13782671/215945 et https://github.com/angular/angular.js/issues/1267 .

ng-switch

ng-switch scope héritage fonctionne tout comme ng-include. Donc, si vous avez besoin de données bidirectionnelles se liant à une primitive dans le champ parent, utilisez $parent, ou changez le modèle pour être un objet et puis se lient à une propriété de cet objet. Cela permettra d'éviter la portée de l'enfant masquage / ombrage des propriétés de champ parent.

Voir aussi AngularJS, lier la portée d'un cas de commutateur?

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 item / itération, ng-repeat crée une nouvelle portée, qui hérite prototypiquement de la champ d'application parent, mais il attribue également la valeur de l'article à un nouveau bien sur le nouveau champ d'application enfant . (Le nom de la nouvelle propriété est la variable de boucle.) Voici ce que le code source angulaire pour ng-repeat est en fait:

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), essentiellement une copie de la valeur est assignée à la nouvelle propriété child scope. Changer la valeur de la propriété child scope (c.-à-D., en utilisant ng-model, donc child champ d'application num ) ne pas changer la pile de la portée parent références. Ainsi, dans le premier ng-repeat ci-dessus, chaque enfant scope obtient une propriété num qui est indépendante du tableau myArrayOfPrimitives:

ng-repeat with primitives

ce ng-repeat ne fonctionnera pas (comme vous voulez/attendez qu'il fonctionne). Taper dans les boîtes de texte change les valeurs dans les boîtes grises, qui ne sont visibles que dans les portées enfants. Ce que nous voulons, c'est que les entrées affectent le tableau myArrayOfPrimitives, pas une propriété primitive child scope. Pour ce faire, nous devons changer le modèle pour être un tableau d'objets.

ainsi, si item est un objet, une référence à l'objet original (et non une copie) est assignée à la propriété new child scope. Changer la valeur de la propriété child scope (i.e., en utilisant le modèle ng, donc obj.num ) change l'objet the parent scope référence. Ainsi, dans la deuxième ng-répéter ci-dessus, nous avons:

ng-repeat with objects

(j'ai coloré d'une ligne grise juste de sorte qu'il est très bien où il va.)

Cela fonctionne comme prévu. Taper dans les boîtes de texte change les valeurs dans les boîtes grises, qui sont visibles à la fois pour les enfants et les parents.

Voir aussi difficulté avec ng-model, ng-repeat, and inputs et https://stackoverflow.com/a/13782671/215945

ng-controller

les contrôleurs de nidification utilisant ng-controller donnent un héritage prototypique normal, tout comme ng-include et ng-switch, de sorte que les mêmes techniques s'appliquent. Toutefois, "il est considéré comme mauvais pour les deux contrôleurs de partager des informations via $la portée de l'héritage" -- http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture / Un service devrait plutôt être utilisé pour partager des données entre les contrôleurs.

(si vous voulez vraiment partager des données via les contrôleurs scope Heritage, il n'y a rien que vous devez faire. L'enfant scope aura accès à toutes les propriétés parent scope. Voir aussi l'ordre de charge du contrôleur diffère lors du chargement ou de la navigation )

Directives

  1. 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 pourrait penser qu'elle crée une nouvelle propriété sur le champ d'application, alors qu'en fait elle abat une propriété existante. Ce n'est pas un bon choix pour écrire des directives qui sont conçues comme des composants réutilisables.
  2. scope: true - la directive crée un nouveau champ d'application pour les enfants prototypiquement hérite de la portée mère. Si plus d'une directive (sur le même élément DOM) demande un nouveau champ d'application, un seul nouveau champ d'application enfant est créé. Puisque nous avons un héritage de prototypage "normal", c'est comme ng-include et ng-switch, donc méfiez-vous de la liaison de données à deux voies aux primitives parent scope, et child scope Cacher/shadowing des propriétés parent scope.
  3. scope: { ... } - la directive crée un nouveau champ d'application isolat/isolé. Il n'a pas fait hériter. C'est généralement votre meilleur choix lors de la création de composants réutilisables, puisque la directive ne peut pas accidentellement lire ou modifier le champ d'application parent. Toutefois, de telles directives nécessitent souvent l'accès à quelques propriétés de parent scope. Le hachage objet est utilisé pour configurer la liaison bidirectionnelle (en utilisant '=') ou la liaison unidirectionnelle (en utilisant '@') entre le champ parent et le champ isolate. Il y a aussi '&' à lier aux expressions parent scope. Donc, tout cela crée des propriétés de portée locale qui sont dérivées de scope 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és parent scope dans le hachage d'objet, vous devez utiliser un attribut. Par exemple, cela ne fonctionnera pas si vous voulez lier à la propriété mère parentProp dans le champ d'application isolé: <div my-directive> et scope: { localProp: '@parentProp' } . Un attribut doit être utilisé pour spécifier chaque propriété mère que la directive veut lier à: <div my-directive the-Parent-Prop=parentProp> et scope: { localProp: '@theParentProp' } .

    Isole scope __proto__ références objet. Isolate $parent de scope fait référence au scope parent, donc bien qu'il soit isolé et n'hérite pas prototypiquement du scope parent, il reste un scope enfant.

    Pour l'image ci-dessous, nous avons

    <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2"> et

    scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }

    aussi, supposons que la directive le fasse dans sa fonction de lien: scope.someIsolateProp = "I'm isolated"

    isolated scope

    pour plus d'information sur les champs d'isolement, voir http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope /
  4. transclude: true - la directive crée un nouveau champ d'application" transclusif " pour les enfants, qui hérite prototypiquement du champ d'application parent. Le transcluse et le champ d'application isolé (s'il y en a) sont des frères et sœurs -- la propriété $parent de chaque champ d'application renvoie au même champ d'application parent. Lorsqu'un transcluded et un isoler la portée les deux existent, isoler la propriété de la portée $ $ nextSibling fera référence à la portée transcluse. Je ne suis pas au courant d'aucune nuance avec la portée transclustrée.

    pour l'image ci-dessous, supposons la même directive que ci-dessus avec cet ajout: transclude: true

    transcluded scope

ce violon a une fonction showScope() qui peut être utilisée pour examiner un isolat et champ de vision transclusif. Voir les instructions dans les commentaires en coulisse.


résumé

il existe quatre types de portées:

  1. transmission normale de portée prototypale -- ng-include, ng-switch, ng-controller, directive avec scope: true
  2. transmission normale de portée prototypique avec copie / assignation -- ng-repeat. Chaque itération de ng-repeat crée un nouvel enfant portée, et que de nouvelles enfant obtient toujours une nouvelle propriété.
  3. isole champ d'application -- directive avec scope: {...} . Celle-ci n'est pas prototypique, mais '=', '@', et '&' fournissent un mécanisme pour accéder aux propriétés parent scope, via des attributs.
  4. transcluse scope -- directive with transclude: true . Celle-ci est aussi une hérédité normale de portée prototypique, mais elle est aussi une sœur de n'importe quelle portée d'isolat.

pour tous les domaines (prototype ou non), Angular suit toujours une relation parent-enfant (c.-à-d. une hiérarchie), via les propriétés $parent et $$childHead et $$childTail.

Les diagrammes

ont été générés avec "*.dot" les fichiers qui sont sur le github . Le " de Tim Caswell, "Learning JavaScript with Object Graphs " a été l'inspiration pour L'utilisation de GraphViz pour les diagrammes.

1707
répondu Mark Rajcok 2017-05-23 12:02:49

Je ne veux en aucun cas rivaliser avec la réponse de Mark, mais je voulais juste mettre en évidence la pièce qui a finalement fait tout cliquer comme quelqu'un de nouveau à héritage Javascript et sa chaîne prototype .

seule la propriété lit rechercher la chaîne prototype, pas écrit. donc quand vous mettez

myObject.prop = '123';

il ne regarde pas la chaîne, mais quand vous mettez""

myObject.myThing.prop = '123';

il y a une lecture subtile à l'intérieur de cette opération d'écriture qui tente de chercher myThing avant d'écrire à son prop. C'est pourquoi l'écriture de l'objet.propriétés de l'enfant, la mère, les objets.

136
répondu Scott Driscoll 2014-07-17 12:09:38

je voudrais ajouter un exemple d'héritage prototypique avec javascript à la réponse @Scott Driscoll. Nous utiliserons le schéma d'héritage classique avec L'objet.create() qui fait partie de la spécification EcmaScript 5.

nous créons D'abord la fonction d'objet "Parent

function Parent(){

}

puis Ajouter un prototype à la fonction" Parent "de l'objet

 Parent.prototype = {
 primitive : 1,
 object : {
    one : 1
   }
}

Créer "Enfant" de la fonction de l'objet

function Child(){

}

Attribuer enfant prototype (enfants, prototype hérite de l'objet parent prototype)

Child.prototype = Object.create(Parent.prototype);

Attribuer un véritable "Enfant" prototype du constructeur

Child.prototype.constructor = Child;

ajouter la méthode " changeProps "à un prototype enfant, qui réécrira la valeur de propriété" primitive "dans L'objet enfant et modifiera" l'objet.une "valeur à la fois dans les objets enfant et Parent

Child.prototype.changeProps = function(){
    this.primitive = 2;
    this.object.one = 2;
};

initialise les objets Parent (dad) et enfant (son).

var dad = new Parent();
var son = new Child();

l'Appel de l'Enfant (fils) changeProps méthode

son.changeProps();

vérifier les résultats.

Parent primitives de la propriété n'a pas changé

console.log(dad.primitive); /* 1 */

enfant propriété primitive changée (réécrite)

console.log(son.primitive); /* 2 */

le Parent et l'Enfant objet.une propriété a changé

console.log(dad.object.one); /* 2 */
console.log(son.object.one); /* 2 */

exemple de travail ici http://jsbin.com/xexurukiso/1/edit /

Plus d'infos sur l'Objet.créer ici https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create

19
répondu tylik 2014-11-08 22:51:55