Website-Suche

javascript - Was sind die Nuancen des Scope-, prototypalen/prototypischen Erbes in AngularJS?


Die API-Referenzbereich-Seite sagt:

Ein Scope kann von einem übergeordneten Scope erben.

Die Seite zum Umfang des Entwicklerhandbuchs sagt:

Ein Scope erbt (prototypisch) Eigenschaften von seinem übergeordneten Scope.

  • Also, erbt ein Kind-Scope immer prototypisch von seinem Eltern-Scope?
  • Gibt es Ausnahmen?
  • Wenn es erbt, ist es dann immer normale JavaScript-Prototypenvererbung?

Antwort #1

Schnelle Antwort:
Ein Kind-Scope erbt normalerweise prototypisch von seinem Eltern-Scope, aber nicht immer. Eine Ausnahme von dieser Regel ist eine Direktive mit scope: { ... } - dies erzeugt ein „isoliertes“ Scope, das nicht prototypisch erbt. Diese Konstruktion wird häufig verwendet, wenn eine Direktive als „wiederverwendbare Komponente“ erstellt wird.

Was die Nuancen betrifft, ist die Vererbung von Scopes normalerweise unkompliziert... bis man eine zweiseitige Datenbindung (d. h. Formularelemente, ng-model) im Kind-Scope benötigt. Ng-repeat, ng-switch und ng-include können einem Probleme bereiten, wenn man versucht, in einem Kind-Scope an ein primitives Element (z. B. Zahl, String, Boolean) im Eltern-Scope zu binden. Es funktioniert nicht so, wie die meisten Leute erwarten würden. Der Kind-Scope erhält seine eigene Eigenschaft, die die gleichnamige Eigenschaft im Eltern-Scope verdeckt/überschattet. Ihre Umgehungslösungen sind

  1. Definieren Sie Objekte im übergeordneten Element für Ihr Modell, und beziehen Sie sich dann im Kind auf eine Eigenschaft dieses Objekts: parentObj.someProp
  2. verwende $parent.parentScopeProperty (nicht immer möglich, aber einfacher als 1., wo möglich)
  3. Definieren Sie eine Funktion im übergeordneten Bereich und rufen Sie sie vom Kind auf (nicht immer möglich)

Neue AngularJS-Entwickler erkennen oft nicht, dass ng-repeat, ng-switch, ng-view, ng-include und ng-if alle neue Kind-Scopes erstellen, sodass das Problem häufig auftritt, wenn diese Direktiven beteiligt sind. (Siehe dieses Beispiel für eine schnelle Veranschaulichung des Problems.)

Dieses Problem mit Primitiven kann leicht vermieden werden, indem man der „Best Practice“ folgt, immer einen '.' in seinen ng-modellen zu haben - sehen Sie sich 3 Minuten dazu an. Misko demonstriert das Problem der primitiven Bindung mit ng-switch.

Wenn Ihre Modelle einen '.' enthalten, wird sichergestellt, dass prototypische Vererbung aktiv ist. Verwenden Sie also

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

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


L-a-n-g-e Antwort:

JavaScript-Prototypenvererbung

Ebenfalls auf dem AngularJS-Wiki platziert: https://github.com/angular/angular.js/wiki/Understanding-Scopes

Es ist wichtig, zuerst ein solides Verständnis von prototypischer Vererbung zu haben, insbesondere wenn Sie aus einem Server-seitigen Hintergrund kommen und mit klassischer Vererbung vertrauter sind. Also lassen Sie uns das zuerst durchgehen.

Angenommen, parentScope hat die Eigenschaften aString, aNumber, anArray, anObject und aFunction. Wenn childScope prototypisch von parentScope erbt, haben wir:

(Beachten Sie, dass ich aus Platzgründen das anArray-Objekt als ein einzelnes blaues Objekt mit seinen drei Werten und nicht als ein einzelnes blaues Objekt mit drei separaten grauen Literalen darstelle.)

