Frage Was sind die Nuancen des Umfangs prototypischer / prototypischer Vererbung in AngularJS?


Das API-Referenzbereichsseite sagt:

Ein Bereich kann erben von einem übergeordneten Bereich.

Das Entwicklerleitfaden-Scope-Seite sagt:

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

Existiert also ein untergeordneter Bereich immer prototypisch von seinem übergeordneten Bereich? Gibt es Ausnahmen? Wenn es erbt, ist es immer eine normale JavaScript-Prototyp-Vererbung?


965
2017-12-27 04:48


Ursprung


Antworten:


Schnelle Antwort:
Ein untergeordneter Bereich erbt normalerweise prototypisch von seinem übergeordneten Bereich, aber nicht immer. Eine Ausnahme von dieser Regel ist eine Direktive mit scope: { ... } - Dies erzeugt einen "isolieren" Bereich, der nicht prototypisch erbt. Dieses Konstrukt wird häufig beim Erstellen einer Richtlinie für "wiederverwendbare Komponenten" verwendet.

Was die Nuancen betrifft, ist die Vererbung des Bereichs normalerweise geradlinig ... bis Sie es brauchen 2-Wege-Datenbindung (d. h. Formularelemente, ng-Modell) in dem Kindbereich. Ng-repeat, ng-switch und ng-include können Sie stolpern, wenn Sie versuchen, sich an a zu binden Primitive (z. B. Zahl, Zeichenfolge, boolescher Wert) im übergeordneten Bereich aus dem untergeordneten Bereich. Es funktioniert nicht so, wie die meisten Leute erwarten, dass es funktioniert. Der untergeordnete Bereich erhält eine eigene Eigenschaft, die die gleichnamige übergeordnete Eigenschaft ver- / versteckt. Ihre Problemumgehungen sind

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

Neue AngularJS-Entwickler merken das oft nicht ng-repeat, ng-switch, ng-view, ng-include und ng-if Alle erstellen neue untergeordnete Bereiche, sodass das Problem häufig auftaucht, wenn diese Direktiven betroffen sind. (Sehen Dieses Beispiel für eine schnelle Illustration des Problems.)

Dieses Problem mit Primitiven lässt sich leicht vermeiden, wenn man die "Best Practice" von immer ein '.' in deinen ng-Modellen - Sehen Sie sich 3 Minuten an. Misko demonstriert das primitive Bindeproblem mit ng-switch.

Ein ... haben '.' in Ihren Modellen wird sichergestellt, dass prototypische Vererbung im Spiel ist. Also, benutze es

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

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


Lange Antwort:

JavaScript Prototypische Vererbung

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

Es ist wichtig, dass Sie zunächst ein solides Verständnis der prototypischen Vererbung haben, insbesondere wenn Sie von einem serverseitigen Hintergrund kommen und Sie mit der klassischen Vererbung besser vertraut sind. Lasst uns das zuerst durchgehen.

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

prototypal inheritance

(Hinweis: Um Platz zu sparen, zeige ich die anArray Objekt als ein einzelnes blaues Objekt mit seinen drei Werten, anstatt ein einzelnes blaues Objekt mit drei separaten grauen Literalen.)

Wenn wir versuchen, über den untergeordneten Bereich auf eine Eigenschaft zuzugreifen, die im parentScope definiert ist, sucht JavaScript zuerst im untergeordneten Bereich, sucht die Eigenschaft nicht, sucht dann im vererbten Bereich und sucht nach der Eigenschaft. (Wenn die Eigenschaft im parentScope nicht gefunden wurde, wird die Prototypkette fortgesetzt ... bis zum Root-Bereich). Also, das sind alle wahr:

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

Angenommen, wir machen das dann:

childScope.aString = 'child string'

