Apa nuansa lingkup prototipal / warisan prototipikal di AngularJS?

1028

The API Halaman Lingkup Referensi mengatakan:

Lingkup dapat mewarisi dari lingkup induk.

The Pengembang Halaman Lingkup Panduan mengatakan:

Lingkup (prototipikal) mewarisi properti dari lingkup induknya.

  • Jadi, apakah ruang lingkup anak selalu mewarisi secara prototipik dari ruang lingkup orang tuanya?
  • Apakah ada pengecualian?
  • Ketika memang mewarisi, apakah itu selalu normal warisan prototypal JavaScript?
Mark Rajcok
sumber

Jawaban:

1741

Jawaban cepat :
Lingkup anak biasanya secara bawaan mewarisi dari lingkup induknya, tetapi tidak selalu. Satu pengecualian untuk aturan ini adalah direktif dengan scope: { ... }- ini menciptakan ruang lingkup "mengisolasi" yang tidak mewarisi secara prototipe. Konstruk ini sering digunakan ketika membuat arahan "komponen yang dapat digunakan kembali".

Sedangkan untuk nuansa, pewarisan lingkup biasanya langsung ... sampai Anda membutuhkan pengikatan data 2 arah (yaitu, elemen bentuk, model-ng) di lingkup anak. Ng-repeat, ng-switch, dan ng-include dapat membuat Anda tersandung jika Anda mencoba untuk mengikat ke primitif (misalnya, angka, string, boolean) dalam lingkup induk dari dalam lingkup anak. Ini tidak berfungsi seperti yang diharapkan kebanyakan orang. Ruang lingkup anak mendapatkan properti sendiri yang menyembunyikan / membayangi properti induk dengan nama yang sama. Solusi Anda adalah

  1. tentukan objek dalam induk untuk model Anda, kemudian referensi properti objek itu pada anak: parentObj.someProp
  2. gunakan $ parent.parentScopeProperty (tidak selalu memungkinkan, tetapi lebih mudah dari 1. jika memungkinkan)
  3. tentukan fungsi pada lingkup orang tua, dan panggil dari anak (tidak selalu memungkinkan)

New AngularJS pengembang sering tidak menyadari bahwa ng-repeat, ng-switch, ng-view, ng-includedan ng-ifsemua membuat lingkup anak yang baru, sehingga masalah sering muncul ketika arahan ini terlibat. (Lihat contoh ini untuk ilustrasi singkat masalah.)

Masalah dengan primitif ini dapat dengan mudah dihindari dengan mengikuti "praktik terbaik" untuk selalu memiliki '.' di ng-model Anda - tonton 3 menit. Misko menunjukkan masalah mengikat primitif dengan ng-switch.

Memiliki sebuah '.' dalam model Anda akan memastikan bahwa warisan prototypal sedang dimainkan. Jadi, gunakan

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

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


Jawaban panjang :

Warisan Prototip JavaScript

Juga ditempatkan di wiki AngularJS: https://github.com/angular/angular.js/wiki/Understanding-Scopes

Penting untuk terlebih dahulu memiliki pemahaman yang kuat tentang pewarisan prototypal, terutama jika Anda berasal dari latar belakang sisi server dan Anda lebih akrab dengan warisan klasik. Jadi mari kita tinjau dulu itu.

Misalkan parentScope memiliki properti aString, aNumber, anArray, anObject, dan aFunction. Jika childScope mewarisi secara prototipe dari parentScope, kami memiliki:

warisan prototypal

(Perhatikan bahwa untuk menghemat ruang, saya menunjukkan anArrayobjek sebagai objek biru tunggal dengan tiga nilainya, daripada objek biru tunggal dengan tiga literal abu-abu yang terpisah.)

Jika kami mencoba mengakses properti yang ditentukan di parentScope dari cakupan anak, JavaScript pertama-tama akan melihat dalam lingkup anak, tidak menemukan properti, kemudian melihat dalam lingkup yang diwarisi, dan menemukan properti. (Jika tidak menemukan properti di parentScope, itu akan melanjutkan rantai prototipe ... sampai ke lingkup root). Jadi, ini semua benar:

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

Misalkan kita melakukan ini:

childScope.aString = 'child string'