Wenn wir versuchen, auf eine im parentScope definierte Eigenschaft vom Child-Scope aus zuzugreifen, wird JavaScript zuerst im Child-Scope nachsehen, die Eigenschaft nicht finden und dann im geerbten Scope nachsehen und die Eigenschaft finden. (Wenn es die Eigenschaft im parentScope nicht gefunden hätte, würde es die Prototyp-Kette weiter nach oben verfolgen... bis zum Root-Scope). Also, dies ist alles wahr:

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

Angenommen, wir machen dann dies:

childScope.aString = 'child string'

Die Prototypkette wird nicht konsultiert, und eine neue aString-Eigenschaft wird zu childScope hinzugefügt. Diese neue Eigenschaft verdeckt/schattet die parentScope-Eigenschaft mit demselben Namen. Dies wird sehr wichtig werden, wenn wir unten über ng-repeat und ng-include sprechen.

Angenommen, wir machen dann dies:

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

Die Prototypkette wird konsultiert, weil die Objekte (anArray und anObject) im childScope nicht gefunden werden. Die Objekte werden im parentScope gefunden, und die Eigenschaftswerte werden an den ursprünglichen Objekten aktualisiert. Es werden keine neuen Eigenschaften zum childScope hinzugefügt; es werden keine neuen Objekte erstellt. (Beachten Sie, dass in JavaScript Arrays und Funktionen ebenfalls Objekte sind.)

Angenommen, wir machen dann dies:

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

Die Prototypenkette wird nicht berücksichtigt, und der Kind-Scope erhält zwei neue Objekteigenschaften, die die Objekteigenschaften des Parent-Scope mit denselben Namen verbergen/schatten.

Erkenntnisse:

  • Wenn wir childScope.propertyX lesen und childScope besitzt propertyX, wird die Prototypkette nicht konsultiert.
  • Wenn wir childScope.propertyX setzen, wird die Prototypkette nicht berücksichtigt.

Noch ein letztes Szenario:

delete childScope.anArray
childScope.anArray[1] === 22  // true

Wir haben zunächst die Eigenschaft childScope gelöscht, dann wird beim erneuten Zugriff auf die Eigenschaft die Prototypkette konsultiert.

Angular-Scope-Vererbung

Die Anwärter:

  • Die folgenden erstellen neue Bereiche und erben prototypisch: ng-repeat, ng-include, ng-switch, ng-controller, Direktive mit scope: true, Direktive mit transclude: true.
  • Das Folgende erzeugt einen neuen Geltungsbereich, der nicht prototypisch erbt: Direktive mit scope: { ... }. Dies erzeugt stattdessen einen "isolierten" Geltungsbereich.

Hinweis: Standardmäßig erzeugen Direktiven keinen neuen Geltungsbereich - d. h., der Standard ist scope: false.

ng-include

Angenommen, wir haben in unserem Controller:

$scope.myPrimitive = 50;
$scope.myObject    = {aNumber: 11};

Und in unserem 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>

Jedes ng-include erzeugt einen neuen Kindbereich, der prototypisch vom übergeordneten Bereich erbt.

Wenn man (zum Beispiel „77“) in das erste Eingabetextfeld eingibt, führt dies dazu, dass der Kind-Scope eine neue myPrimitive-Scope-Eigenschaft erhält, die die gleichnamige Eigenschaft des Eltern-Scope verdeckt/schattet. Das ist wahrscheinlich nicht das, was man will/erwartet.

Die Eingabe (zum Beispiel „99“) in das zweite Eingabetextfeld führt nicht zu einer neuen Kindeigenschaft. Da tpl2.html das Modell an eine Objekteigenschaft bindet, greift die prototypische Vererbung, wenn ngModel nach dem Objekt myObject sucht - es wird im übergeordneten Scope gefunden.

Wir können die erste Vorlage so umschreiben, dass sie $parent verwendet, wenn wir unser Modell nicht von einem primitiven Typ in ein Objekt ändern wollen:

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

