Jakie są niuanse dziedziczenia prototypowego / prototypowego w AngularJS?

1028

API Reference strona Zakres mówi:

Zakres może dziedziczyć z zakresu nadrzędnego.

Developer Guide strona Zakres mówi:

Zakres (prototypowo) dziedziczy właściwości z zakresu nadrzędnego.

  • Czy zatem zakres potomny zawsze dziedziczy prototypowo po zakresie macierzystym?
  • Czy są wyjątki?
  • Kiedy dziedziczy, czy zawsze jest to normalne dziedziczenie prototypowe JavaScript?
Mark Rajcok
źródło

Odpowiedzi:

1740

Szybka odpowiedź :
zakres potomny zwykle prototypowo dziedziczy po zakresie macierzystym, ale nie zawsze. Jedynym wyjątkiem od tej reguły jest dyrektywa z scope: { ... }- tworzy to zakres „izolowania”, który nie dziedziczy prototypowo. Ta konstrukcja jest często używana podczas tworzenia dyrektywy „komponent wielokrotnego użytku”.

Jeśli chodzi o niuanse, dziedziczenie zakresu jest zwykle proste ... dopóki nie będzie potrzebne dwukierunkowe wiązanie danych (tj. Elementy formularza, model ng) w zasięgu potomnym. Ng-repeat, ng-switch i ng-include mogą cię wyzwolić, jeśli spróbujesz połączyć się z operacją podstawową (np. Liczbą, łańcuchem, wartością logiczną) w zakresie nadrzędnym z zakresu podrzędnego. Nie działa tak, jak większość ludzi się spodziewa. Zasięg podrzędny otrzymuje własną właściwość, która ukrywa / ukrywa właściwość nadrzędną o tej samej nazwie. Twoje obejścia są

  1. zdefiniuj obiekty w obiekcie nadrzędnym dla swojego modelu, a następnie odwołaj się do właściwości tego obiektu w obiekcie podrzędnym: parentObj.someProp
  2. użyj $ parent.parentScopeProperty (nie zawsze możliwe, ale łatwiejsze niż 1. tam, gdzie to możliwe)
  3. zdefiniuj funkcję w zakresie nadrzędnym i wywołaj ją od dziecka (nie zawsze jest to możliwe)

Nowe angularjs deweloperzy często nie zdają sobie sprawy, że ng-repeat, ng-switch, ng-view, ng-includei ng-ifwszystkim tworzenie nowych zakresów podrzędnych, więc problem często pojawia się, gdy zaangażowane są te wytyczne. (Zobacz ten przykład, aby szybko zilustrować problem.)

Tego problemu z prymitywami można łatwo uniknąć, postępując zgodnie z „najlepszymi praktykami” zawsze mieć „.” w swoich modelach ng - obejrzyj 3 minuty. Misko demonstruje pierwotny problem wiązania z ng-switch.

Posiadające '.' w twoich modelach zapewni, że dziedzictwo prototypowe będzie w grze. Więc użyj

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

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


Długa odpowiedź :

JavaScript Prototypal Inheritance

Umieszczono również na wiki AngularJS: https://github.com/angular/angular.js/wiki/Understanding-Scopes

Ważne jest, aby najpierw dobrze zrozumieć dziedziczenie prototypów, szczególnie jeśli pochodzisz z tła po stronie serwera i jesteś bardziej zaznajomiony z dziedziczeniem klasowym. Sprawdźmy to najpierw.

Załóżmy, że parentScope ma właściwości aString, aNumber, anArray, anObject i aFunction. Jeśli childScope prototypowo dziedziczy po parentScope, mamy:

dziedziczenie prototypowe

(Uwaga: aby zaoszczędzić miejsce, pokazuję anArrayobiekt jako pojedynczy niebieski obiekt z jego trzema wartościami, a nie jako pojedynczy niebieski obiekt z trzema oddzielnymi szarymi literałami.)

Jeśli spróbujemy uzyskać dostęp do właściwości zdefiniowanej w parentScope z zakresu potomnego, JavaScript najpierw zajmie się zasięgiem potomnym, nie znajdzie właściwości, a następnie przejdzie w dziedziczonym zakresie i znajdzie właściwość. (Gdyby nie znalazł właściwości w parentScope, kontynuowałby w górę łańcucha prototypów ... aż do zakresu głównego). Tak więc wszystkie są prawdziwe:

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

Załóżmy, że wtedy to robimy:

childScope.aString = 'child string'