Die Prototypkette wird nicht konsultiert, und dem ChildScope wird eine neue aString-Eigenschaft hinzugefügt. Diese neue Eigenschaft blendet die parentScope-Eigenschaft mit dem gleichen Namen aus / schattet sie aus.  Dies wird sehr wichtig werden, wenn wir unten auf ng-repeat und ng-include eingehen.

property hiding

Angenommen, wir machen das dann:

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

Die Prototypkette wird konsultiert, weil die Objekte (anArray und anObject) nicht in dem childScope gefunden werden. Die Objekte befinden sich im parentScope, und die Eigenschaftswerte werden für die ursprünglichen Objekte aktualisiert. Dem ChildScope werden keine neuen Eigenschaften hinzugefügt. Es werden keine neuen Objekte erstellt. (Beachten Sie, dass in Arrays und Funktionen von JavaScript auch Objekte enthalten sind.)

follow the prototype chain

Angenommen, wir machen das dann:

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

Die Musterkette wird nicht konsultiert, und der untergeordnete Bereich erhält zwei neue Objekteigenschaften, die die Eigenschaften des ParentScope-Objekts mit denselben Namen ausblenden oder schattieren.

more property hiding

Takeaways:

  • Wenn wir childScope.propertyX und childScope propertyX lesen, wird die Prototypkette nicht konsultiert.
  • Wenn wir childScope.propertyX setzen, wird die Prototypkette nicht konsultiert.

Ein letztes Szenario:

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

Wir haben die Eigenschaft childScope zuerst gelöscht. Wenn wir dann erneut auf die Eigenschaft zugreifen möchten, wird die Prototypkette herangezogen.

after removing a child property


Winkelbereich-Vererbung

Die Anwärter:

  • Die folgenden erstellen neue Bereiche und erben prototypisch: ng-repeat, ng-include, ng-switch, ng-controller, Direktive mit scope: true, Richtlinie mit transclude: true.
  • Der folgende erstellt einen neuen Bereich, der nicht prototypisch erbt: Direktive mit scope: { ... }. Dies erstellt stattdessen einen "isolieren" Bereich.

Beachten Sie, dass Direktiven standardmäßig keinen neuen Bereich erstellen, d. H. Der Standardwert 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 generiert einen neuen untergeordneten Bereich, der prototypisch vom übergeordneten Bereich erbt.

ng-include child scopes

Wenn Sie (z. B. "77") in das erste Eingabetextfeld eingeben, erhält der untergeordnete Bereich einen neuen Wert myPrimitive Bereichseigenschaft, die die Eigenschaft des übergeordneten Bereichs mit demselben Namen verbirgt / schattiert. Dies ist wahrscheinlich nicht das, was Sie wollen / erwarten.

ng-include with a primitive

Das Eingeben (z. B. "99") in das zweite Eingabetextfeld führt nicht zu einer neuen untergeordneten Eigenschaft. Da tpl2.html das Modell an eine Objekteigenschaft bindet, tritt die prototypische Vererbung ein, wenn ngModel nach dem Objekt myObject sucht - es findet es im übergeordneten Bereich.

ng-include with an object

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

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

Das Eingeben (z. B. "22") in dieses Eingabetextfeld führt nicht zu einer neuen untergeordneten Eigenschaft. Das Modell ist jetzt an eine Eigenschaft des übergeordneten Bereichs gebunden (da $ parent eine untergeordnete Bereichseigenschaft ist, die auf den übergeordneten Bereich verweist).

ng-include with $parent

Für alle Bereiche (prototypisch oder nicht) verfolgt Angular immer eine Eltern-Kind-Beziehung (d. H. Eine Hierarchie) über die Bereichseigenschaften $ parent, $$ childHead und $$ childTail. Normalerweise zeige ich diese Bereichseigenschaften nicht in den Diagrammen an.

