起因是在StackOverflow上看到了這個帖子:Directive isolate scope with ng-repeat scope in AngularJS - Stack Overflow
題主原本想問的是一個AngularJS中對指令使用ng-repeat情況下的孤立作用域問題,最佳答主指出了題主理解的根本性錯誤商佑,并用了最簡單的demo來復(fù)現(xiàn)這個問題贱迟。
有問題的Demo:
html代碼:
<div ng-app="my-app" ng-controller="MainController">
<div>
Selected: {{selected.first}} {{selected.last}}
</div>
<div>
<ul>
<li ng-repeat="name in names"
ng-class="{ active: $index == selected }"
ng-click="selected = $index">
{{$index}}: {{name.first}} {{name.last}}
</li>
</ul>
</div>
</div>
js代碼:
var module = angular.module('my-app', []);
function MainController($scope) {
$scope.names = [
{first: "John", last: "Smith"},
{first: "Jane", last: "Smith"}
];
$scope.selected = undefined;
}
點擊行就會發(fā)現(xiàn)停局,點擊并不能觸發(fā)"Selected:"后面出現(xiàn)被選中行的文字现柠。
正常工作的demo:
html代碼:
<div ng-app="my-app" ng-controller="MainController">
<div>
Selected: {{selected.first}} {{selected.last}}
</div>
<div>
<ul>
<li ng-repeat="name in names"
ng-class="{ active: $index == state.selected }"
ng-click="state.selected = $index">
{{$index}}: {{name.first}} {{name.last}}
</li>
</ul>
</div>
</div>
js代碼:
var module = angular.module('my-app', []);
function MainController($scope) {
$scope.names = [
{first: "John", last: "Smith"},
{first: "Jane", last: "Smith"}
];
$scope.state = { selected: undefined };
}
對比兩個Demo磕潮,就會發(fā)現(xiàn)效床,問題的關(guān)鍵在于正確代碼中使用state對象來封裝了這個selected屬性候引。那么,為什么使用了state對象封裝就能解決這個問題呢辱揭?
最佳答主解釋說离唐,selected是一個prmitive類型(姑且翻譯為基本數(shù)據(jù)類型,包括null问窃、undefined、boolean完沪、number域庇、string和symbol(es6),參考這里)覆积,這意味著听皿,當子類在寫這個屬性時會覆蓋父類的同名屬性。并強調(diào)了一條AngularJS中的黃金法則:所有的模型值必須包含一個"."宽档。
這讓我想到在讀《AngularJS權(quán)威教程》P8時也看到了一句類似的話:由于JavaScript自身的特點尉姨,以及它在傳遞值和引用時的不同處理方式,通常認為吗冤,在視圖中通過對象的屬性而非對象本身來進行引用綁定又厉,是Angular中的最佳實踐九府。
這么解釋就可以了嗎?非也非也覆致,對前端小白來說侄旬,看到這些話時仍然是丈二和尚摸不著頭腦。
如果你在Chrome中安裝了Batarang插件煌妈,在點擊任一行前查看每一個li行的Model儡羔,會發(fā)現(xiàn)其Model中并沒有selected屬性,而點擊之后璧诵,其Model中就多了這個屬性汰蜘,并且與父Model中的selected值不一致,父Model中它的值仍為undefined之宿。這個首先能說明鉴扫, 每一個li行在被點擊時,確實創(chuàng)建了自己的selected屬性澈缺,并且覆蓋了父類的同名屬性坪创。
最佳答主在答案中說到了一個關(guān)鍵的地方:the parameters passed into ngRepeat for use on your directive's attributes do use a prototypically-inherited scope。這里面提到了原型繼承這個東西姐赡。首先莱预,要搞明白Javascript中的原型(prototype)是怎么回事,這個可以參考Javascript繼承機制的設(shè)計思想 - 阮一峰的網(wǎng)絡(luò)日志。
簡單總結(jié)一下:Javascript中一個"類"所有實例對象需要共享的屬性和方法项滑,都放在prototype對象里面.
這個StackOverflow問題依沮,其實歸根到底還是一個js基礎(chǔ)問題,運行一下下面的代碼枪狂,你就知道是怎么回事了:
var Parent = function(){
} ;
Parent.prototype.name = 'parent' ;
Parent.prototype.obj = {name : 'objparent'} ;
var Child = function(){
};
Child.prototype = new Parent() ;
var parent = new Parent() ;
var child = new Child() ;
console.log(parent.name) ; //parent
console.log(child.name) ; //parent
child.name = 'child' ;
console.log(parent.name) ; //parent
console.log(child.name) ; //child
console.log(parent.obj.name) ; //objparent
console.log(child.obj.name) ; //objparent
child.obj.name = 'objchild' ;
console.log(parent.obj.name) ; //objchild
console.log(child.obj.name) ; //objchild
console.log(parent.obj) ; //Object name:"objchild"
console.log(child.obj) ; //Object name:"objchild"
child.obj = {name : 'test'} ;
console.log(parent.obj) ; //Object name:"objchild"
console.log(child.obj) ; //Object name:"test"
在Parent原型中危喉,name是string類型,屬于Javascript中的Primitive類型(基本數(shù)據(jù)類型)州疾,在子類中寫name時辜限,不會修改父類的name值;
而obj是Javascript中的Object類型严蓖,不過直接在子類中修改obj對象薄嫡,其表現(xiàn)與上面的name一樣
只有修改obc.name時,才能保證父類與子類的obc.name保持一致颗胡。
這說明毫深,子類無論是寫與父類同名的Primitive類型屬性,還是寫Object屬性毒姨,都是在子類中創(chuàng)建了新的屬性覆蓋了父類的同名屬性哑蔫。只有寫對象(Object)的屬性(即使用點表達式),才能實際上修改到父類中同名對象的屬性值。
正因為如此闸迷,在Angular設(shè)置Model時嵌纲,在父級作用域中宜使用.(點表達式)來處理基本數(shù)據(jù)模型,本質(zhì)就是對像的原型繼承會保持同一個引用。這樣就可以避免上面的問題稿黍。
其實從根本上來說疹瘦,是因為在子類進行屬性寫操作時,只有使用點表達式才能通過原型鏈訪問父類的對象的屬性值巡球。
JavaScript 秘密花園也提到了這一點:
當查找一個對象的屬性時言沐,JavaScript 會向上遍歷原型鏈,直到找到給定名稱的屬性為止酣栈。到查找到達原型鏈的頂部 - 也就是 Object.prototype - 但是仍然沒有找到指定的屬性险胰,就會返回 undefined
那么,為什么不使用點表達式矿筝,就是覆蓋父類的同名屬性呢起便?從Javascript的設(shè)計上來說,這個是給予了我們方便:在使用第三方JS類庫的時候窖维,往往有時候他們定義的原型方法是不能滿足我們的需要榆综,但是又離不開這個類庫,直接對屬性或者方法進行寫操作就可以重寫他們的原型中的一個或者多個屬性或function铸史。
注:以上說的點表達式不是絕對的鼻疮,比如訪問數(shù)組元素就是不用點表達式的。
寫這篇文章時參考了下面的文章琳轿,我列出了一些關(guān)鍵的要點判沟,需要的可以自行參考:
AngularJS子級作用域問題(ngInclude;ngView;ngSwitch;ngRepeat) - 簡書
AngularJS的繼承是通過javascript的原型繼承方式實現(xiàn)的,進行原型繼承即意味著父作用域在子作用域的原型鏈上崭篡。因為原型鏈的檢索只會在屬性檢索的時候觸發(fā)挪哄,不會在改變屬性值的時候觸發(fā)。所以我們需要把原始類型轉(zhuǎn)換成對象琉闪,把值綁定在對象的屬性上迹炼。
Feenan's Blog-[譯]深入理解ng里的scope-程序人生
scope里的原型繼承比較容易理解,一般情況下都不需要你去了解它的實現(xiàn),但是當你在子作用域里綁定父作用域里的基本數(shù)據(jù)類型(比如,整型,字符串,布爾型)的時候,這種情況下就會出現(xiàn)問題,你會發(fā)現(xiàn)它并沒有像你指望的那樣去運行,當修改子作用域里的基本數(shù)據(jù)類型時,并不會修改父作用域,而是在子作用域里創(chuàng)建一個新的屬性,這并不是ng干的,這只是js里原型繼承所導(dǎo)致的,關(guān)于這個問題,可以看看這個例子
-
AngularJS中scope基于原型鏈的繼承 // 進擊的馬斯特
- 讀子類的屬性時,子類有這個屬性(hasOwnProperty)的時候則讀子類自己的塘偎,子類沒有的時候讀父類的疗涉,不管子類有沒有這個屬性,在子類上都不會有新屬性被創(chuàng)建吟秩。
- 寫子類的屬性時,如果子類有這個屬性(hasOwnProperty)則寫子類的绽淘,子類沒有的話就會在子類上新建一個同名的新屬性涵防,而父類繼承過來的屬性被隱藏。