Łańcuch prototypów nie jest konsultowany, a nowa właściwość aString jest dodawana do childScope. Ta nowa właściwość ukrywa / zacienia właściwość parentScope o tej samej nazwie. Stanie się to bardzo ważne, gdy omówimy poniżej powtórzenie i powtórzenie ng.

ukrywanie nieruchomości

Załóżmy, że wtedy to robimy:

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

Sprawdzono łańcuch prototypów, ponieważ obiektów (anArray i anObject) nie znaleziono w childScope. Obiekty znajdują się w parentScope, a wartości właściwości są aktualizowane na oryginalnych obiektach. Żadne nowe właściwości nie są dodawane do childScope; nie są tworzone nowe obiekty. (Pamiętaj, że w JavaScript tablice i funkcje są również obiektami).

postępuj zgodnie z łańcuchem prototypów

Załóżmy, że wtedy to robimy:

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

Łańcuch prototypów nie jest konsultowany, a zakres potomny otrzymuje dwie nowe właściwości obiektu, które ukrywają / cień właściwości obiektu parentScope o tych samych nazwach.

więcej ukrywania nieruchomości

Na wynos:

  • Jeśli czytamy childScope.propertyX, a childScope ma właściwość X, wówczas łańcuch prototypów nie jest konsultowany.
  • Jeśli ustawimy childScope.propertyX, łańcuch prototypów nie jest konsultowany.

Ostatni scenariusz:

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

Najpierw usunęliśmy właściwość childScope, a następnie, gdy próbujemy ponownie uzyskać dostęp do właściwości, sprawdzany jest łańcuch prototypów.

po usunięciu własności potomnej


Dziedziczenie zakresu kątowego

Uczestnicy:

  • Poniższe elementy tworzą nowe zakresy i dziedziczą prototypowo: ng-repeat, ng-include, ng-switch, ng-controller, dyrektywa z scope: true, dyrektywa z transclude: true.
  • Poniżej utworzono nowy zakres, który nie dziedziczy prototypowo: dyrektywa z scope: { ... }. To tworzy zamiast tego zakres „izoluj”.

Zauważ, że domyślnie dyrektywy nie tworzą nowego zakresu - tzn. Domyślnie jest to scope: false.

ng-include

Załóżmy, że mamy w naszym kontrolerze:

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

A w naszym 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>

Każde dołączenie ng generuje nowy zakres potomny, który prototypowo dziedziczy z zakresu macierzystego.

ng-include zakresy potomne

Wpisanie (powiedzmy „77”) w pierwszym wejściowym polu tekstowym powoduje, że zakres potomny otrzymuje nową myPrimitivewłaściwość scope, która ukrywa / ukrywa właściwość zakresu nadrzędnego o tej samej nazwie. Prawdopodobnie nie jest to, czego chcesz / oczekujesz.

ng-include z prymitywem

Wpisanie (powiedzmy „99”) w drugim wejściowym polu tekstowym nie powoduje powstania nowej właściwości potomnej. Ponieważ tpl2.html wiąże model z właściwością obiektu, dziedziczenie prototypowe rozpoczyna się, gdy ngModel szuka obiektu myObject - znajduje go w zakresie nadrzędnym.

ng-include z obiektem

Możemy przepisać pierwszy szablon, aby użyć $ parent, jeśli nie chcemy zmieniać naszego modelu z prymitywnego na obiekt:

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

Wpisanie (powiedz „22”) w tym wejściowym polu tekstowym nie powoduje utworzenia nowej właściwości potomnej. Model jest teraz powiązany z właściwością zakresu nadrzędnego (ponieważ $ parent jest właściwością zakresu podrzędnego, która odwołuje się do zakresu nadrzędnego).

ng-include z $ parent

Dla wszystkich zakresów (prototypowych lub nie), Angular zawsze śledzi relacje rodzic-dziecko (tj. Hierarchia), poprzez właściwości zakresu $ parent, $$ childHead i $$ childTail. Zwykle nie pokazuję tych właściwości zakresu na diagramach.

W scenariuszach, w których elementy formularza nie są zaangażowane, innym rozwiązaniem jest zdefiniowanie funkcji w zakresie nadrzędnym w celu zmodyfikowania operacji podstawowej. Następnie upewnij się, że dziecko zawsze wywołuje tę funkcję, która będzie dostępna dla zakresu potomnego z powodu dziedziczenia prototypowego. Na przykład,

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