Rantai prototipe tidak dikonsultasikan, dan properti aString baru ditambahkan ke childScope. Properti baru ini menyembunyikan / membayangi properti parentScope dengan nama yang sama. Ini akan menjadi sangat penting ketika kita membahas ng-repeat dan ng-include di bawah ini.

properti bersembunyi

Misalkan kita melakukan ini:

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

Rantai prototipe dikonsultasikan karena objek (anArray dan anObject) tidak ditemukan di childScope. Objek ditemukan di parentScope, dan nilai properti diperbarui pada objek asli. Tidak ada properti baru yang ditambahkan ke childScope; tidak ada objek baru yang dibuat. (Perhatikan bahwa dalam array dan fungsi JavaScript juga objek.)

ikuti rantai prototipe

Misalkan kita melakukan ini:

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

Rantai prototipe tidak dikonsultasikan, dan ruang lingkup anak mendapat dua properti objek baru yang menyembunyikan / membayangi properti objek parentScope dengan nama yang sama.

lebih banyak properti bersembunyi

Takeaways:

  • Jika kita membaca childScope.propertyX, dan childScope memiliki propertyX, maka rantai prototipe tidak dikonsultasikan.
  • Jika kita menetapkan childScope.propertyX, rantai prototipe tidak dikonsultasikan.

Satu skenario terakhir:

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

Kami menghapus properti childScope terlebih dahulu, kemudian ketika kami mencoba mengakses properti lagi, rantai prototipe dikonsultasikan.

setelah menghapus properti anak


Warisan Lingkup Sudut

Peserta:

  • Berikut ini membuat lingkup baru, dan mewarisi secara prototipe: ng-repeat, ng-include, ng-switch, ng-controller, direktif dengan scope: true, direktif dengan transclude: true.
  • Berikut ini menciptakan ruang lingkup baru yang tidak mewarisi secara prototipe: direktif dengan scope: { ... }. Ini menciptakan ruang lingkup "mengisolasi".

Catatan, secara default, arahan tidak membuat ruang lingkup baru - yaitu, defaultnya adalah scope: false.

ng-sertakan

Misalkan kita ada di controller kita:

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

Dan dalam HTML kami:

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

Setiap ng-include menghasilkan ruang lingkup anak baru, yang secara prototipe mewarisi dari ruang lingkup orang tua.

ng-sertakan cakupan anak

Mengetik (katakanlah, "77") ke dalam kotak teks input pertama menyebabkan lingkup anak untuk mendapatkan myPrimitiveproperti lingkup baru yang menyembunyikan / bayangan properti lingkup induk dengan nama yang sama. Ini mungkin bukan yang Anda inginkan / harapkan.

ng-sertakan dengan primitif

Mengetik (katakanlah, "99") ke kotak teks input kedua tidak menghasilkan properti anak baru. Karena tpl2.html mengikat model ke properti objek, pewarisan prototipal muncul ketika ngModel mencari objek myObject - ia menemukannya dalam lingkup induk.

ng-sertakan dengan objek

Kita dapat menulis ulang templat pertama yang menggunakan $ parent, jika kita tidak ingin mengubah model kita dari yang primitif ke objek:

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

Mengetik (misalnya, "22") ke dalam kotak teks input ini tidak menghasilkan properti anak baru. Model sekarang terikat ke properti dari lingkup induk (karena $ parent adalah properti lingkup anak yang mereferensikan lingkup induk).

ng-sertakan dengan $ parent

Untuk semua lingkup (prototipal atau tidak), Angular selalu melacak hubungan orangtua-anak (yaitu, hierarki), melalui properti lingkup $ parent, $$ childHead, dan $$ childTail. Saya biasanya tidak menunjukkan properti lingkup ini dalam diagram.

Untuk skenario di mana elemen bentuk tidak terlibat, solusi lain adalah mendefinisikan fungsi pada lingkup induk untuk memodifikasi primitif. Kemudian pastikan anak selalu memanggil fungsi ini, yang akan tersedia untuk ruang lingkup anak karena warisan prototypal. Misalnya,

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