Für Szenarien, in denen Formularelemente nicht beteiligt sind, besteht eine andere Lösung darin, eine Funktion für den übergeordneten Bereich zu definieren, um das Grundelement zu ändern. Stellen Sie dann sicher, dass das Kind immer diese Funktion aufruft, die aufgrund einer prototypischen Vererbung für den Kindbereich verfügbar ist. Z.B.,

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

Hier ist ein Probe Geige das verwendet diese "Elternfunktion" -Ansatz. (Die Geige wurde als Teil 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-Schalter

Die Vererbung von ng-switch scope funktioniert genau wie ng-include. Wenn Sie also eine 2-Wege-Datenbindung an ein Primitiv im übergeordneten Bereich benötigen, verwenden Sie $ parent, oder ändern Sie das Modell als Objekt und binden Sie dann an eine Eigenschaft dieses Objekts. Dadurch wird verhindert, dass der Bereich "Kind" die übergeordneten Eigenschaften des übergeordneten Bereichs verdeckt / überschattet.

Siehe auch AngularJS, binde Bereich eines Switch-Case?

ng-Wiederholung

Ng-Repeat funktioniert etwas 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 / jede Iteration erstellt ng-repeat einen neuen Bereich, der prototypisch vom übergeordneten Bereich erbt. Es weist jedoch auch den Wert des Elements einer neuen Eigenschaft im neuen untergeordneten Bereich zu. (Der Name der neuen Eigenschaft ist der Name der Schleifenvariablen.) Der Angular-Quellcode für ng-repeat lautet folgendermaßen:

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

Wenn Element ein Grundelement ist (wie in myArrayOfPrimitives), wird der neuen untergeordneten Bereichseigenschaft im Wesentlichen eine Kopie des Werts zugewiesen. Ändern des Werts der untergeordneten Bereichseigenschaft (d. H. Mithilfe von ng-model, also untergeordneten Bereich num) tut es nicht Ändern Sie das Array, auf das der übergeordnete Bereich verweist. In der ersten og ng-Wiederholung erhält also jeder Kind-Bereich a num Eigenschaft, die unabhängig vom Array myArrayOfPrimitives ist:

ng-repeat with primitives

Diese ng-Wiederholung funktioniert nicht (wie du es willst / erwartest). Durch Eingabe in die Textfelder werden die Werte in den grauen Feldern geändert, die nur in den untergeordneten Bereichen sichtbar sind. Wir möchten, dass die Eingaben das myArrayOfPrimitives-Array und nicht die primitive Eigenschaft eines untergeordneten Bereichs beeinflussen. Um dies zu erreichen, müssen wir das Modell zu einem Array von Objekten machen.

Wenn also das Objekt ein Objekt ist, wird der neuen untergeordneten Bereichseigenschaft ein Verweis auf das ursprüngliche Objekt (keine Kopie) zugewiesen. Ändern des Werts der Eigenschaft der untergeordneten Bereichseigenschaft (d. H. Mithilfe von ng-model obj.num) tut Ändern Sie das Objekt, auf das der übergeordnete Bereich verweist. Also haben wir in der zweiten ng-Wiederholung oben:

ng-repeat with objects

(Ich habe nur eine Zeile grau eingefärbt, damit klar ist, wohin es geht.)

Dies funktioniert wie erwartet. Beim Eintippen in die Textfelder werden die Werte in den grauen Feldern geändert, die für den untergeordneten Bereich und den übergeordneten Bereich sichtbar sind.

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

ng-Controller

Nesting-Controller ng-Controller führt zu normaler prototypische Vererbung, wie ng-umfassen und ng-Schalter, so dass die gleichen Techniken anwenden. "Es wird jedoch als schlechte Form betrachtet, dass zwei Controller Informationen über $ scope vererben" - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ Ein Dienst sollte stattdessen verwendet werden, um Daten zwischen Controllern zu teilen.

(Wenn Sie wirklich Daten über die Vererbung von Controllern freigeben möchten, müssen Sie nichts tun. Der untergeordnete Bereich hat Zugriff auf alle Eigenschaften des übergeordneten Bereichs. Siehe auch Die Ladereihenfolge der Controller unterscheidet sich beim Laden oder Navigieren)

Richtlinien

  1. Standard (scope: false) - Die Direktive erstellt keinen neuen Bereich, daher gibt es hier keine Vererbung. Dies ist einfach, aber auch gefährlich, da z. B. eine Direktive denken könnte, dass sie eine neue Eigenschaft für den Bereich erstellt, während sie tatsächlich eine vorhandene Eigenschaft verfälscht. Dies ist keine gute Wahl zum Schreiben von Direktiven, die als wiederverwendbare Komponenten gedacht sind.
  2. scope: true - Die Direktive erstellt einen neuen untergeordneten Bereich, der prototypisch vom übergeordneten Bereich erbt. Wenn mehr als eine Direktive (für dasselbe DOM-Element) einen neuen Bereich anfordert, wird nur ein neuer untergeordneter Bereich erstellt. Da wir eine "normale" prototypische Vererbung haben, ist dies wie ng-include und ng-switch, also seien Sie vorsichtig bei der 2-Wege-Datenbindung an Elternbereich-Grundelemente und beim Kindbereich-Verbergen / -Verbergen von übergeordneten Bereichseigenschaften.
  3. scope: { ... } - Die Richtlinie erstellt einen neuen isolierten / isolierten Bereich. Erbt nicht prototypisch. Dies ist normalerweise die beste Wahl beim Erstellen wiederverwendbarer Komponenten, da die Direktive den übergeordneten Bereich nicht versehentlich lesen oder ändern kann. Solche Direktiven benötigen jedoch häufig Zugriff auf einige Eigenschaften des übergeordneten Bereichs. Der Objekt-Hash wird verwendet, um eine bidirektionale Bindung (mit '=') oder eine unidirektionale Bindung (mit '@') zwischen dem übergeordneten Bereich und dem isolierenden Bereich einzurichten. Es gibt auch '&', die an übergeordnete Bereichsausdrücke gebunden werden. Diese erstellen also alle lokalen Bereichseigenschaften, die vom übergeordneten Bereich abgeleitet sind. Beachten Sie, dass Attribute verwendet werden, um beim Einrichten der Bindung zu helfen. Sie können nicht einfach die übergeordneten Bereichseigenschaftsnamen im Objekthash referenzieren, Sie müssen ein Attribut verwenden. Z. B. funktioniert dies nicht, wenn Sie an die Elterneigenschaft binden wollen parentProp im isolierten Umfang: <div my-directive> und scope: { localProp: '@parentProp' }. Ein Attribut muss verwendet werden, um jede Elterneigenschaft anzugeben, an die die Richtlinie binden möchte: <div my-directive the-Parent-Prop=parentProp> und scope: { localProp: '@theParentProp' }.
    Bereich isolieren __proto__ Referenzen Objekt. Der $ parent von isolate scope verweist auf den übergeordneten Bereich. Obwohl er isoliert ist und nicht vom übergeordneten Bereich prototypisch erbt, ist er immer noch ein untergeordneter Bereich.
    Für das Bild unten haben wir
      <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2"> und
      scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
    Nehmen Sie außerdem an, dass die Richtlinie dies in ihrer Verknüpfungsfunktion ausführt: scope.someIsolateProp = "I'm isolated"
      isolated scope
    Weitere Informationen zu isolierbaren Bereichen finden Sie unter http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true- Die Richtlinie erstellt einen neuen "übersetzten" Kindbereich, der prototypisch vom übergeordneten Geltungsbereich erbt. Der transcluded und der isolierte Bereich (falls vorhanden) sind Geschwister - die $ parent-Eigenschaft jedes Bereichs verweist auf denselben übergeordneten Bereich. Wenn sowohl ein transcluded- als auch ein isolate-Bereich vorhanden sind, verweist die isolate-Bereichseigenschaft $$ nextSibling auf den übergangenen Bereich. Mir sind keine Nuancen mit dem überlieferten Umfang bekannt.
    Nehmen Sie für das Bild unten die gleiche Anweisung wie oben mit diesem Zusatz an: transclude: true
    transcluded scope

Dies Geige hat ein showScope() Funktion, die verwendet werden kann, um einen Isolat und einen übergangenen Bereich zu untersuchen. Siehe die Anweisungen in den Kommentaren in der Geige.


Zusammenfassung

Es gibt vier Arten von Bereichen:

  1. Normaler prototypischer Gültigkeitsbereich - ng-include, ng-switch, ng-controller, Direktive mit scope: true
  2. Normale prototypische Gültigkeitsbereichvererbung mit einer Kopie / Zuweisung - ng-repeat. Jede Iteration von ng-repeat erstellt einen neuen untergeordneten Bereich, und dieser neue untergeordnete Bereich erhält immer eine neue Eigenschaft.
  3. Isolieren Sie die Gültigkeitsbereichsrichtlinie mit scope: {...}. Dieser ist nicht prototypisch, aber '=', '@' und '&' bieten einen Mechanismus zum Zugriff auf Eigenschaften des übergeordneten Bereichs über Attribute.
  4. übertragene Scope - Direktive mit transclude: true. Dies ist auch eine normale prototypische Gültigkeitsbereichsvererbung, aber es ist auch ein Geschwister eines beliebigen Gültigkeitsbereichs.

Für alle Bereiche (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 erstellt  "* .dot" Dateien, die eingeschaltet sind Github. Tim Caswells "JavaScript mit Objektdiagrammen lernen"war die Inspiration für die Verwendung von GraphViz für die Diagramme.


1694
2017-12-27 04:48



Ich möchte keineswegs mit Marks Antwort konkurrieren, sondern wollte nur das Stück hervorheben, das schließlich alles zu einem neuen Klick gemacht hat Javascript Vererbung und ihre Prototypkette.

Nur Eigenschaft liest Suche die Prototypkette, schreibt nicht. Also wenn du es einstellst

myObject.prop = '123';

Es schaut nicht die Kette nach oben, sondern beim Setzen

myObject.myThing.prop = '123';

Innerhalb dieser Schreiboperation wird ein subtiler Lesevorgang ausgeführt das versucht, myThing nachzuschlagen, bevor er auf seine Requisite schreibt. Aus diesem Grund wird das Schreiben in die object.properties vom Kind an die Objekte des Elternteils gesendet.


135
2018-05-16 15:22



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

Zuerst erstellen wir die Objektfunktion "Parent"

function Parent(){

}

Fügen Sie dann der Objektfunktion "Parent" einen Prototyp hinzu

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

Erstellen Sie die Objektfunktion "Kind"

function Child(){

}

Zuweisen eines untergeordneten Prototyps (Machen Sie einen Kindprototyp vom übergeordneten Prototyp ab)

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

Ordnen Sie den richtigen Konstruktor "Kind" zu

Child.prototype.constructor = Child;

Fügen Sie die Methode "changeProps" zu einem untergeordneten Prototyp hinzu, der den "primitiven" Eigenschaftswert im Child-Objekt neu schreibt und den Wert "object.one" sowohl in Child- als auch Parent-Objekten ändert

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

Initiiere Eltern (Vater) und Kinder (Sohn) Objekte.

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

Rufen Sie child (son) changeProps-Methode auf

son.changeProps();

Überprüfen Sie die Ergebnisse.

Die ursprüngliche Eigenschaft des Elternelements hat sich nicht geändert

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

Child primitive Eigenschaft geändert (neu geschrieben)

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

Parent und Child object.one Eigenschaften geändert

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

Arbeitsbeispiel hier http://jsbin.com/xexurukiso/1/edit/

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


18
2017-11-08 22:45