Question Quelles sont les nuances de l'héritage prototype / prototypique dans AngularJS?


le Page Scope Reference de l'API dit:

Une portée pouvez hérite d'une portée parent.

le Page Scope Guide Scope dit:

Une portée (prototypiquement) hérite des propriétés de sa portée parente.

Ainsi, une portée enfant hérite-t-elle toujours prototypiquement de sa portée parente? Y at-il des exceptions? Quand il hérite, est-ce toujours l'héritage prototypique JavaScript normal?


965
2017-12-27 04:48


origine


Réponses:


Réponse rapide:
Normalement, une portée enfant hérite de manière prototypique de sa portée parent, mais pas toujours. Une exception à cette règle est une directive avec scope: { ... } - ceci crée une portée "isolate" qui n'hérite pas prototypiquement. 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 droit ... jusqu'à ce que vous ayez besoin Liaison de données bidirectionnelle (c'est-à-dire, des éléments de formulaire, ng-model) dans la portée enfant. Ng-repeat, ng-switch, et ng-include peuvent vous trébucher si vous essayez de vous lier à un primitif (par exemple, nombre, chaîne, booléen) dans la portée parent à l'intérieur de la portée enfant. Cela ne fonctionne pas comme la plupart des gens s'attendent à ce que cela fonctionne. La portée enfant obtient sa propre propriété qui masque / ombrage la propriété parent du même nom. Vos solutions de contournement sont

  1. Définissez les objets du parent pour votre modèle, puis référencez une propriété de cet objet dans l'enfant: parentObj.someProp
  2. use $ parent.parentScopeProperty (pas toujours possible, mais plus facile que 1. si possible)
  3. définir une fonction sur la portée parent et l'appeler depuis l'enfant (pas toujours possible)

Les nouveaux développeurs d'AngularJS ne réalisent souvent pas que ng-repeat, ng-switch, ng-view, ng-include et ng-if tous créent de nouvelles portées enfant, 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 primitives peut être facilement évité en suivant la "meilleure pratique" de toujours avoir un '.' dans vos modèles ng - regarder 3 minutes de valeur. Misko démontre le problème de liaison primitif avec ng-switch.

Avoir un '.' Dans vos modèles, l'héritage prototypique est en jeu. Donc, utilisez

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

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


Longue réponse:

Héritage prototypique JavaScript

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

Il est important d'avoir une bonne compréhension de l'héritage prototypique, en particulier si vous venez d'un environnement serveur et que vous êtes plus familier avec l'héritage classique. Donc, passons en revue cela en premier.

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

prototypal inheritance

(Notez que pour économiser de l'espace, je montre le anArray objet comme un seul objet bleu avec ses trois valeurs, plutôt qu'un seul objet bleu avec trois littéraux gris séparés.)

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 faisons alors ceci:

childScope.aString = 'child string'

La chaîne du 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 avec le même nom.  Cela deviendra très important lorsque nous discuterons ng-repeat et ng-include ci-dessous.

property hiding

Supposons que nous faisons alors ceci:

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

La chaîne du prototype est consultée car les objets (anArray et anObject) ne sont pas trouvés dans childScope. Les objets se trouvent dans parentScope et les valeurs de propriété sont mises à jour sur les objets d'origine. Aucune nouvelle propriété n'est ajoutée à childScope; aucun nouvel objet n'est créé. (Notez qu'en JavaScript, les tableaux et les fonctions sont aussi des objets.)

follow the prototype chain

Supposons que nous faisons alors ceci:

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

La chaîne 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 de l'objet parentScope avec les mêmes noms.

more property hiding

Plats à emporter:

  • Si nous lisons childScope.propertyX et childScope a la propriétéX, la chaîne du 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 prototype est consultée.

after removing a child property


Héritage de la 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 avec scope: true, directive avec transclude: true.
  • Ce qui suit crée une nouvelle portée qui n'hérite pas de façon 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 prototypiquement de la portée parent.

ng-include child scopes

La saisie (par exemple, "77") dans la première zone de texte d'entrée entraîne l'obtention par l'étendue enfant d'une nouvelle myPrimitive propriété scope qui masque / masque la propriété scope parent du même nom. Ce n'est probablement pas ce que vous voulez / attendez.

ng-include with a primitive

