Quali sono le sfumature dell'ambito prototipo / eredità prototipica in AngularJS?


1028

La pagina Ambito di riferimento dell'API dice:

Un ambito può ereditare da un ambito padre.

La pagina Ambito della Guida per gli sviluppatori dice:

Un ambito (prototipicamente) eredita le proprietà dal suo ambito genitore.

  • Quindi, un ambito figlio eredita sempre prototipicamente dal suo ambito genitore?
  • Ci sono delle eccezioni?
  • Quando eredita, è sempre normale eredità prototipo JavaScript?

Risposte:


1741

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

  1. definire gli oggetti nel genitore per il proprio modello, quindi fare riferimento a una proprietà di tale oggetto nel figlio: parentObj.someProp
  2. usa $ parent.parentScopeProperty (non sempre possibile, ma più facile di 1. dove possibile)
  3. 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-includee ng-iftutto 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:

eredità prototipale

(Nota che per risparmiare spazio, mostro l' anArrayoggetto 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.

nascondiglio di proprietà

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.)

seguire la catena di prototipi

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.

più nascondiglio di proprietà

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.

dopo aver rimosso una proprietà figlio


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.

ng-include ambiti figlio

Digitando (diciamo "77") nella prima casella di testo di input, l'ambito figlio ottiene una nuova myPrimitiveproprietà dell'ambito che nasconde / ombre la proprietà dell'ambito padre con lo stesso nome. Questo probabilmente non è quello che vuoi / ti aspetti.

ng-include con una primitiva

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.

ng-include con un oggetto

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).

ng-include con $ parent

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 numproprietà indipendente dall'array myArrayOfPrimitives:

ng-repeat con primitivi

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:

ng-repeat con oggetti

(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

  1. 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.
  2. 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.
  3. 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à padreparentPropnell'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"
    portata isolata
    Per ulteriori informazioni sugli ambiti isolati, vedere http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. 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
    ambito escluso

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:

  1. normale eredità dell'ambito prototipo: ng-include, ng-switch, ng-controller, direttiva con scope: true
  2. 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à.
  3. ambito di applicazione - direttiva con scope: {...} . Questo non è un prototipo, ma '=', '@' e '&' forniscono un meccanismo per accedere alle proprietà dell'ambito padre, tramite attributi.
  4. 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 File "* .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.


48
Articolo fantastico, troppo lungo per una risposta SO, ma comunque molto utile. Inseriscilo sul tuo blog prima che un editor lo riduca alle dimensioni.
Iwein

43
Ho messo una copia sul wiki di AngularJS .
Mark Rajcok,

3
Correzione: "Isola i __proto__riferimenti dell'ambito dell'oggetto". dovrebbe invece essere "Isolare i __proto__riferimenti dell'ambito a un oggetto Scope". Quindi, nelle ultime due immagini, le caselle "Oggetto" arancioni dovrebbero invece essere caselle "Scope".
Mark Rajcok,

15
Questo asnwer dovrebbe essere incluso nella guida angularjs. Questo è molto più didattico ...
Marcelo De Zen

2
Il wiki mi lascia perplesso, per prima cosa dice: "La catena del prototipo viene consultata perché l'oggetto non si trova in childScope". e quindi si legge: "Se impostiamo childScope.propertyX, la catena di prototipi non viene consultata.". Il secondo implica una condizione mentre il primo no.
Stephane

140

Non voglio in alcun modo competere con la risposta di Mark, ma volevo solo evidenziare il pezzo che alla fine ha fatto sì che tutto facesse clic come qualcuno di nuovo sull'eredità di Javascript e sulla sua catena di prototipi .

Solo le letture delle proprietà effettuano ricerche nella catena di prototipi, non nelle scritture. Quindi quando si imposta

myObject.prop = '123';

Non cerca la catena, ma quando si imposta

myObject.myThing.prop = '123';

c'è una lettura sottile in corso all'interno di quell'operazione di scrittura che cerca di cercare myThing prima di scrivere sul suo supporto. Ecco perché la scrittura su object.properties dal bambino arriva agli oggetti del genitore.


12
Sebbene questo sia un concetto molto semplice, potrebbe non essere molto ovvio poiché, a mio avviso, molte persone lo mancano. Ben messo.
moljac024,

3
Osservazione eccellente. Porto via, la risoluzione di una proprietà non oggetto non comporta una lettura, mentre la risoluzione di una proprietà oggetto lo fa.
Stephane

1
Perché? Qual è la motivazione per cui le scritture di proprietà non vanno nella catena dei prototipi? Sembra folle ...
Jonathan.

1
Sarebbe bello se tu aggiungessi un semplice esempio.
tylik,

2
Si noti che si fa cercare la catena di prototipi per il setter . Se non viene trovato nulla, crea una proprietà sul ricevitore.
Bergi,

21

Vorrei aggiungere un esempio di eredità prototipica con javascript alla risposta di @Scott Driscoll. Useremo il modello di ereditarietà classica con Object.create () che fa parte della specifica EcmaScript 5.

Per prima cosa creiamo la funzione oggetto "Parent"

function Parent(){

}

Quindi aggiungere un prototipo alla funzione oggetto "Parent"

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

Creare la funzione oggetto "Figlio"

function Child(){

}

Assegna prototipo figlio (Rendi il prototipo figlio ereditato dal prototipo padre)

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

Assegna il costruttore di prototipi "figlio" corretto

Child.prototype.constructor = Child;

Aggiungi il metodo "changeProps" a un prototipo figlio, che riscriverà il valore della proprietà "primitiva" nell'oggetto figlio e cambierà il valore "object.one" sia negli oggetti figlio che padre

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

Inizia oggetti Parent (papà) e Child (figlio).

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

Chiama il metodo changeProps di Child (son)

son.changeProps();

Controlla i risultati

La proprietà primitiva padre non è cambiata

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

Proprietà primitiva figlio modificata (riscritta)

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

Proprietà object.one padre e figlio modificate

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

Esempio di lavoro qui http://jsbin.com/xexurukiso/1/edit/

Maggiori informazioni su Object.create qui https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.