Die Eingabe von (zum Beispiel „22“) in dieses Eingabetextfeld führt nicht zu einer neuen untergeordneten Eigenschaft. Das Modell ist jetzt an eine Eigenschaft des übergeordneten Bereichs gebunden (weil $parent eine untergeordnete Bereichseigenschaft ist, die auf den übergeordneten Bereich verweist).

Für alle Scopes (prototypisch oder nicht) verfolgt Angular immer eine Eltern-Kind-Beziehung (d.h. eine Hierarchie) über die Scope-Eigenschaften $parent, $$childHead und $$childTail. Normalerweise zeige ich diese Scope-Eigenschaften nicht in den Diagrammen.

Für Szenarien, in denen keine Formularelemente beteiligt sind, besteht eine andere Lösung darin, eine Funktion im übergeordneten Bereich zu definieren, um das Primitive zu ändern. Dann stellen Sie sicher, dass das Kind diese Funktion immer aufruft, die dem Kindbereich aufgrund der prototypischen Vererbung zur Verfügung stehen wird. Zum Beispiel,

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

Hier ist ein Beispiel-Fiddle, das diesen "Elternfunktionen"-Ansatz verwendet. (Das Fiddle wurde im Rahmen dieser Antwort geschrieben: https://stackoverflow.com/a/14104318/215945.)

Siehe auch https://stackoverflow.com/a/13782671/215945 und https://github.com/angular/angular.js/issues/1267.

ng-switch

Die Vererbung des ng-switch-Bereichs funktioniert genau wie ng-include. Wenn Sie also eine bidirektionale Datenbindung zu einem primitiven Wert im übergeordneten Bereich benötigen, verwenden Sie $parent oder ändern Sie das Modell in ein Objekt und binden Sie dann an eine Eigenschaft dieses Objekts. Dies verhindert, dass Kinderbereiche die Eigenschaften des übergeordneten Bereichs verbergen/schatten.

Siehe auch AngularJS, Bindungsbereich einer switch-case?

ng-wiederholen

Ng-repeat funktioniert ein wenig anders. Angenommen, wir haben in unserem Controller:

$scope.myArrayOfPrimitives = [ 11, 22 ];
$scope.myArrayOfObjects    = [{num: 101}, {num: 202}]

Und in unserem 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>

Für jedes Element/Iteraton erstellt ng-repeat einen neuen Scope, der prototypisch vom übergeordneten Scope erbt, aber es weist auch den Wert des Elements einer neuen Eigenschaft im neuen Kind-Scope zu. (Der Name der neuen Eigenschaft ist der Name der Schleifenvariablen.) Hier ist, wie der Angular-Quellcode für ng-repeat tatsächlich aussieht:

childScope = scope.$new();  // child scope prototypically inherits from parent scope
...
childScope[valueIdent] = value;  // creates a new childScope property

Wenn das Element ein primitiver Wert ist (wie in myArrayOfPrimitives), wird im Wesentlichen eine Kopie des Wertes der Eigenschaft des neuen Kind-Scopes zugewiesen. Das Ändern des Wertes der Kind-Scopes-Eigenschaft (d. h. die Verwendung von ng-model, also Child-Scope num) ändert nicht das Array, auf das der Parent-Scope verweist. Im ersten ng-repeat oben erhält also jeder Child-Scope eine num-Eigenschaft, die unabhängig vom myArrayOfPrimitives-Array ist:

Dieses ng-repeat wird nicht funktionieren (wie Sie es wollen/erwarten). Wenn Sie in die Textfelder tippen, ändern sich die Werte in den grauen Kästchen, die nur in den Kind-Scopes sichtbar sind. Was wir wollen, ist, dass die Eingaben das Array myArrayOfPrimitives beeinflussen, nicht eine primitive Eigenschaft eines Kind-Scopes. Um dies zu erreichen, müssen wir das Modell so ändern, dass es ein Array von Objekten ist.

Wenn also item ein Objekt ist, wird eine Referenz auf das ursprüngliche Objekt (nicht eine Kopie) der neuen Eigenschaft des Kind-Scopes zugewiesen. Das Ändern des Werts der Kind-Scopes-Eigenschaft (d. h. die Verwendung von ng-model, also obj.num) ändert das Objekt, auf das der Parent-Scope verweist. Also haben wir im zweiten ng-repeat oben:

(Ich habe eine Linie grau gefärbt, nur damit klar ist, wohin sie führt.)

Dies funktioniert wie erwartet. Das Eingeben in die Textfelder ändert die Werte in den grauen Kästchen, die sowohl für die Kinder- als auch für die Elternbereiche sichtbar sind.

Siehe auch Schwierigkeit mit ng-model, ng-repeat und Eingaben und https://stackoverflow.com/a/13782671/215945

ng-controller

Das Verschachteln von Controllern mit ng-controller führt zu normaler prototypischer Vererbung, genau wie bei ng-include und ng-switch, sodass dieselben Techniken gelten. Es wird jedoch als schlechte Praxis angesehen, dass zwei Controller Informationen über die $scope-Vererbung teilen -- http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ Stattdessen sollte ein Service verwendet werden, um Daten zwischen Controllern zu teilen.

(Wenn Sie wirklich Daten über die Vererbung des Controllerscope teilen möchten, müssen Sie nichts tun. Der Kindscope hat Zugriff auf alle Eigenschaften des Elternscope. Siehe auch Controller-Ladereihenfolge unterscheidet sich beim Laden oder Navigieren)

Richtlinien

  1. Standard (scope: false) - die Direktive erstellt keinen neuen Scope, daher gibt es hier keine Vererbung. Dies ist einfach, aber auch gefährlich, weil zum Beispiel eine Direktive denken könnte, dass sie eine neue Eigenschaft im Scope erstellt, während sie in Wirklichkeit eine bestehende Eigenschaft überschreibt. Dies ist keine gute Wahl für das Schreiben von Direktiven, die als wiederverwendbare Komponenten gedacht sind.
  2. scope: true - die Direktive erstellt einen neuen Kind-Scope, der prototypisch vom übergeordneten Scope erbt. Wenn mehr als eine Direktive (am gleichen DOM-Element) einen neuen Scope anfordert, wird nur ein neuer Kind-Scope erstellt. Da wir eine „normale“ prototypische Vererbung haben, ist dies wie bei ng-include und ng-switch, daher sei vorsichtig mit der Zwei-Wege-Datenbindung zu primitiven Werten des übergeordneten Scopes sowie mit dem Verstecken/Überschreiben von Eigenschaften des übergeordneten Scopes durch den Kind-Scope.
  3. Ein Attribut muss verwendet werden, um jede übergeordnete Eigenschaft anzugeben, an die die Direktive binden möchte: <div my-directive the-Parent-Prop=parentProp> und scope: { localProp: '@theParentProp' }.
    Der __proto__ der isolierten Scope verweist auf Object. Der $parent der isolierten Scope verweist auf die übergeordnete Scope, sodass, obwohl sie isoliert ist und nicht prototypisch von der übergeordneten Scope erbt, sie dennoch eine Kind-Scope ist.
    Für das untenstehende Bild haben wir
    <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2"> und
    scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
    Angenommen, die Direktive macht außerdem Folgendes in ihrer Linking-Funktion: scope.someIsolateProp="Ich bin isoliert"

  4. Für weitere Informationen zu isolierten Scopes siehe http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/

  5. transclude: true - die Direktive erstellt einen neuen "transkludierten" Kindbereich, der prototypisch vom Elternbereich erbt. Der transkludierte und der isolierte Bereich (falls vorhanden) sind Geschwister - die $parent-Eigenschaft jedes Bereichs verweist auf denselben Elternbereich. Wenn sowohl ein transkludierter als auch ein isolierter Bereich existieren, wird die Eigenschaft $$nextSibling des isolierten Bereichs auf den transkludierten Bereich verweisen. Mir sind keine Besonderheiten beim transkludierten Bereich bekannt.
    Für das untenstehende Bild, nehmen wir dieselbe Direktive wie oben an mit dieser Ergänzung: transclude: true

Dieses Fiddle hat eine showScope()-Funktion, die verwendet werden kann, um einen Isolat- und transkludierten Scope zu untersuchen. Siehe die Anweisungen in den Kommentaren im Fiddle.

Zusammenfassung

Es gibt vier Arten von Bereichen:

  1. normale prototypische Scope-Vererbung -- ng-include, ng-switch, ng-controller, Direktive mit scope: true
  2. Normale prototypische Bereichsvererbung mit einer Kopie/Zuweisung -- ng-repeat. Jede Iteration von ng-repeat erstellt einen neuen Kindbereich, und dieser neue Kindbereich erhält immer eine neue Eigenschaft.
  3. isolierter Scope -- Direktive mit scope: {...}. Diese ist nicht prototypisch, aber '=', '@' und '&' bieten einen Mechanismus, um auf Eigenschaften des übergeordneten Scopes über Attribute zuzugreifen.
  4. transkludierter Bereich -- Direktive mit transclude: true. Dieser ist ebenfalls eine normale prototypische Bereichsvererbung, aber er ist auch ein Geschwister eines jeden isolierten Bereichs.

Für alle Scopes (prototypisch oder nicht) verfolgt Angular immer eine Eltern-Kind-Beziehung (d. h. eine Hierarchie) über die Eigenschaften $parent und $$childHead und $$childTail.

Diagramme wurden mit Graphviz-"*.dot"-Dateien erstellt, die auf GitHub sind. Tim Caswells "Learning JavaScript with Object Graphs" war die Inspiration für die Verwendung von GraphViz für die Diagramme.

Antwort #2

Ich möchte auf keinen Fall mit Marks Antwort konkurrieren, wollte aber nur den Teil hervorheben, der schließlich alles verständlich machte, als jemand, der neu in JavaScript-Vererbung und dessen Prototypkette ist.

Nur Lesezugriffe auf Eigenschaften durchsuchen die Prototypenkette, Schreibzugriffe nicht. Also wenn Sie setzen

myObject.prop = '123';

Es schaut nicht die Kette hinauf, aber wenn du einstellst

myObject.myThing.prop = '123';

Es gibt einen subtilen Lesevorgang innerhalb dieser Schreiboperation, der versucht, myThing nachzuschlagen, bevor in dessen Eigenschaft geschrieben wird. Deshalb greift das Schreiben auf object.properties vom Kind aus auf die Objekte des Elternteils zu.

Antwort #3

Ich möchte ein Beispiel für prototypische Vererbung mit JavaScript zur Antwort von @Scott Driscoll hinzufügen. Wir werden das klassische Vererbungsmuster mit Object.create() verwenden, das Teil der EcmaScript 5-Spezifikation ist.

Zuerst erstellen wir die "Parent"-Objektfunktion

function Parent(){

}

Dann fügen Sie dem "Parent"-Objekt eine Prototypfunktion hinzu

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

Erstelle 'Child'-Objektfunktion

function Child(){

}

Weise Kind-Prototyp zu (Kind-Prototyp soll vom Eltern-Prototyp erben)

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

Weise den richtigen "Child"-Prototyp-Konstruktor zu

Child.prototype.constructor = Child;

Fügen Sie der Kind-Prototypmethode „changeProps“ hinzu, die den Wert der Eigenschaft „primitive“ im Child-Objekt überschreibt und den Wert von „object.one“ sowohl im Child- als auch im Parent-Objekt ändert

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

Initiiere Objekte zwischen Eltern (Vater) und Kind (Sohn).

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

Rufe die changeProps-Methode des Kindes (Sohn) auf

son.changeProps();

Überprüfen Sie die Ergebnisse.

Elternelement-Primitiveigenschaft hat sich nicht geändert

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

Kind primitives Eigenschaft geändert (umschrieben)

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

Eltern- und Kinderobjekt.one Eigenschaften geändert

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

Hier ein funktionierendes Beispiel http://jsbin.com/xexurukiso/1/edit/

Mehr Informationen zu Object.create hier https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create

Verwandte Artikel