Taper (disons "99") dans la deuxième zone de texte d'entrée n'aboutit pas à une nouvelle propriété enfant. Comme tpl2.html lie le modèle à une propriété d'objet, l'héritage prototypique intervient lorsque le ngModel cherche l'objet myObject - il le trouve dans la portée 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 (par exemple, "22") dans cette zone de texte d'entrée n'aboutit pas à une nouvelle propriété enfant. Le modèle est maintenant 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).

ng-include with $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 de portée $ parent, $$ childHead et $$ childTail. Normalement, je ne montre pas ces propriétés dans les diagrammes.

Pour les scénarios dans lesquels 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 prototypal. Par exemple.,

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

Voici une échantillon de violon qui utilise cette approche "fonction parente". (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 ng-switch scope fonctionne exactement comme ng-include. Par conséquent, si vous avez besoin d'une liaison de données bidirectionnelle avec une primitive dans la portée parente, utilisez $ parent ou modifiez le modèle pour qu'il s'agisse d'un objet, puis liez-le à une propriété de cet objet. Cela évitera la dissimulation / l'observation de portée d'enfant des propriétés d'étendue parent.

Voir également AngularJS, lier la portée d'un 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>

Ng-repeat crée une nouvelle portée pour chaque item / itération, qui hérite prototypiquement de la portée parent, mais il affecte également la valeur de l'élément à une nouvelle propriété sur la portée de nouveau enfant. (Le nom de la nouvelle propriété est le nom de la variable de boucle.) Voici en quoi consiste le code source Angular 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 assignée à la nouvelle propriété de portée enfant. Modification de la valeur de la propriété d'étendue enfant (c'est-à-dire, utilisation de ng-model, d'où la portée enfant num) Est-ce que ne pas changez le tableau des références de portée parent. Donc, dans la première ng-repeat ci-dessus, chaque portée enfant obtient un num propriété indépendante du tableau myArrayOfPrimitives:

ng-repeat with primitives

Cette ng-repeat ne fonctionnera pas (comme vous le voulez / attendez). La saisie dans les zones de texte modifie les valeurs dans les zones grises, qui sont uniquement visibles dans les étendues enfant. Ce que nous voulons, c'est que les entrées affectent le tableau myArrayOfPrimitives, pas une propriété de primitive de portée enfant. Pour ce faire, nous devons changer le modèle en 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é d'étendue enfant. Modification de la valeur de la propriété d'étendue enfant (c'est-à-dire en utilisant ng-model, d'où obj.num) Est-ce que change l'objet référencé par la portée parent. Donc, dans la deuxième ng-repeat ci-dessus, nous avons:

ng-repeat with objects

(Je colorie une ligne grise juste pour qu'elle soit claire là où elle va.)

Cela fonctionne comme prévu. La saisie dans les zones de texte modifie les valeurs dans les zones grises, qui sont visibles à la fois sur les étendues enfant et parent.

Voir également Difficulté avec ng-model, ng-repeat et entrées et https://stackoverflow.com/a/13782671/215945

ng-controller

L'imbrication de contrôleurs à l'aide de ng-controller produit 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 devrait être utilisé pour partager des données entre les contrôleurs à la place.

(Si vous voulez vraiment partager des données via l'héritage de la portée des contrôleurs, vous n'avez rien à faire.) La portée enfant aura accès à toutes les propriétés de la portée parent. Voir également L'ordre de chargement du contrôleur diffère lors du chargement ou de la navigation)

directives

  1. défaut (scope: false) - la directive ne crée pas de nouvelle portée, donc il n'y a 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 la portée, alors qu'en fait elle est en train de taper 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 une nouvelle portée enfant qui hérite prototypiquement de la portée parent. Si plus d'une directive (sur le même élément DOM) demande une nouvelle portée, une seule nouvelle portée enfant est créée. Comme nous avons un héritage prototypique "normal", c'est comme ng-include et ng-switch, donc méfiez-vous de la liaison de données bidirectionnelle aux primitives de portée parent et de la dissimulation / observation de portée enfant des propriétés de portée parent.
  3. scope: { ... } - la directive crée une nouvelle portée isolate / isolée. Il n'hérite pas prototypiquement. C'est généralement le meilleur choix lorsque vous créez des composants réutilisables, car la directive ne peut pas lire ou modifier accidentellement la portée parente. Cependant, ces directives nécessitent souvent l'accès à quelques propriétés de portée parent. Le hachage d'objet est utilisé pour définir une liaison bidirectionnelle (en utilisant '=') ou une liaison unidirectionnelle (en utilisant '@') entre la portée parent et la portée isolate. Il y a aussi '&' pour lier aux expressions de portée parent. Ainsi, tous créent des propriétés de portée locales 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 la portée parent dans le hachage de l'objet, vous devez utiliser un attribut. Par exemple, cela ne fonctionnera pas si vous voulez lier à la propriété parent parentProp dans le périmètre isolé: <div my-directive> et scope: { localProp: '@parentProp' }. Un attribut doit être utilisé pour spécifier chaque propriété parente à laquelle la directive veut se lier: <div my-directive the-Parent-Prop=parentProp> et scope: { localProp: '@theParentProp' }.
    Isoler la portée __proto__ références Objet. Isoler $ parent de la portée fait référence à la portée parent, même si elle est isolée et n'hérite pas de façon prototypique de la portée parent, elle est toujours une portée enfant.
    Pour la photo ci-dessous nous avons
      <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2"> et
      scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
    Supposons également que la directive le fasse dans sa fonction de liaison: scope.someIsolateProp = "I'm isolated"
      isolated scope
    Pour plus d'informations sur les oscilloscopes, voir http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true- La directive crée une nouvelle portée enfant "transcluded", qui hérite prototypiquement de la portée parent. La portée transcluded 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. Lorsqu'une portée transcluded et une portée isolate existent, isoler la propriété d'étendue $$ nextSibling référencera l'étendue transcluded. Je ne suis pas au courant des nuances avec la portée transcluded.
    Pour l'image ci-dessous, supposons la même directive que ci-dessus avec cet ajout: transclude: true
    transcluded scope

Ce violon a un showScope() fonction qui peut être utilisée pour examiner une portée isolate et transcluded. Voir les instructions dans les commentaires dans le violon.


Résumé

Il y a quatre types d'étendues:

  1. héritage normal de portée prototypal - ng-include, ng-switch, ng-controller, directive avec scope: true
  2. héritage de portée prototypique normal avec une copie / assignation - 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é.
  3. isoler scope - directive avec scope: {...}. Celui-ci n'est pas prototypique, mais '=', '@' et '&' fournissent un mécanisme pour accéder aux propriétés de la portée parent, via des attributs.
  4. scope transclus - directive avec transclude: true. Celui-ci est également un héritage de portée prototypique normal, mais il est également un frère ou une sœur de toute portée isolée.

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.

Les diagrammes ont été générés avec  "* .dot" fichiers, qui sont sur github. Tim Caswell "Apprendre JavaScript avec des graphes d'objets"a été l'inspiration pour utiliser GraphViz pour les diagrammes.


1694
2017-12-27 04:48



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 cliqueter comme quelqu'un de nouveau à L'héritage Javascript et sa chaîne de prototypes.

Seules les lectures de propriétés recherchent la chaîne du prototype, pas les écritures. Donc, quand vous définissez

myObject.prop = '123';

Il ne regarde pas la chaîne, mais quand vous définissez

myObject.myThing.prop = '123';

il y a une lecture subtile dans cette opération d'écriture qui essaie de chercher myThing avant d'écrire à son prop. C'est pourquoi l'écriture dans object.properties de l'enfant obtient les objets du parent.


135
2018-05-16 15:22



Je voudrais ajouter un exemple d'héritage prototypique avec javascript à @Scott Driscoll réponse. Nous utiliserons un modèle d'héritage classique avec Object.create () qui fait partie de la spécification EcmaScript 5.

Nous créons d'abord la fonction d'objet "Parent"

function Parent(){

}

Ensuite, ajoutez un prototype à la fonction d'objet "Parent"

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

Créer une fonction d'objet "Enfant"

function Child(){

}

Affecter un prototype enfant (Make prototype prototype hérité du prototype parent)

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

Affecter un constructeur prototype "enfant" approprié

Child.prototype.constructor = Child;

Ajoutez la méthode "changeProps" à un prototype enfant, qui réécrira la valeur de la propriété "primitive" dans l'objet enfant et modifiera la valeur "object.one" à la fois dans les objets enfant et parent

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

Initier les objets Parent (papa) et Child (fils).

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

Call Child (fils) méthode changeProps

son.changeProps();

Vérifiez les résultats.

La propriété primitive parent n'a pas changé

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

Propriété primitive enfant modifiée (réécrite)

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

Les propriétés parent et child object.one ont été modifiées

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 Object.create ici https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create


18
2017-11-08 22:45