Berikut adalah contoh biola yang menggunakan pendekatan "fungsi orangtua" ini. (Biola ditulis sebagai bagian dari jawaban ini: https://stackoverflow.com/a/14104318/215945 .)

Lihat juga https://stackoverflow.com/a/13782671/215945 dan https://github.com/angular/angular.js/issues/1267 .

ng-switch

ng-switch lingkup warisan berfungsi seperti ng-include. Jadi, jika Anda memerlukan data 2 arah yang mengikat ke primitif di lingkup induk, gunakan $ parent, atau ubah model menjadi objek dan kemudian ikat ke properti objek itu. Ini akan menghindari menyembunyikan lingkup anak / membayangi properti lingkup orangtua.

Lihat juga AngularJS, ikat ruang lingkup switch-case?

ng-ulangi

Ng-repeat bekerja sedikit berbeda. Misalkan kita ada di controller kita:

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

Dan dalam HTML kami:

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

Untuk setiap item / iterasi, ng-repeat menciptakan ruang lingkup baru, yang secara purwarupa mewarisi dari ruang lingkup induk, tetapi juga menetapkan nilai item ke properti baru pada ruang lingkup anak baru . (Nama properti baru adalah nama variabel loop.) Inilah kode sumber Angular untuk ng-repeat sebenarnya:

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

Jika item adalah primitif (seperti dalam myArrayOfPrimitive), pada dasarnya salinan nilai diberikan ke properti lingkup anak baru. Mengubah nilai properti lingkup anak (yaitu, menggunakan ng-model, maka lingkup anak num) tidak mengubah array referensi lingkup induk. Jadi pada ng-repeat pertama di atas, setiap ruang lingkup anak mendapatkan numproperti yang independen dari array myArrayOfPrimitive:

ng-ulangi dengan primitif

Ng-repeat ini tidak akan berfungsi (seperti yang Anda inginkan / harapkan). Mengetik ke dalam kotak teks mengubah nilai dalam kotak abu-abu, yang hanya terlihat dalam cakupan anak. Apa yang kami inginkan adalah agar input memengaruhi array myArrayOfPrimitive, bukan properti primitif lingkup anak. Untuk mencapai ini, kita perlu mengubah model menjadi array objek.

Jadi, jika item adalah objek, referensi ke objek asli (bukan salinan) ditugaskan ke properti lingkup anak baru. Mengubah nilai properti lingkup anak (yaitu, menggunakan ng model, maka obj.num) tidak mengubah objek lingkup referensi induk. Jadi pada ng-repeat kedua di atas, kita memiliki:

ng-ulangi dengan benda

(Saya mewarnai satu garis abu-abu hanya supaya jelas ke mana ia pergi.)

Ini berfungsi seperti yang diharapkan. Mengetik ke dalam kotak teks mengubah nilai dalam kotak abu-abu, yang terlihat oleh cakupan anak dan orangtua.

Lihat juga Kesulitan dengan ng-model, ng-repeat, dan input dan https://stackoverflow.com/a/13782671/215945

ng-controller

Pengendali yang bersarang menggunakan ng-controller menghasilkan warisan prototypal normal, seperti ng-include dan ng-switch, sehingga teknik yang sama berlaku. Namun, "ini dianggap sebagai bentuk buruk bagi dua pengendali untuk berbagi informasi melalui $ lingkup warisan" - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ Layanan harus digunakan untuk berbagi data antara pengendali sebagai gantinya.

(Jika Anda benar-benar ingin berbagi data melalui warisan lingkup pengontrol, tidak ada yang perlu Anda lakukan. Ruang lingkup anak akan memiliki akses ke semua properti lingkup induk. Lihat juga Urutan beban pengontrol berbeda ketika memuat atau menavigasi )

arahan

  1. default ( scope: false) - arahan tidak membuat ruang lingkup baru, jadi tidak ada warisan di sini. Ini mudah, tetapi juga berbahaya karena, misalnya, arahan mungkin berpikir itu menciptakan properti baru pada ruang lingkup, padahal sebenarnya itu clobber properti yang sudah ada. Ini bukan pilihan yang baik untuk arahan penulisan yang dimaksudkan sebagai komponen yang dapat digunakan kembali.
  2. scope: true- Arahan menciptakan ruang lingkup anak baru yang secara prototipe mewarisi dari ruang lingkup orang tua. Jika lebih dari satu arahan (pada elemen DOM yang sama) meminta ruang lingkup baru, hanya satu ruang lingkup anak baru dibuat. Karena kita memiliki pewarisan prototipal "normal", ini seperti ng-include dan ng-switch, jadi berhati-hatilah dengan pengikatan data 2 arah ke primitif cakupan orangtua, dan menyembunyikan anak / membayangi properti lingkup orangtua.
  3. scope: { ... }- Arahan menciptakan ruang lingkup isolat / terisolasi baru. Ini tidak mewarisi secara prototipe. Ini biasanya pilihan terbaik Anda saat membuat komponen yang dapat digunakan kembali, karena arahan tidak dapat secara tidak sengaja membaca atau memodifikasi lingkup induk. Namun, arahan tersebut sering membutuhkan akses ke beberapa properti lingkup induk. Hash objek digunakan untuk mengatur pengikatan dua arah (menggunakan '=') atau pengikatan satu arah (menggunakan '@') antara lingkup induk dan cakupan isolat. Ada juga '&' untuk mengikat ekspresi lingkup induk. Jadi, ini semua membuat properti lingkup lokal yang diturunkan dari lingkup induk. Perhatikan bahwa atribut digunakan untuk membantu mengatur pengikatan - Anda tidak bisa hanya merujuk nama properti lingkup induk dalam hash objek, Anda harus menggunakan atribut. Misalnya, ini tidak akan berfungsi jika Anda ingin mengikat ke properti indukparentPropdalam ruang lingkup yang terisolasi: <div my-directive>dan scope: { localProp: '@parentProp' }. Atribut harus digunakan untuk menentukan setiap properti induk yang ingin dirujuk oleh direktif ke: <div my-directive the-Parent-Prop=parentProp>dan scope: { localProp: '@theParentProp' }.
    Mengisolasi __proto__Objek referensi lingkup . Isolate scope's $ parent mereferensikan lingkup parent, jadi meskipun ia diisolasi dan tidak mewarisi prototipically dari lingkup parent, itu masih lingkup anak.
    Untuk gambar di bawah ini yang kami miliki
    <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">dan
    scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
    Juga, asumsikan arahan melakukan ini dalam fungsi tautannya: scope.someIsolateProp = "I'm isolated"
    ruang lingkup yang terisolasi
    Untuk informasi lebih lanjut tentang cakupan isolat lihat http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true- arahan menciptakan ruang lingkup anak baru "ditransklusikan", yang secara purwarupa mewarisi dari ruang lingkup orang tua. Ruang lingkup ditransklusikan dan terisolasi (jika ada) adalah saudara kandung - properti $ parent dari setiap lingkup referensi lingkup induk yang sama. Ketika ada ruang lingkup yang ditransklusikan dan terisolasi, mengisolasi properti lingkup $$ nextSibling akan merujuk pada ruang lingkup yang ditransklusikan. Saya tidak mengetahui adanya nuansa dengan ruang lingkup yang ditransklusikan.
    Untuk gambar di bawah ini, asumsikan arahan yang sama seperti di atas dengan tambahan ini:transclude: true
    ruang lingkup yang ditransklusikan

Biola ini memiliki showScope()fungsi yang dapat digunakan untuk memeriksa ruang lingkup isolat dan transklusi. Lihat instruksi dalam komentar di biola.


Ringkasan

Ada empat jenis cakupan:

  1. pewarisan lingkup prototypal normal - ng-include, ng-switch, ng-controller, direktif dengan scope: true
  2. pewarisan lingkup prototypal normal dengan salinan / penugasan - ng-repeat. Setiap iterasi dari ng-repeat menciptakan lingkup anak baru, dan bahwa lingkup anak baru selalu mendapatkan properti baru.
  3. mengisolasi ruang lingkup - direktif dengan scope: {...}. Yang ini bukan prototipe, tetapi '=', '@', dan '&' menyediakan mekanisme untuk mengakses properti lingkup induk, melalui atribut.
  4. lingkup yang ditransklusikan - direktif dengan transclude: true. Yang ini juga merupakan warisan normal lingkup prototipe, tetapi juga saudara kandung dari setiap lingkup isolasi.

Untuk semua lingkup (prototipal atau tidak), Angular selalu melacak hubungan orangtua-anak (yaitu, hierarki), melalui properti $ parent dan $$ childHead dan $$ childTail.

Diagram dihasilkan dengan File "* .dot", yang ada di github . " Belajar JavaScript dengan Object Graphs " dari Tim Caswell adalah inspirasi untuk menggunakan GraphViz untuk diagram.

Mark Rajcok
sumber
48
Artikel yang luar biasa, terlalu lama untuk jawaban SO, tapi tetap sangat berguna. Harap taruh di blog Anda sebelum editor memotongnya menjadi ukuran.
iwein
43
Saya menaruh salinan di wiki AngularJS .
Mark Rajcok
3
Koreksi: "Mengisolasi __proto__Objek referensi lingkup ." seharusnya menjadi "Mengisolasi __proto__referensi lingkup objek Lingkup." Jadi, dalam dua gambar terakhir, kotak oranye "Objek" seharusnya menjadi kotak "Cakupan".
Mark Rajcok
15
Asnwer ini harus dimasukkan dalam panduan angularjs. Ini jauh lebih didatik ...
Marcelo De Zen
2
Wiki membuat saya bingung, pertama berbunyi: "Rantai prototipe dikonsultasikan karena objek tidak ditemukan di childScope." dan kemudian terbaca: "Jika kita menetapkan childScope.propertyX, rantai prototipe tidak dikonsultasikan.". Yang kedua menyiratkan suatu kondisi sedangkan yang pertama tidak.
Stephane
140

Saya sama sekali tidak ingin bersaing dengan jawaban Mark, tetapi hanya ingin menyoroti bagian yang akhirnya membuat semuanya klik sebagai seseorang yang baru dalam warisan Javascript dan rantai prototipe-nya .

Hanya properti yang membaca pencarian rantai prototipe, bukan tulisan. Jadi saat Anda mengatur

myObject.prop = '123';

Itu tidak mencari rantai, tetapi ketika Anda mengatur

myObject.myThing.prop = '123';

ada pembacaan halus yang terjadi dalam operasi penulisan yang mencoba untuk mencari myThing sebelum menulis ke prop. Jadi itu sebabnya menulis ke object.properties dari anak didapat di objek orang tua.

Scott Driscoll
sumber
12
Walaupun ini adalah konsep yang sangat sederhana, mungkin tidak terlalu jelas karena, saya percaya, banyak orang melewatkannya. Baik.
moljac024
3
Komentar yang sangat baik. Saya ambil, resolusi properti non objek tidak melibatkan membaca sedangkan resolusi properti objek tidak.
Stephane
1
Mengapa? Apa motivasi untuk menulis properti tidak naik rantai prototipe? Sepertinya gila ...
Jonathan.
1
Akan lebih bagus jika Anda menambahkan contoh sederhana yang nyata.
tylik
2
Perhatikan bahwa itu tidak mencari rantai prototipe untuk setter . Jika tidak ada yang ditemukan, itu menciptakan properti pada penerima.
Bergi
21

Saya ingin menambahkan contoh pewarisan prototipikal dengan javascript ke jawaban @Scott Driscoll. Kami akan menggunakan pola pewarisan klasik dengan Object.create () yang merupakan bagian dari spesifikasi EcmaScript 5.

Pertama kita membuat fungsi objek "Induk"

function Parent(){

}

Kemudian tambahkan prototipe ke fungsi objek "Induk"

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

Buat fungsi objek "Anak"

function Child(){

}

Tetapkan prototipe anak (Buat prototipe anak diturunkan dari prototipe induk)

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

Tetapkan konstruktor prototipe "Anak" yang tepat

Child.prototype.constructor = Child;

Tambahkan metode "changeProps" ke prototipe anak, yang akan menulis ulang nilai properti "primitif" di objek Anak dan mengubah nilai "object.one" baik di objek Anak dan Orang Tua

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

Memulai objek Induk (ayah) dan Anak (putra).

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

Panggil Anak (anak) metode changeProps

son.changeProps();

Periksa hasilnya.

Properti primitif induk tidak berubah

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

Properti primitif anak berubah (ditulis ulang)

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

Properti object.one Induk dan Anak berubah

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

Contoh kerja di sini http://jsbin.com/xexurukiso/1/edit/

Info lebih lanjut tentang Object.create di sini https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create

tylik
sumber