javascript - Hva er nyansene ved omfang, prototypisk/prototypisk arv i AngularJS?
API-referansesides rekkevidde sier:
Et scope kan arve fra et foreldrescope.
Utviklerveiledningens omfangsside sier:
Et scope arver (prototypisk) egenskaper fra sitt overordnede scope.
- Så, arver et barneskop alltid prototypisk fra foreldreskopet sitt?
- Er det unntak?
- Når det arver, er det alltid vanlig JavaScript-prototypisk arv?
Svar #1
Raskt svar:
Et barns scope arver normalt prototypisk fra sitt foreldre-scope, men ikke alltid. Ett unntak fra denne regelen er en direktiv med scope: { ... } -- dette lager et "isolert" scope som ikke arver prototypisk. Denne konstruksjonen brukes ofte når man lager en "gjenbrukbar komponent" direktiv.
Når det gjelder nyansene, er arving av scope vanligvis grei... helt til du trenger to-veis databinding (dvs. skjemaelementer, ng-model) i child scope. Ng-repeat, ng-switch og ng-include kan lure deg hvis du prøver å binde til en primitiv (f.eks. nummer, streng, boolsk) i parent scope fra innenfor child scope. Det fungerer ikke på den måten de fleste forventer at det skal fungere. Child scope får sin egen egenskap som skjuler/overskygger parent-egenskapen med samme navn. Dine løsninger er
- definer objekter i forelderen for modellen din, og referer deretter til en egenskap av det objektet i barnet: parentObj.someProp
- bruk $parent.parentScopeProperty (ikke alltid mulig, men lettere enn 1. der det er mulig)
- definer en funksjon i foreldresammenhengen, og kall den fra barnet (ikke alltid mulig)
Nye AngularJS-utviklere innser ofte ikke at ng-repeat, ng-switch, ng-view, ng-include og ng-if alle oppretter nye barne-scopes, så problemet dukker ofte opp når disse direktivene er involvert. (Se dette eksemplet for en rask illustrasjon av problemet.)
Dette problemet med primitivene kan enkelt unngås ved å følge "beste praksis" om alltid å ha en '.' i dine ng-modeller - se 3 minutter av videoen. Misko viser problemet med primitiv binding med ng-switch.
Å ha en '.' i modellene dine vil sikre at prototypisk arv er i spill. Så, bruk
<input type="text" ng-model="someObj.prop1">
<!--rather than
<input type="text" ng-model="prop1">`
-->
L-a-n-g svar:
JavaScript Prototypbasert Arv
Også lagt ut på AngularJS-wikien: https://github.com/angular/angular.js/wiki/Understanding-Scopes
Det er viktig å først ha en solid forståelse av prototypisk arv, spesielt hvis du kommer fra en serversidebakgrunn og er mer kjent med klassisk arv. Så la oss gå gjennom det først.
Anta at parentScope har egenskapene aString, aNumber, anArray, anObject og aFunction. Hvis childScope arver prototypisk fra parentScope, har vi:
(Merk at for å spare plass, viser jeg anArray-objektet som ett enkelt blått objekt med sine tre verdier, i stedet for ett enkelt blått objekt med tre separate grå litteraler.)
Hvis vi prøver å få tilgang til en egenskap definert i parentScope fra child scope, vil JavaScript først se i child scope, ikke finne egenskapen, deretter se i det arvede scope og finne egenskapen. (Hvis det ikke fant egenskapen i parentScope, ville det fortsette opp prototypkjeden... helt opp til root scope). Så, disse er alle sanne:
childScope.aString === 'parent string'
childScope.anArray[1] === 20
childScope.anObject.property1 === 'parent prop1'
childScope.aFunction() === 'parent output'
La oss da anta at vi gjør dette:
childScope.aString = 'child string'
Prototypekjeden blir ikke konsultert, og en ny aString-egenskap blir lagt til i childScope. Denne nye egenskapen skjuler/overskygger parentScope-egenskapen med samme navn. Dette vil bli veldig viktig når vi diskuterer ng-repeat og ng-include nedenfor.
Anta at vi da gjør dette:
childScope.anArray[1] = '22'
childScope.anObject.property1 = 'child prop1'
Prototypekjeden konsulteres fordi objektene (anArray og anObject) ikke finnes i childScope. Objektene finnes i parentScope, og egenskapsverdiene oppdateres på de originale objektene. Ingen nye egenskaper legges til i childScope; ingen nye objekter opprettes. (Merk at i JavaScript er også arrays og funksjoner objekter.)
La oss da anta at vi gjør dette:
childScope.anArray = [100, 555]
childScope.anObject = { name: 'Mark', country: 'USA' }
Prototypekjeden blir ikke konsultert, og barneområdet får to nye objekt-egenskaper som skjuler/overskygger foreldreområdet objekt-egenskaper med samme navn.
Viktige punkter:
- Hvis vi leser childScope.propertyX, og childScope har propertyX, blir ikke prototypekjeden konsultert.
- Hvis vi setter childScope.propertyX, blir ikke prototypkjeden konsultert.
Ett siste scenario:
delete childScope.anArray
childScope.anArray[1] === 22 // true
Vi slettet først childScope-egenskapen, og når vi deretter prøver å få tilgang til egenskapen igjen, blir prototypekjeden konsultert.
Angular Scope-arv
Konkurrentene:
- Følgende oppretter nye scope, og arver prototypisk: ng-repeat, ng-include, ng-switch, ng-controller, direktiv med
scope: true, direktiv medtransclude: true. - Følgende oppretter et nytt scope som ikke arver prototypisk: direktiv med
scope: { ... }. Dette oppretter i stedet et «isolerings»-scope.
Merk, som standard oppretter ikke direktiver nytt område -- dvs. standarden er scope: false.
ng-include
Anta at vi har i vår kontroller:
$scope.myPrimitive = 50;
$scope.myObject = {aNumber: 11};
Og i vår 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>
Hver ng-include genererer et nytt barneskop, som prototypisk arver fra foreldreskopet.
Å skrive inn (for eksempel, "77") i den første inndatatboksen får barn-scope til å få en ny myPrimitive scope-egenskap som skjuler/overskygger foreldrescope-egenskapen med samme navn. Dette er sannsynligvis ikke det du ønsker/forventer.
Å skrive inn (for eksempel, "99") i den andre inndatafeltet resulterer ikke i en ny barneegenskap. Fordi tpl2.html binder modellen til en objekt-egenskap, trer prototypisk arv i kraft når ngModel leter etter objektet myObject - det finner det i foreldresammenhengen.
Vi kan omskrive den første malen til å bruke $parent, hvis vi ikke ønsker å endre modellen vår fra et primitivt til et objekt:
<input ng-model="$parent.myPrimitive">
Å skrive inn (si, "22") i denne inntastingsboksen resulterer ikke i en ny barneegenskap. Modellen er nå bundet til en egenskap i foreldresammenhengen (fordi $parent er en barneegenskap som refererer til foreldresammenhengen).
For alle scopes (prototypiske eller ikke), sporer Angular alltid et forelder-barn-forhold (dvs. en hierarki) via scope-egenskapene $parent, $$childHead og $$childTail. Jeg viser normalt ikke disse scope-egenskapene i diagrammene.
For scenarier der skjemaelementer ikke er involvert, er en annen løsning å definere en funksjon på foreldrescope for å endre primitiven. Deretter sørg for at barnet alltid kaller denne funksjonen, som vil være tilgjengelig for barnescope på grunn av prototypisk arv. F.eks.,
// in the parent scope
$scope.setMyPrimitive = function(value) {
$scope.myPrimitive = value;
}
Her er et eksempel på en fiddle som bruker denne «foreldre-funksjon»-tilnærmingen. (Fiddlen ble skrevet som en del av dette svaret: https://stackoverflow.com/a/14104318/215945.)
Se også https://stackoverflow.com/a/13782671/215945 og https://github.com/angular/angular.js/issues/1267.
ng-bryter
ng-switch scope-arv fungerer akkurat som ng-include. Så hvis du trenger toveis databinding til en primitiv i foreldrescope, bruk $parent, eller endre modellen til å være et objekt og bind deretter til en egenskap av det objektet. Dette vil unngå at barnescopes skjuler/overskygger foreldrescope-egenskaper.
Se også AngularJS, bind scope på en switch-case?
ng-repeat
Ng-repeat fungerer litt annerledes. Anta at vi har i vår kontrollør:
$scope.myArrayOfPrimitives = [ 11, 22 ];
$scope.myArrayOfObjects = [{num: 101}, {num: 202}]
Og i vår 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>
For hvert element/iterasjon oppretter ng-repeat et nytt scope, som prototypisk arver fra foreldrescopet, men det tildeler også elementets verdi til en ny egenskap på det nye barns scopet. (Navnet på den nye egenskapen er navnet på løkkevariabelen.) Her er hva Angular-kildekoden for ng-repeat faktisk er:
childScope = scope.$new(); // child scope prototypically inherits from parent scope
...
childScope[valueIdent] = value; // creates a new childScope property
Hvis elementet er en primitiv (som i myArrayOfPrimitives), blir i praksis en kopi av verdien tildelt den nye egenskapen i child-scope. Å endre verdien til child-scope-egenskapen (dvs. ved å bruke ng-model, altså child-scope num) endrer ikke arrayet som parent-scope refererer til. Så i det første ng-repeat-eksempelet over, får hver child-scope en num-egenskap som er uavhengig av myArrayOfPrimitives-arrayet:
Denne ng-repeat vil ikke fungere (som du vil/forventer). Å skrive inn i tekstboksene endrer verdiene i de grå boksene, som kun er synlige i de underordnede scope-ene. Det vi ønsker, er at inputtene skal påvirke myArrayOfPrimitives-arrayen, ikke en primitive egenskap i et underordnet scope. For å oppnå dette, må vi endre modellen til å være et array av objekter.
Så, hvis elementet er et objekt, blir en referanse til det opprinnelige objektet (ikke en kopi) tildelt den nye egenskapen i barn-scope. Å endre verdien til barn-scope-egenskapen (dvs. ved bruk av ng-model, altså obj.num) endrer objektet som foreldrescope refererer til. Så i den andre ng-repeat ovenfor, har vi:
(Jeg farget en linje grå bare for at det skal være klart hvor den går.)
Dette fungerer som forventet. Å skrive inn i tekstboksene endrer verdiene i de grå boksene, som er synlige for både barne- og foreldrescope.
Se også Vanskeligheter med ng-model, ng-repeat og input og https://stackoverflow.com/a/13782671/215945
ng-kontroller
Nestede controllere ved bruk av ng-controller resulterer i normal prototypisk arv, akkurat som ng-include og ng-switch, så de samme teknikkene gjelder. Imidlertid «anses det som dårlig form at to controllere deler informasjon via $scope-arv» -- http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ En tjeneste bør brukes for å dele data mellom controllere i stedet.
(Hvis du virkelig vil dele data via controlleres scopes arv, er det ingenting du trenger å gjøre. Barnescope vil ha tilgang til alle foreldrescope-egenskapene. Se også Controller last rekkefølge varierer når du laster eller navigerer)
direktiver
- standard (
scope: false) - direktivet oppretter ikke et nytt scope, så det er ingen arv her. Dette er enkelt, men også farlig fordi for eksempel et direktiv kan tro at det oppretter en ny egenskap på scopet, når det faktisk overskriver en eksisterende egenskap. Dette er ikke et godt valg for å skrive direktiver som er ment å være gjenbrukbare komponenter. scope: true- direktivet oppretter et nytt barneomfang som prototypisk arver fra foreldreskopet. Hvis mer enn ett direktiv (på samme DOM-element) ber om et nytt omfang, opprettes kun ett nytt barneomfang. Siden vi har "normal" prototypisk arv, ligner dette på ng-include og ng-switch, så vær forsiktig med toveis datainnbinding til primitive verdier i foreldreskopet, og barneomfang som skjuler/skygger foreldreskopets egenskaper.Et attributt må brukes for å spesifisere hver foreldre-egenskap som direktivet ønsker å binde til:
<div my-directive the-Parent-Prop=parentProp>ogscope: { localProp: '@theParentProp' }.
Isolert scope sin__proto__refererer til Object. Isolert scope sin $parent refererer til foreldrescope, så selv om det er isolert og ikke arver prototypisk fra foreldrescopet, er det fortsatt et barnescope.
For bildet under har vi
<my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">og
scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
Også, anta at direktivet gjør dette i sin linking-funksjon:scope.someIsolateProp="Jeg er isolert"transclude: true- direktivet oppretter et nytt "transkludert" barneskop, som prototypisk arver fra foreldreskopet. Det transkluderte skopet og det isolerte skopet (hvis noen) er søsken -- $parent-egenskapen til hvert skop refererer til samme foreldreskop. Når både et transkludert og et isolert skop eksisterer, vil isolate skopets egenskap $$nextSibling referere til det transkluderte skopet. Jeg kjenner ikke til noen nyanser med det transkluderte skopet.
For bildet nedenfor, anta samme direktiv som ovenfor med dette tillegget:transclude: true
For mer informasjon om isolerte scopes, se http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
Denne fiddlen har en showScope()-funksjon som kan brukes til å undersøke et isolert og transkludert scope. Se instruksjonene i kommentarene i fiddlen.
Sammendrag
Det finnes fire typer omfang:
- normal prototypisk omfangsarv -- ng-include, ng-switch, ng-controller, direktiv med
scope: true - normal prototypisk scoperarv med en kopi/tildeling -- ng-repeat. Hver iterasjon av ng-repeat lager et nytt barnscope, og det nye barnscoopen får alltid en ny egenskap.
- isolert scope -- direktiv med
scope: {...}. Denne er ikke prototypisk, men '=', '@', og '&' gir en mekanisme for å få tilgang til foreldrescope-egenskaper, via attributter. - transkludert omfang -- direktiv med
transclude: true. Dette er også normal prototypisk omfangsarv, men det er også en søsken til ethvert isolert omfang.
For alle scopes (prototypiske eller ikke), følger Angular alltid en foreldre-barn-relasjon (dvs. et hierarki), via egenskapene $parent og $$childHead og $$childTail.
Diagrammer ble generert med Graphviz "*.dot"-filer, som finnes på GitHub. Tim Caswells "Learning JavaScript with Object Graphs" var inspirasjonen for å bruke GraphViz til diagrammene.
Svar #2
Jeg ønsker på ingen måte å konkurrere med Marks svar, men ville bare fremheve delen som endelig fikk alt til å falle på plass som en nybegynner i Javascript-arv og dens prototypekjede.
Kun egenskapsavlesninger søker i prototype-kjeden, ikke skrivinger. Så når du setter
myObject.prop = '123';
Det ser ikke oppover kjeden, men når du setter
myObject.myThing.prop = '123';
det pågår en subtil lesing innenfor den skriveoperasjonen som prøver å slå opp myThing før den skriver til dens prop. Så det er derfor skriving til object.properties fra barnet får tilgang til foreldrens objekter.
Svar #3
Jeg vil gjerne legge til et eksempel på prototypisk arv med JavaScript til @Scott Driscolls svar. Vi vil bruke det klassiske arvsmønsteret med Object.create(), som er en del av EcmaScript 5-spesifikasjonen.
Først oppretter vi "Parent" objektfunksjon
function Parent(){
}
Legg deretter til en prototype til "Forelder"-objektfunksjonen
Parent.prototype = {
primitive : 1,
object : {
one : 1
}
}
Opprett «Child»-objektfunksjon
function Child(){
}
Tildel barneprototyp (La barneprototyp arve fra foreldreprototyp)
Child.prototype = Object.create(Parent.prototype);
Tildel riktig «Child»-prototypekonstruktør
Child.prototype.constructor = Child;
Legg til metoden "changeProps" til et barneprototype, som vil omskrive verdien av egenskapen "primitive" i Child-objektet og endre verdien av "object.one" både i Child- og Parent-objektene
Child.prototype.changeProps = function(){
this.primitive = 2;
this.object.one = 2;
};
Initier foreldre (pappa) og barn (sønn) objekter.
var dad = new Parent();
var son = new Child();
Kall Child (sønn) changeProps-metoden
son.changeProps();
Sjekk resultatene.
Foreldre-primitiv egenskap endret seg ikke
console.log(dad.primitive); /* 1 */
Barnets primitive egenskap endret (overskrevet)
console.log(son.primitive); /* 2 */
Forelder- og barnobjekt. En egenskap endret
console.log(dad.object.one); /* 2 */
console.log(son.object.one); /* 2 */
Arbeidende eksempel her http://jsbin.com/xexurukiso/1/edit/
Mer informasjon om Object.create her https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create