Oto przykładowe skrzypce, które wykorzystują to podejście „funkcji nadrzędnej”. (Skrzypek został napisany w ramach tej odpowiedzi: https://stackoverflow.com/a/14104318/215945 .)

Zobacz także https://stackoverflow.com/a/13782671/215945 i https://github.com/angular/angular.js/issues/1267 .

przełącznik ng

Dziedziczenie zakresu ng-switch działa tak samo jak ng-include. Jeśli więc potrzebujesz dwukierunkowego powiązania danych z operacją podstawową w zakresie nadrzędnym, użyj $ parent lub zmień model na obiekt, a następnie powiąż z właściwością tego obiektu. Pozwoli to uniknąć ukrywania / cieniowania zakresu potomnego właściwości zakresu macierzystego.

Zobacz także AngularJS, powiązać zakres skrzynki przełączników?

powtórz ng

Powtarzanie Ng działa trochę inaczej. Załóżmy, że mamy w naszym kontrolerze:

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

A w naszym 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>

Dla każdego elementu / iteracji ng-repeat tworzy nowy zakres, który prototypowo dziedziczy z zakresu nadrzędnego, ale przypisuje również wartość elementu do nowej właściwości w nowym zakresie potomnym . (Nazwą nowej właściwości jest nazwa zmiennej pętli.) Oto, czym właściwie jest kod źródłowy Angulara dla ng-repeat:

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

Jeśli element jest prymitywny (jak w myArrayOfPrimitives), w zasadzie kopia wartości jest przypisywana do nowej właściwości zasięgu potomnego. Zmiana wartości właściwości zakresu potomnego (tj. Użycie modelu ng, a więc zakresu potomnego num) nie zmienia tablicy, do której odwołują się zakresy rodzicielskie. Tak więc w pierwszym powtórzeniu ng powyżej każdy zakres potomny otrzymuje numwłaściwość niezależną od tablicy myArrayOfPrimitives:

powtórz ng z prymitywami

To powtórzenie ng nie będzie działać (tak jak tego chcesz / oczekujesz). Wpisywanie w polach tekstowych zmienia wartości w szarych polach, które są widoczne tylko w zakresach potomnych. Chcemy, aby dane wejściowe wpływały na tablicę myArrayOfPrimitives, a nie na właściwość pierwotną zakresu potomnego. Aby to osiągnąć, musimy zmienić model na tablicę obiektów.

Tak więc, jeśli element jest obiektem, odwołanie do oryginalnego obiektu (nie kopii) jest przypisywane do nowej właściwości zasięgu potomnego. Zmiana wartości tego mienia zakres dziecka (czyli używając NG-modelu, stąd obj.num) ma zmienić obiekt odniesienia, zakres rodzicielskich. Tak więc w drugim powtórzeniu ng powyżej mamy:

powtórz ng z obiektami

(Pokolorowałem jedną linię na szaro, aby było jasne, dokąd zmierza).

Działa to zgodnie z oczekiwaniami. Wpisywanie w polach tekstowych zmienia wartości w szarych polach, które są widoczne zarówno dla zakresu potomnego, jak i macierzystego.

Zobacz także Trudność z modelem ng, powtórzeniem ng i wejściami oraz https://stackoverflow.com/a/13782671/215945

kontroler ng

Zagnieżdżanie kontrolerów za pomocą kontrolera ng powoduje normalne dziedziczenie prototypów, podobnie jak ng-include i ng-switch, więc obowiązują te same techniki. Jednak „uważa się, że dzielenie się informacjami przez dwóch kontrolerów za pomocą dziedziczenia $ scope jest uważane za złą formę” - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ Należy udostępnić usługę między danymi kontrolery zamiast.

(Jeśli naprawdę chcesz udostępniać dane poprzez dziedziczenie zakresu kontrolerów, nie musisz nic robić. Zasięg potomny będzie miał dostęp do wszystkich właściwości zakresu nadrzędnego. Zobacz także Kolejność ładowania kontrolera różni się podczas ładowania lub nawigacji )

dyrektywy

  1. default ( scope: false) - dyrektywa nie tworzy nowego zakresu, więc nie ma tu dziedziczenia. Jest to łatwe, ale również niebezpieczne, ponieważ np. Dyrektywa może myśleć, że tworzy nową właściwość w zakresie, podczas gdy w rzeczywistości blokuje istniejącą właściwość. To nie jest dobry wybór do pisania dyrektyw, które mają być komponentami wielokrotnego użytku.
  2. scope: true- dyrektywa tworzy nowy zakres potomny, który prototypowo dziedziczy z zakresu macierzystego. Jeśli więcej niż jedna dyrektywa (dla tego samego elementu DOM) żąda nowego zakresu, tworzony jest tylko jeden nowy zakres potomny. Ponieważ mamy „normalne” dziedziczenie prototypów, działa to podobnie do ng-include i ng-switch, więc bądź ostrożny w przypadku dwukierunkowego wiązania danych z operacjami podstawowymi zakresu nadrzędnego oraz ukrywania / cieniowania zakresu potomnego właściwości zakresu nadrzędnego.
  3. scope: { ... }- dyrektywa tworzy nowy zakres izolacji / izolacji. Nie dziedziczy prototypowo. Jest to zwykle najlepszy wybór podczas tworzenia komponentów wielokrotnego użytku, ponieważ dyrektywa nie może przypadkowo odczytać lub zmodyfikować zakresu nadrzędnego. Jednak takie dyrektywy często wymagają dostępu do kilku właściwości zakresu nadrzędnego. Skrót obiektu służy do konfigurowania wiązania dwukierunkowego (przy użyciu „=”) lub wiązania jednokierunkowego (przy użyciu „@”) między zakresem nadrzędnym a zakresem izolowania. Dostępne są również znaki „&” do powiązania z wyrażeniami zakresu nadrzędnego. Tak więc wszystkie one tworzą właściwości zakresu lokalnego, które pochodzą z zakresu nadrzędnego. Zauważ, że atrybuty służą do konfigurowania powiązania - nie możesz po prostu odwoływać się do nazw właściwości zakresu nadrzędnego w skrócie obiektu, musisz użyć atrybutu. Np. To nie zadziała, jeśli chcesz powiązać z właściwością nadrzędnąparentPropw izolowanym zakresie: <div my-directive>oraz scope: { localProp: '@parentProp' }. Do określenia każdej właściwości nadrzędnej, z którą dyrektywa chce się powiązać, należy użyć atrybutu: <div my-directive the-Parent-Prop=parentProp>i scope: { localProp: '@theParentProp' }.
    Izoluj __proto__referencje zakresu Obiekt. Wyodrębnij zakres nadrzędny $ odwołuje się do zakresu nadrzędnego, więc chociaż jest izolowany i nie dziedziczy prototypowo z zakresu nadrzędnego, nadal jest zakresem podrzędnym.
    Na poniższym zdjęciu mamy
    <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">i
    scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
    załóżmy, że dyrektywa robi to w funkcji łączenia: scope.someIsolateProp = "I'm isolated"
    izolowany zakres
    Aby uzyskać więcej informacji na temat zakresów izolowanych, patrz http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true- dyrektywa tworzy nowy „transkoncepcyjny” zakres potomny, który prototypowo dziedziczy z zakresu macierzystego. Uwzględniony i izolowany zakres (jeśli istnieje) jest rodzeństwem - właściwość $ parent każdego zakresu odnosi się do tego samego zakresu macierzystego. Jeśli istnieje zakres transkludowany i zakres izolowany, właściwość isolate scope $$ nextSibling będzie odwoływać się do zakresu włączonego. Nie znam żadnych niuansów w zakresie objętym transkludją.
    Na poniższym zdjęciu załóż tę samą dyrektywę, co powyżej z tym dodatkiem:transclude: true
    zakres objęty

To skrzypce ma showScope()funkcję, której można użyć do zbadania izolowanego i transkludowanego zakresu. Zobacz instrukcje w komentarzach w skrzypcach.


Podsumowanie

Istnieją cztery typy zakresów:

  1. normalne dziedziczenie zakresu prototypowego - ng-include, ng-switch, ng-kontroler, dyrektywa z scope: true
  2. normalne dziedziczenie zakresu prototypowego z kopiowaniem / przypisaniem - powtórzenie ng. Każda iteracja ng-repeat tworzy nowy zakres potomny, a ten nowy zasięg potomny zawsze otrzymuje nową właściwość.
  3. izoluj zakres - dyrektywa z scope: {...}. Ten nie jest prototypowy, ale „=”, „@” i „&” zapewniają mechanizm dostępu do właściwości zakresu nadrzędnego za pośrednictwem atrybutów.
  4. zakres objęty - dyrektywa z transclude: true. Ten jest również normalnym dziedziczeniem zakresu prototypowego, ale jest również rodzeństwem dowolnego zakresu izolowanego.

Dla wszystkich zakresów (prototypowych lub nie), Angular zawsze śledzi relacje rodzic-dziecko (tj. Hierarchia), poprzez właściwości $ parent i $$ childHead i $$ childTail.

Diagramy zostały wygenerowane za pomocą Pliki „* .dot” znajdujące się na github . Inspiracją do wykorzystania GraphViz do diagramów była „ Uczenie się JavaScript z wykresami obiektowymi ” Tima Caswella.

Mark Rajcok
źródło
48
Niesamowity artykuł, zdecydowanie za długi na odpowiedź SO, ale i tak bardzo przydatny. Umieść go na swoim blogu, zanim redaktor zmniejszy go do rozmiaru.
iwein 27.12.12
43
Umieściłem kopię na wiki AngularJS .
Mark Rajcok
3
Korekta: „Wyodrębnij __proto__referencje zakresu Obiekt”. zamiast tego powinno być „Izoluj __proto__odwołania zakresu obiekt Scope”. Tak więc na dwóch ostatnich obrazach pomarańczowe pola „Obiekt” powinny zamiast tego być polami „Zakres”.
Mark Rajcok
15
Ta odpowiedź powinna być zawarta w instrukcji angularjs. To jest znacznie bardziej dydaktyczne ...
Marcelo De Zen
2
Wiki wprawia mnie w zakłopotanie, najpierw czyta: „Sprawdzono łańcuch prototypów, ponieważ obiektu nie znaleziono w childScope”. a następnie brzmi: „Jeśli ustawimy childScope.propertyX, łańcuch prototypów nie jest konsultowany.”. Drugi oznacza warunek, podczas gdy pierwszy nie.
Stephane
140

W żaden sposób nie chcę konkurować z odpowiedzią Marka, ale chciałem tylko podkreślić ten element, który w końcu sprawił, że wszystko kliknęło jako ktoś nowy w dziedziczeniu Javascript i jego łańcuchu prototypów .

Tylko właściwość czyta wyszukiwanie łańcucha prototypów, a nie zapisuje. Więc kiedy ustawisz

myObject.prop = '123';

Nie wyszukuje łańcucha, ale po ustawieniu

myObject.myThing.prop = '123';

w tej operacji zapisu zachodzi subtelny odczyt, który próbuje wyszukać myThing przed zapisaniem do jego rekwizytu. Dlatego właśnie pisanie do object.properties od dziecka dostaje się do obiektów rodzica.

Scott Driscoll
źródło
12
Chociaż jest to bardzo prosta koncepcja, może nie być to bardzo oczywiste, ponieważ, jak sądzę, wielu ludziom tęskni. Dobrze wyłożone.
moljac024
3
Doskonała uwaga. Zabieram, rozdzielczość właściwości innej niż obiekt nie wymaga odczytu, podczas gdy rozdzielczość właściwości obiektu tak.
Stephane
1
Dlaczego? Jaka jest motywacja do pisania nieruchomości, które nie idą w górę łańcucha prototypów? To wydaje się szalone ...
Jonathan.
1
Byłoby wspaniale, gdybyś dodał naprawdę prosty przykład.
tylik
2
Zauważ, że to nie szukaj łańcuch prototypów dla ustawiaczy . Jeśli nic nie zostanie znalezione, tworzy właściwość w odbiorniku.
Bergi,
21

Chciałbym dodać przykład prototypowego dziedziczenia z javascript do odpowiedzi @Scott Driscoll. Będziemy używać klasycznego wzorca dziedziczenia z Object.create (), który jest częścią specyfikacji EcmaScript 5.

Najpierw tworzymy funkcję obiektu „Parent”

function Parent(){

}

Następnie dodaj prototyp do funkcji obiektu „Nadrzędny”

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

Utwórz funkcję obiektu „Dziecko”

function Child(){

}

Przypisz prototyp potomny (Spraw, by prototyp potomny odziedziczył po prototypie rodzica)

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

Przypisz odpowiedni konstruktor prototypu „Dziecko”

Child.prototype.constructor = Child;

Dodaj metodę „changeProps” do prototypu podrzędnego, który przepisze wartość właściwości „pierwotnej” w obiekcie podrzędnym i zmieni wartość „object.one” zarówno w obiektach podrzędnych, jak i nadrzędnych

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

Zainicjuj obiekty Parent (tata) i Child (syn).

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

Wywołaj metodę child (son) changeProps

son.changeProps();

Sprawdź wyniki.

Pierwotna pierwotna właściwość nie uległa zmianie

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

Zmieniono pierwotną właściwość potomną (przepisano)

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

Zmieniono właściwości obiektu nadrzędnego i podrzędnego object.one

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

Przykład działania tutaj http://jsbin.com/xexurukiso/1/edit/

Więcej informacji na temat Object.create tutaj https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create

tylik
źródło