Risposta rapida :
un ambito figlio normalmente eredita prototipicamente dall'ambito padre, ma non sempre. Un'eccezione a questa regola è una direttiva con scope: { ... }
: questo crea un ambito "isolato" che non eredita prototipicamente. Questo costrutto viene spesso utilizzato quando si crea una direttiva "componente riutilizzabile".
Per quanto riguarda le sfumature, l'ereditarietà dell'ambito è normalmente diretta ... fino a quando non hai bisogno di un'associazione dati bidirezionale (ovvero elementi del modulo, modello ng) nell'ambito secondario. Ng-repeat, ng-switch e ng-include possono farti inciampare se provi a legarti a una primitiva (ad es. Numero, stringa, booleano) nell'ambito genitore dall'interno dell'ambito figlio. Non funziona come la maggior parte delle persone si aspettano che dovrebbe funzionare. L'ambito figlio ottiene la propria proprietà che nasconde / ombreggia la proprietà padre con lo stesso nome. Le soluzioni alternative sono
- definire gli oggetti nel genitore per il proprio modello, quindi fare riferimento a una proprietà di tale oggetto nel figlio: parentObj.someProp
- usa $ parent.parentScopeProperty (non sempre possibile, ma più facile di 1. dove possibile)
- definire una funzione sull'ambito padre e chiamarla dal figlio (non sempre possibile)
Nuovi sviluppatori AngularJS spesso non si rendono conto che ng-repeat
, ng-switch
, ng-view
, ng-include
e ng-if
tutto creare nuovi ambiti di bambino, in modo che il problema spesso mostra quando queste direttive sono coinvolti. (Vedi questo esempio per una rapida illustrazione del problema.)
Questo problema con i primitivi può essere facilmente evitato seguendo la "best practice" di avere sempre un '.' nei tuoi modelli ng : guarda 3 minuti. Misko dimostra il problema del legame primitivo con ng-switch
.
Avere un '.' nei tuoi modelli assicurerà che l'ereditarietà prototipale sia in gioco. Quindi usa
<input type="text" ng-model="someObj.prop1">
<!--rather than
<input type="text" ng-model="prop1">`
-->
Risposta lunga :
Eredità prototipale di JavaScript
Inserito anche sul wiki di AngularJS: https://github.com/angular/angular.js/wiki/Understanding-Scopes
È importante innanzitutto avere una solida conoscenza dell'ereditarietà prototipale, soprattutto se si proviene da uno sfondo lato server e si ha più familiarità con l'eredità classica. Quindi rivediamolo prima.
Supponiamo che parentScope abbia proprietà aString, aNumber, anArray, anObject e aFunction. Se childScope eredita prototipicamente da parentScope, abbiamo:
(Nota che per risparmiare spazio, mostro l' anArray
oggetto come un singolo oggetto blu con i suoi tre valori, piuttosto che un singolo oggetto blu con tre letterali grigi separati.)
Se proviamo ad accedere a una proprietà definita su parentScope dall'ambito figlio, JavaScript cercherà prima nell'ambito secondario, non troverà la proprietà, quindi cercherà nell'ambito ereditato e troverà la proprietà. (Se non trovasse la proprietà in parentScope, continuerebbe fino alla catena del prototipo ... fino all'ambito principale). Quindi, questi sono tutti veri:
childScope.aString === 'parent string'
childScope.anArray[1] === 20
childScope.anObject.property1 === 'parent prop1'
childScope.aFunction() === 'parent output'
Supponiamo quindi di fare questo:
childScope.aString = 'child string'
La catena di prototipi non viene consultata e una nuova proprietà aString viene aggiunta a childScope. Questa nuova proprietà nasconde / ombreggia la proprietà parentScope con lo stesso nome. Questo diventerà molto importante quando discuteremo di ng-repeat e ng-include di seguito.
Supponiamo quindi di fare questo:
childScope.anArray[1] = '22'
childScope.anObject.property1 = 'child prop1'
La catena di prototipi viene consultata perché gli oggetti (anArray e anObject) non sono stati trovati in childScope. Gli oggetti si trovano in parentScope e i valori delle proprietà vengono aggiornati sugli oggetti originali. Nessuna nuova proprietà viene aggiunta a childScope; non vengono creati nuovi oggetti. (Notare che in JavaScript le matrici e le funzioni sono anche oggetti.)
Supponiamo quindi di fare questo:
childScope.anArray = [100, 555]
childScope.anObject = { name: 'Mark', country: 'USA' }
La catena di prototipi non viene consultata e l'ambito figlio ottiene due nuove proprietà dell'oggetto che nascondono / ombreggiano le proprietà dell'oggetto parentScope con gli stessi nomi.
Asporto:
- Se leggiamo childScope.propertyX e childScope ha propertyX, la catena di prototipi non viene consultata.
- Se impostiamo childScope.propertyX, la catena di prototipi non viene consultata.
Un ultimo scenario:
delete childScope.anArray
childScope.anArray[1] === 22 // true
Abbiamo prima eliminato la proprietà childScope, quindi quando proviamo ad accedere nuovamente alla proprietà, viene consultata la catena di prototipi.
Eredità dell'ambito angolare
I contendenti:
- Di seguito vengono creati nuovi ambiti ed ereditati prototipicamente: ng-repeat, ng-include, ng-switch, ng-controller, direttiva con
scope: true
, direttiva con transclude: true
.
- Quanto segue crea un nuovo ambito che non eredita prototipicamente: direttiva con
scope: { ... }
. Questo crea invece un ambito "isolato".
Si noti che, per impostazione predefinita, le direttive non creano un nuovo ambito, ovvero l'impostazione predefinita è scope: false
.
ng-includere
Supponiamo di avere nel nostro controller:
$scope.myPrimitive = 50;
$scope.myObject = {aNumber: 11};
E nel nostro 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>
Ogni ng-include genera un nuovo ambito figlio, che eredita prototipicamente dall'ambito padre.
Digitando (diciamo "77") nella prima casella di testo di input, l'ambito figlio ottiene una nuova myPrimitive
proprietà dell'ambito che nasconde / ombre la proprietà dell'ambito padre con lo stesso nome. Questo probabilmente non è quello che vuoi / ti aspetti.
Digitando (diciamo "99") nella seconda casella di testo di input non si ottiene una nuova proprietà figlio. Poiché tpl2.html associa il modello a una proprietà dell'oggetto, l'ereditarietà prototipale interviene quando ngModel cerca l'oggetto myObject - lo trova nell'ambito genitore.
Possiamo riscrivere il primo modello per usare $ parent, se non vogliamo cambiare il nostro modello da una primitiva a un oggetto:
<input ng-model="$parent.myPrimitive">
Digitando (diciamo "22") in questa casella di testo di input non si ottiene una nuova proprietà figlio. Il modello è ora associato a una proprietà dell'ambito padre (poiché $ parent è una proprietà dell'ambito figlio che fa riferimento all'ambito padre).
Per tutti gli ambiti (prototipo o meno), Angular tiene sempre traccia di una relazione genitore-figlio (ovvero una gerarchia), tramite le proprietà dell'ambito $ parent, $$ childHead e $$ childTail. Normalmente non mostro queste proprietà dell'ambito nei diagrammi.
Per gli scenari in cui gli elementi del modulo non sono coinvolti, un'altra soluzione è quella di definire una funzione sull'ambito padre per modificare la primitiva. Quindi assicurati che il bambino chiami sempre questa funzione, che sarà disponibile per l'ambito figlio a causa dell'eredità prototipale. Per esempio,
// in the parent scope
$scope.setMyPrimitive = function(value) {
$scope.myPrimitive = value;
}
Ecco un violino di esempio che utilizza questo approccio "funzione genitore". (Il violino è stato scritto come parte di questa risposta: https://stackoverflow.com/a/14104318/215945 .)
Vedi anche https://stackoverflow.com/a/13782671/215945 e https://github.com/angular/angular.js/issues/1267 .
ng-switch
L'ereditarietà dell'ambito di ng-switch funziona esattamente come ng-include. Quindi, se hai bisogno di un collegamento dati a 2 vie a una primitiva nell'ambito genitore, usa $ parent o modifica il modello in modo che sia un oggetto e quindi associa una proprietà di quell'oggetto. Ciò eviterà il nascondere / oscurare l'ambito figlio delle proprietà dell'ambito padre.
Vedi anche AngularJS, collegare l'ambito di una custodia?
ng-repeat
Ng-repeat funziona in modo leggermente diverso. Supponiamo di avere nel nostro controller:
$scope.myArrayOfPrimitives = [ 11, 22 ];
$scope.myArrayOfObjects = [{num: 101}, {num: 202}]
E nel nostro 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>
Per ogni elemento / iterazione, ng-repeat crea un nuovo ambito, che eredita prototipicamente dall'ambito padre, ma assegna anche il valore dell'elemento a una nuova proprietà sul nuovo ambito figlio . (Il nome della nuova proprietà è il nome della variabile loop.) Ecco cosa è in realtà il codice sorgente angolare per ng-repeat:
childScope = scope.$new(); // child scope prototypically inherits from parent scope
...
childScope[valueIdent] = value; // creates a new childScope property
Se l'elemento è una primitiva (come in myArrayOfPrimitives), essenzialmente una copia del valore viene assegnata alla nuova proprietà dell'ambito figlio. La modifica del valore della proprietà dell'ambito figlio (ovvero, utilizzando ng-model, quindi l'ambito figlio num
) non modifica l'array a cui fa riferimento l'ambito padre. Quindi nella prima ripetizione ng precedente, ogni ambito figlio ottiene una num
proprietà indipendente dall'array myArrayOfPrimitives:
Questa ripetizione non funzionerà (come vuoi / ti aspetti). La digitazione nelle caselle di testo modifica i valori nelle caselle grigie, che sono visibili solo negli ambiti figlio. Ciò che vogliamo è che gli input influenzino l'array myArrayOfPrimitives, non una proprietà primitiva di ambito figlio. A tale scopo, è necessario modificare il modello in una matrice di oggetti.
Pertanto, se l'elemento è un oggetto, un riferimento all'oggetto originale (non una copia) viene assegnato alla nuova proprietà dell'ambito figlio. La modifica del valore della proprietà dell'ambito figlio (ovvero, utilizzando ng-model, quindi obj.num
) modifica l'oggetto a cui fa riferimento l'ambito padre. Quindi, nella seconda ripetizione precedente, abbiamo:
(Ho colorato di grigio una riga solo in modo che sia chiaro dove sta andando.)
Funziona come previsto. La digitazione nelle caselle di testo modifica i valori nelle caselle grigie, che sono visibili sia agli ambiti figlio che a quello padre.
Vedi anche Difficoltà con ng-model, ng-repeat e input e
https://stackoverflow.com/a/13782671/215945
ng-controllore
L'annidamento dei controller mediante ng-controller produce una normale eredità prototipale, proprio come ng-include e ng-switch, quindi si applicano le stesse tecniche. Tuttavia, "è considerata una forma errata per due controller condividere informazioni tramite l'ereditarietà $ scope" - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/
Un servizio dovrebbe essere usato per condividere dati tra controller invece.
(Se si desidera veramente condividere i dati tramite l'ereditarietà dell'ambito dei controller, non è necessario eseguire alcuna operazione. L'ambito figlio avrà accesso a tutte le proprietà dell'ambito padre. Vedere anche L' ordine di caricamento del controller differisce durante il caricamento o la navigazione )
direttive
- default (
scope: false
) - la direttiva non crea un nuovo ambito, quindi qui non c'è ereditarietà. Questo è facile, ma anche pericoloso perché, ad esempio, una direttiva potrebbe pensare che stia creando una nuova proprietà nell'ambito, mentre in realtà sta ostruendo una proprietà esistente. Questa non è una buona scelta per scrivere direttive intese come componenti riutilizzabili.
scope: true
- la direttiva crea un nuovo ambito figlio che eredita prototipicamente dall'ambito padre. Se più di una direttiva (sullo stesso elemento DOM) richiede un nuovo ambito, viene creato solo un nuovo ambito figlio. Poiché abbiamo un'eredità prototipale "normale", questo è come ng-include e ng-switch, quindi fai attenzione ai dati a 2 vie associati alle primitive dell'ambito parent e al nascondimento / shadowing dell'ambito child delle proprietà dell'ambito parent.
scope: { ... }
- la direttiva crea un nuovo ambito isolato / isolato. Non eredita prototipicamente. Questa è di solito la scelta migliore quando si creano componenti riutilizzabili, poiché la direttiva non può leggere o modificare accidentalmente l'ambito padre. Tuttavia, tali direttive spesso richiedono l'accesso ad alcune proprietà dell'ambito padre. L'hash dell'oggetto viene utilizzato per impostare l'associazione bidirezionale (usando '=') o l'associazione unidirezionale (usando '@') tra l'ambito padre e l'ambito isolato. C'è anche '&' da associare alle espressioni dell'ambito padre. Pertanto, tutti creano proprietà dell'ambito locale che derivano dall'ambito padre. Nota che gli attributi sono usati per aiutare a impostare l'associazione: non puoi semplicemente fare riferimento ai nomi delle proprietà dell'ambito padre nell'hash dell'oggetto, devi usare un attributo. Ad esempio, questo non funzionerà se si desidera associare alla proprietà padreparentProp
nell'ambito isolato: <div my-directive>
e scope: { localProp: '@parentProp' }
. È necessario utilizzare un attributo per specificare ciascuna proprietà padre a cui la direttiva desidera associare: <div my-directive the-Parent-Prop=parentProp>
e scope: { localProp: '@theParentProp' }
.
Isola i __proto__
riferimenti dell'ambito Oggetto. Isolare l'ambito $ parent fa riferimento all'ambito parent, quindi sebbene sia isolato e non erediti prototipicamente dall'ambito parent, rimane comunque un ambito figlio.
Per l'immagine qui sotto abbiamo
<my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">
e
scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
anche, supponiamo che la direttiva faccia questo nella sua funzione di collegamento:scope.someIsolateProp = "I'm isolated"
Per ulteriori informazioni sugli ambiti isolati, vedere http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
transclude: true
- la direttiva crea un nuovo ambito figlio "escluso", che eredita prototipicamente dall'ambito padre. L'ambito trascluso e l'ambito isolato (se presente) sono fratelli - la proprietà $ parent di ciascun ambito fa riferimento allo stesso ambito padre. Quando esistono entrambi un ambito escluso e un ambito isolato, la proprietà ambito ambito $$ nextSibling farà riferimento all'ambito escluso. Non sono a conoscenza di eventuali sfumature con l'ambito escluso.
Per l'immagine seguente, assumere la stessa direttiva di cui sopra con questa aggiunta:transclude: true
Questo violino ha una showScope()
funzione che può essere utilizzata per esaminare un ambito isolato e escluso. Vedi le istruzioni nei commenti sul violino.
Sommario
Esistono quattro tipi di ambiti:
- normale eredità dell'ambito prototipo: ng-include, ng-switch, ng-controller, direttiva con
scope: true
- normale eredità dell'ambito prototipo con una copia / assegnazione - ng-repeat. Ogni iterazione di ng-repeat crea un nuovo ambito figlio e quel nuovo ambito figlio ottiene sempre una nuova proprietà.
- ambito di applicazione - direttiva con
scope: {...}
. Questo non è un prototipo, ma '=', '@' e '&' forniscono un meccanismo per accedere alle proprietà dell'ambito padre, tramite attributi.
- ambito di applicazione escluso - direttiva con
transclude: true
. Anche questa è una normale eredità dell'ambito prototipo, ma è anche un fratello di qualsiasi ambito isolato.
Per tutti gli ambiti (prototipo o no), Angular tiene sempre traccia di una relazione genitore-figlio (ovvero una gerarchia), tramite le proprietà $ parent e $$ childHead e $$ childTail.
I diagrammi sono stati generati con graphvizFile "* .dot", che si trovano su github . " Learning JavaScript with Object Graphs " di Tim Caswell è stato l'ispirazione per l'utilizzo di GraphViz per i diagrammi.