AngularJS'de prototip / prototip kalıtım kapsamının nüansları nelerdir?


1028

API Referansı Kapsam sayfa diyor ki:

Bir kapsam , üst kapsamdan devralınabilir.

Geliştirici Kılavuzu Kapsam sayfa diyor ki:

Bir kapsam (prototip olarak) özellikleri üst kapsamından devralır.

  • Peki, bir alt kapsam her zaman prototipik olarak üst kapsamından miras kalır?
  • İstisnalar var mı?
  • Devralındığında, her zaman normal bir JavaScript prototipi mirası mı?

Yanıtlar:


1740

Hızlı cevap :
Bir alt kapsam normalde üst kapsamından prototip olarak miras alır, ancak her zaman değil. Bu kuralın bir istisnası, direktiftir scope: { ... }- bu, prototip olarak miras almayan bir "ayrı tutma" kapsamı oluşturur. Bu yapı genellikle "yeniden kullanılabilir bileşen" yönergesi oluşturulurken kullanılır.

Nüanslara gelince, kapsam mirası normalde düzdür ... alt kapsamda 2 yönlü veri bağlama (yani, form öğeleri, ng-model) gerekene kadar . Üst kapsamda bir ilkeye (örn., Sayı, dize, boole) bağlanmaya çalışırsanız , alt kapsamın içinden ng-repeat, ng-switch ve ng-include sizi uyarabilir. Çoğu insanın çalışmasını beklediği şekilde çalışmaz. Alt kapsam, aynı adın üst özelliğini gizleyen / gölgeleyen kendi özelliğini alır. Geçici çözümleriniz

  1. modeliniz için üst öğedeki nesneleri tanımlayın, ardından alt öğedeki o nesnenin özelliğine başvurun: parentObj.someProp
  2. $ parent.parentScopeProperty kullanın (her zaman mümkün değildir, ancak mümkünse 1'den daha kolay)
  3. Üst kapsamda bir işlev tanımlayın ve alt işlevden çağırın (her zaman mümkün değildir)

Yeni angularjs geliştiriciler genellikle fark yoktur ng-repeat, ng-switch, ng-view, ng-includeve ng-ifbu direktifler söz konusu olduğunda sorun genellikle gösterir böylece tüm yeni alt kapsamları oluşturun. ( Sorunun hızlı bir açıklaması için bu örneğe bakın .)

Bu ilkellerle ilgili sorun her zaman bir 'sahip olmak' için "en iyi uygulama" izlenerek kolayca önlenebilir . ng modellerinizde - 3 dakika değerinde izleyin. Misko, ilkel bağlanma sorununu gösterir ng-switch.

Bir '.' modellerinizde prototip mirasın oyunda olduğundan emin olursunuz. Yani, kullanın

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

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


Uzun cevap :

JavaScript Prototypal Kalıtım

Ayrıca AngularJS wiki'sine de yerleştirilir: https://github.com/angular/angular.js/wiki/Understanding-Scopes

İlk olarak, özellikle sunucu tarafı geçmişinden geliyorsanız ve sınıfsal kalıtım hakkında daha fazla bilgi sahibi iseniz, prototippal miras hakkında sağlam bir anlayışa sahip olmanız önemlidir. Önce bunu gözden geçirelim.

ParentScope'un aString, aNumber, anArray, anObject ve aFunction özelliklerine sahip olduğunu varsayalım. ChildScope prototipik olarak parentScope'tan miras alırsa:

prototip kalıtım

(Yerden tasarruf etmek için, anArraynesneyi üç ayrı gri değişmez değerine sahip tek bir mavi nesne yerine üç değeriyle tek bir mavi nesne olarak göstereceğim .)

ParentScope üzerinde tanımlanan bir özelliğe alt kapsamdan erişmeye çalışırsak, JavaScript ilk olarak alt kapsamı arar, özelliği bulmaz, sonra devralınan kapsamı arar ve özelliği bulur. (Mülkü parentScope'ta bulamazsa, prototip zincirini ... kök kapsamına kadar devam ettirir). Yani, bunların hepsi doğrudur:

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

Diyelim ki bunu yapıyoruz:

childScope.aString = 'child string'

Prototip zincirine başvurulmaz ve childScope'a yeni bir aString özelliği eklenir. Bu yeni özellik parentScope özelliğini aynı adla gizler / gölgeler. Aşağıda tekrar ve tekrarlama konularını ele aldığımızda bu çok önemli hale gelecektir.

mülkiyet gizleme

Diyelim ki bunu yapıyoruz:

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

Nesneler (anArray ve anObject) childScope'ta bulunmadığından prototip zincirine danışılır. Nesneler parentScope içinde bulunur ve özellik değerleri orijinal nesnelerde güncellenir. ChildScope'a yeni özellik eklenmez; yeni nesne oluşturulmaz. (JavaScript dizilerinde ve işlevlerinde de nesne olduğunu unutmayın.)

prototip zincirini takip et

Diyelim ki bunu yapıyoruz:

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

Prototip zincirine başvurulmaz ve alt kapsam, parentScope nesne özelliklerini aynı adlarla gizleyen / gölgeleyen iki yeni nesne özelliği alır.

daha fazla mülk gizleme

çıkarımlar:

  • ChildScope.propertyX'i okursak ve childScope'un propertyX'i varsa, prototip zincirine başvurulmaz.
  • ChildScope.propertyX ayarını yaparsak, prototip zincirine başvurulmaz.

Son bir senaryo:

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

Önce childScope özelliğini sildik, sonra mülke tekrar erişmeye çalıştığımızda prototip zincirine danışılıyor.

bir alt mülkü çıkardıktan sonra


Açısal Kapsam Mirası

Yarışmacılar:

  • Aşağıdakiler yeni kapsamlar oluşturur ve prototip olarak miras alır: ng-repeat, ng-include, ng-switch, ng-controller, ile yönerge scope: true, ile yönerge transclude: true.
  • Aşağıda, prototip olarak miras alınmayan yeni bir kapsam oluşturulur: ile yönerge scope: { ... }. Bu, bunun yerine bir "ayrı tutma" kapsamı oluşturur.

Varsayılan olarak, direktiflerin yeni kapsam oluşturmadığını unutmayın - yani, varsayılan değerdir scope: false.

ng içerir

Farz edelim ki denetleyicimizde:

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

Ve HTML kodumuzda:

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

Her ng-include, prototipik olarak üst kapsamdan miras alan yeni bir alt kapsam oluşturur.

ng-include çocuk kapsamları

İlk girdi metin kutusuna (77 gibi) yazmak, alt kapsamın myPrimitive, aynı adın üst kapsam özelliğini gizleyen / gölgeleyen yeni bir kapsam özelliği almasına neden olur . Bu muhtemelen istediğiniz / beklediğiniz değil.

bir ilkel ile ng-include

İkinci giriş metin kutusuna (örneğin, "99") yazmak yeni bir alt öğe ile sonuçlanmaz. Tpl2.html modeli bir nesne özelliğine bağladığından, ngModel myObject nesnesini ararken prototypal mirası devreye girer - üst kapsamda bulur.

ng-include ile bir nesne

Modelimizi ilkelden bir nesneye değiştirmek istemiyorsak, ilk şablonu $ parent kullanmak için yeniden yazabiliriz:

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

Bu giriş metin kutusuna (örneğin, "22") yazmak yeni bir alt öğe ile sonuçlanmaz. Model artık üst kapsamın bir özelliğine bağlıdır (çünkü $ üst, üst kapsama başvuran bir alt kapsam özelliğidir).

ng-include ile $ parent

Tüm kapsamlar için (prototip veya değil), Açısal, $ parent, $$ childHead ve $$ childTail kapsam özellikleri aracılığıyla her zaman bir üst-alt ilişkisini (yani bir hiyerarşi) izler. Normalde bu kapsam özelliklerini diyagramlarda göstermem.

Form öğelerinin yer almadığı senaryolarda, başka bir çözüm, ilkel üzerinde değişiklik yapmak için üst kapsamda bir işlev tanımlamaktır. Daha sonra, çocuğun her zaman prototip mirası nedeniyle alt kapsam tarafından kullanılabilecek bu işlevi çağırdığından emin olun. Örneğin,

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

İşte bu "ana işlev" yaklaşımını kullanan örnek bir keman . (Bu keman bu cevabın bir parçası olarak yazılmıştır: https://stackoverflow.com/a/14104318/215945 .)

Ayrıca bkz . Https://stackoverflow.com/a/13782671/215945 ve https://github.com/angular/angular.js/issues/1267 .

ng şalter

ng-switch kapsam mirası tıpkı ng-include gibi çalışır. Bu nedenle, üst kapsamdaki bir ilkeye 2 yönlü veri bağlamaya ihtiyacınız varsa, $ parent kullanın veya modeli bir nesne olarak değiştirin ve sonra bu nesnenin bir özelliğine bağlayın. Bu, alt kapsamın üst kapsam özelliklerinin gizlenmesini / gölgelenmesini önleyecektir.

Ayrıca bkz. AngularJS, bir anahtar kasasının bağlama kapsamı?

ng tekrar

Ng-repeat biraz farklı çalışır. Farz edelim ki denetleyicimizde:

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

Ve HTML kodumuzda:

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

Her öğe / yineleme için ng-repeat, üst kapsamdan prototipik olarak miras alan yeni bir kapsam oluşturur, ancak aynı zamanda öğenin değerini yeni alt kapsamdaki yeni bir özelliğe atar . (Yeni özelliğin adı döngü değişkeninin adıdır.) Ng-repeat için Açısal kaynak kodunun gerçekte şudur:

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

Öğe bir ilkel ise (myArrayOfPrimitives'ta olduğu gibi), esas olarak değerin bir kopyası yeni alt kapsam özelliğine atanır. (Ng-modeli, bu nedenle çocuk kapsamı kullanılarak, örneğin, çocuk kapsamı özelliğin değerini değiştirmek numetmez) değil dizi üst kapsamı referans olarak değiştirin. Bu nedenle, yukarıdaki ilk ng yinelenmesinde, her alt kapsam nummyArrayOfPrimitives dizisinden bağımsız bir özellik alır :

ilkellerle tekrarlama

Bu tekrarlama çalışmaz (istediğiniz gibi / beklediğiniz gibi). Metin kutularına yazmak, gri kutulardaki değerleri değiştirir; bunlar yalnızca alt kapsamlarda görünür. İstediğimiz girdilerin alt kapsam ilkel özelliğini değil, myArrayOfPrimitives dizisini etkilemesi. Bunu yapmak için, modeli bir nesne dizisi olarak değiştirmemiz gerekir.

Bu nedenle, öğe bir nesne ise, yeni alt etki özelliğine orijinal nesneye (kopya değil) bir başvuru atanır. Çocuk kapsamı özelliğin değerini değiştirme (ng-modeli kullanılarak, örneğin, bu nedenle obj.num) yapar nesne üst kapsamı referans olarak değiştirin. Yani yukarıdaki ikinci tekrarda:

nesnelerle ng-tekrar

(Nereye gideceğini netleştirmek için bir çizgi gri renklendirdim.)

Bu beklendiği gibi çalışır. Metin kutularına yazmak, hem alt hem de üst kapsamlar tarafından görülebilen gri kutulardaki değerleri değiştirir.

Ayrıca bkz. Ng-model, ng-repeat ve girişlerle ilgili zorluk ve https://stackoverflow.com/a/13782671/215945

ng kontrol cihazı

Ng-controller kullanan kontrolörler yuvalama, tıpkı ng-include ve ng-switch gibi normal prototip palet mirasıyla sonuçlanır, bu nedenle aynı teknikler geçerlidir. Ancak, "iki denetleyicinin $ scope miras yoluyla bilgi paylaşması kötü bir form olarak kabul edilir" - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ Verileri paylaşmak için bir hizmet kullanılmalıdır yerine denetleyiciler.

(Verileri gerçekten denetleyicilerin kapsamını devralma yoluyla paylaşmak istiyorsanız, yapmanız gereken hiçbir şey yoktur. Alt kapsamın tüm üst kapsam özelliklerine erişimi olacaktır. Ayrıca bkz. Denetleyici yükleme sırası yükleme veya gezinme sırasında farklılık gösterir )

direktifler

  1. default ( scope: false) - yönerge yeni bir kapsam oluşturmaz, bu nedenle burada kalıtım yoktur. Bu kolay, ama aynı zamanda tehlikelidir, örneğin, örneğin, bir direktif, aslında mevcut bir mülkü gizlerken, kapsamda yeni bir mülk yarattığını düşünebilir. Bu, yeniden kullanılabilir bileşenler olarak tasarlanan yönergeleri yazmak için iyi bir seçim değildir.
  2. scope: true- yönerge, üst kapsamdan prototipik olarak miras kalan yeni bir alt kapsam oluşturur. Birden fazla yönerge (aynı DOM öğesinde) yeni bir kapsam isterse, yalnızca bir yeni alt kapsam oluşturulur. "Normal" prototip kalıtımımız olduğundan, bu ng-include ve ng-switch gibidir, bu nedenle ana kapsam ilkellerine 2 yönlü veri bağlama ve ana kapsam özelliklerinin alt kapsam gizleme / gölgelemesi konusunda dikkatli olun.
  3. scope: { ... }- yönerge yeni bir izolat / yalıtılmış kapsam oluşturur. Prototip olarak miras almaz. Bu, yeniden kullanılabilir bileşenler oluştururken genellikle en iyi seçimdir, çünkü yönerge yanlışlıkla üst kapsamı okuyamaz veya değiştiremez. Ancak, bu tür direktiflerin genellikle birkaç üst kapsam özelliğine erişmesi gerekir. Nesne karması, ana kapsam ile ayırma kapsamı arasında iki yönlü ('=' kullanarak) veya tek yönlü ('@' kullanarak) bağlayıcı ayarlamak için kullanılır. Üst kapsam ifadelerine bağlanmak için '&' de vardır. Böylece, bunların tümü üst kapsamdan türetilen yerel kapsam özellikleri oluşturur. Özniteliklerin bağlayıcıyı ayarlamaya yardımcı olması için kullanıldığını unutmayın; yalnızca nesne karmasındaki üst kapsam özellik adlarına başvuramazsınız, bir öznitelik kullanmanız gerekir. Örneğin, üst mülke bağlanmak istiyorsanız bu işe yaramazparentPropizole kapsamda: <div my-directive>ve scope: { localProp: '@parentProp' }. Direktifin bağlamak istediği her bir üst özelliği belirtmek için bir öznitelik kullanılmalıdır: <div my-directive the-Parent-Prop=parentProp>ve scope: { localProp: '@theParentProp' }.
    Kapsamın __proto__referanslarını izole edin Object. İzolasyon kapsamının $ üst öğesi üst kapsama başvurur, bu nedenle yalıtılmış ve üst kapsamdan prototip olarak miras almasa da, yine de bir alt kapsamdır.
    Aşağıdaki resim için
    <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">ve
    scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
    ayrıca, direktifin bunu bağlantı işlevinde yaptığını varsayalım: scope.someIsolateProp = "I'm isolated"
    izole kapsam
    İzole kapsamlar hakkında daha fazla bilgi için bkz. Http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true- yönerge, prototipik olarak üst kapsamdan miras alan yeni bir "aktarılan" alt kapsam oluşturur. Aktarılan ve yalıtılmış kapsam (varsa) kardeşlerdir - her bir kapsamın $ parent özelliği aynı üst kapsamı belirtir. Kopyalanan ve ayrılan kapsamın her ikisi de mevcutsa, yalıtımlı kapsam özelliği $$ nextSibling, aktarılan kapsamı referans alır. Kopyalanan kapsamdaki herhangi bir nüansın farkında değilim.
    Aşağıdaki resim için, bu ilavelerle yukarıdaki talimatın aynısını varsayalım:transclude: true
    aşılmış kapsam

Bu kemanınshowScope() bir izolat ve transkripsiyon kapsamını incelemek için kullanılabilecek bir işlevi vardır. Kemandaki yorumlardaki talimatlara bakın.


özet

Dört tür kapsam vardır:

  1. normal prototip kapsamı devralma - ng-include, ng-switch, ng-denetleyici, yönerge scope: true
  2. kopya / atama - ng-tekrarlı normal prototip kapsamı devralma. Her ng tekrarlama yeni bir alt kapsam oluşturur ve bu yeni alt kapsam her zaman yeni bir mülk edinir.
  3. kapsamı yalıtmak scope: {...}. Bu prototip değildir, ancak '=', '@' ve '&' öznitelikler aracılığıyla üst kapsam özelliklerine erişmek için bir mekanizma sağlar.
  4. transcluded kapsam - yönerge transclude: true. Bu aynı zamanda normal prototip kapsam mirasıdır, ancak aynı zamanda herhangi bir izolat kapsamının kardeşidir.

Tüm kapsamlar için (prototip veya değil), Angular her zaman $ parent ve $$ childHead ve $$ childTail özellikleri aracılığıyla bir üst-alt ilişkisini (yani, bir hiyerarşi) izler.

Şemalar Hangi "* .dot" dosyaları, github . Tim Caswell'in " Nesne Grafikleriyle JavaScript Öğrenme ", diyagramlar için GraphViz kullanmanın ilham kaynağı oldu.


48
Müthiş makale, SO cevap için çok uzun, ama yine de çok yararlı. Lütfen bir düzenleyicinin boyutlarını azaltmadan blogunuza ekleyin.
Aralık'ta

43
AngularJS wikisine bir kopya koydum .
Mark Rajcok

3
Düzeltme: "Kapsam __proto__başvuruları Nesne izole edin ." bunun yerine "Kapsamın __proto__referanslarını bir Scope nesnesine ayırın" olmalıdır . Bu nedenle, son iki resimde, turuncu "Nesne" kutuları bunun yerine "Kapsam" kutuları olmalıdır.
Mark Rajcok

15
Bu asnwer angularjs rehberine dahil edilmelidir. Bu çok daha didaktik ...
Marcelo De Zen

2
Wiki beni şaşırttı, önce şöyle okur: "Prototip zincirine danışılır, çünkü nesne childScope'ta bulunmaz." ve sonra şunu okur: "childScope.propertyX'i ayarlarsak, prototip zincirine danışılmaz." İkincisi bir koşul anlamına gelirken, birincisi değildir.
Stephane

140

Hiçbir şekilde Mark'ın cevabı ile rekabet etmek istemiyorum, ama sonunda her şeyi Javascript mirasına ve prototip zincirine yeni biri olarak tıklayan parçayı vurgulamak istedim .

Prototip zincirinde arama sadece özellik okur, yazar. Böylece,

myObject.prop = '123';

Zincire bakmaz, ama

myObject.myThing.prop = '123';

Bu yazma işlemi içinde , pervanesine yazmadan önce myThing'i aramaya çalışan ince bir okuma var . Bu yüzden nesneye yazmak. Çocuktan gelen özellikler ebeveynin nesnelerine ulaşır.


12
Bu çok basit bir kavram olsa da, çok açık olmayabilir çünkü inanıyorum ki birçok insan onu özlüyor. İyi koy.
moljac024

3
Mükemmel not. Uzaklaştım, bir nesne özelliğinin çözünürlüğü bir okuma içermezken bir nesne özelliğinin çözünürlüğü yapar.
Stephane

1
Neden? Prototip zincirine çıkmayan mülkiyet yazarlarının motivasyonu nedir? Deli gibi görünüyor ...
Jonathan.

1
Çok basit bir örnek eklerseniz harika olur.
tylik

2
Bildirim o does için prototip zincirini arama belirleyiciler . Hiçbir şey bulunmazsa, alıcıda bir özellik oluşturur.
Bergi

21

@Scott Driscoll cevabına javascript ile prototipsel kalıtım örneği eklemek istiyorum. EcmaScript 5 spesifikasyonunun bir parçası olan Object.create () ile klasik kalıtım modelini kullanacağız.

Önce "Üst" nesne işlevi oluşturuyoruz

function Parent(){

}

Sonra "Üst" nesne işlevine bir prototip ekleyin

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

"Alt" nesne işlevi oluşturma

function Child(){

}

Alt prototip ata (Üst prototipin üst prototipten devralmasını sağla)

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

Uygun "Alt" prototip oluşturucuyu atayın

Child.prototype.constructor = Child;

Child nesnesindeki "ilkel" özellik değerini yeniden yazacak ve hem Child hem de Parent nesnelerinde "object.one" değerini değiştirecek bir alt prototipe "changeProps" yöntemi ekleyin

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

Ebeveyn (baba) ve Çocuk (oğul) nesnelerini başlatın.

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

Çocuk (oğul) changeProps yöntemini çağırın

son.changeProps();

Sonuçları kontrol edin.

Ana ilkel özellik değişmedi

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

Çocuk ilkel özelliği değişti (yeniden yazıldı)

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

Üst ve Alt object.one özellikleri değişti

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

Burada çalışma örneği http://jsbin.com/xexurukiso/1/edit/

Object.create hakkında daha fazla bilgi burada https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.