在AngularJS中,子作用域通常會原型繼承於父作用域。這種情況的唯一例外是當一個指令設置了scope:{ ... } -- 這會創建一個孤立的作用域,該作用域不會進行原型繼承。這種設置通常用於創建可複用組件。在指令中,默認情況下直接使用父作用域,這意味着,你在指令中作的任何改動都會同時改變父作用域。如果你設置scope:true(而不是scope:{ ... }),那麼該指令會進行原型繼承。
一般來說,作用域繼承是很簡單的,通常你甚至不需要知道它正在運作...直到你試圖從子作用域中對父級作用域的基本類型數據(比如,數字,字符串,布爾值)進行數據雙向綁定(即表單元素,ng-model指令)。這種做法通常不會符合我們的預期。這是因爲子作用域會創建自身的屬性,從而隱藏/遮蔽了父級作用域的同名屬性。這種特性是JavaScript原型鏈運作原理,而不是AngularJS本身實現造成的。AngularJS初學者通常沒有意識到,ng-repeat、ng-switch、ng-view和ng-include所有這些指令都會創建一個子作用域,所以當執行這些指令時便會出現問題。
如果我們遵循記得在ng-model指令中使用'.'的“最佳實踐”-- 值得花3分鐘看看,我們能輕易地迴避這個問題。Misko用ng-switch闡述了基本類型數據綁定的問題。
在你的ng-model指令中使用“.”能保證原型繼承鏈起作用。所以,我們應該使用:
1
2
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" ><input type= "text" ng-model= "someObj.prop1" > </font></font></font> |
而不是:
1
2
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" ><input type= "text" ng-model= "prop1" > </font></font></font> |
如果你真的想或者真的需要用到基本類型數據,這裏有兩種變通方案:
- 在子作用域中使用$parent.parentScopeProperty,防止子作用域創建自身的屬性
- 在父作用域中定義一個函數,並在子作用域中調用並傳遞基本類型數據給父作用域(並不是總能夠做到)
JavaScript 原型繼承
首先,我們要對JavaScript的原型繼承有個良好的認知,這很重要,如果你有服務端編程的背景,更是如此。所以讓我們先回顧一下原型繼承的原理。
假設父級作用域有以下屬性aString、aNumber、anArray、anObject 和 aFunction。如果子作用域原型繼承於父作用域,
當我們試圖從子作用域中訪問父作用域上定義的屬性,JavaScript會先在子作用域上查詢該屬性,如果沒有找到該屬性,再訪問父級作用域並查詢該屬性。(如果在父作用域中依舊沒有找到這個屬性,JavaScript會繼續順着原型鏈往上查找... 直到根作用域)。因此,以下均爲true:
1
2
3
4
5
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" >childScope.aString === 'parent string' childScope.anArray[1] === 20 childScope.anObject.property1 === 'parent prop1' childScope.aFunction() === 'parent output' </font></font></font> |
假設我們接下來進行以下操作:
1
2
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" >childScope.aString = 'child string' ; </font></font></font> |
原型鏈並未被查詢,而子作用域中新增了一個 aString 屬性。這個新的屬性隱藏/遮蔽了父作用域的同名屬性。當我們下面討論到ng-repeat指令和ng-include指令時,這特性會變得非常重要。
接下來假設我們執行:
1
2
3
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" >childScope.anArray[1] = '22' childScope.anObject.property1 = 'child prop1' </font></font></font> |
因爲在子作用域中沒有找到 anArray 和 anObject 對象,所以原型鏈被查詢了。在父作用域中被找到這兩個對象,所以屬性值被更新到了原始的對象上。子作用域上沒有添加新的屬性,也沒有創建新的對象。(注意,在JavaScript中數組和函數都是對象)。
接着,假設我們這麼做:
1
2
|
childScope.anArray = [100, 555] childScope.anObject = { name: 'Mark' , country: 'USA' } |
原形鏈並未被訪問,並且子作用域獲得了兩個新的對象屬性,這兩個屬性也會遮蔽父作用域上的同名屬性。
順便提一下:
- 如果我們讀取childScope.propertyX,並且子作用域有 propertyX 屬性,那麼原型鏈將不會被訪問。
- 如果我們設置childScope.propertyX,那麼原型鏈也不會被訪問。
最後一種情況:
1
2
3
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" > delete childScope.anArray childScope.anArray[1] === 22 // true </font></font></font> |
我們先刪除子作用域的屬性,然後當我們試圖再次訪問該屬性,此時原型鏈會被訪問。
Angular 作用域的繼承
兩種不同的情況:
- 以下指令會創建新的作用域,而且原型繼承父級作用域:ng-repeat、 ng-include、ng-switch、ng-view、ng-controller、帶scope: true的指令、設置了transclude:true的指令
- 以下指令會創建新的作用域,但不會原型繼承:設置了scope: { ... }的指令。這指令創建的是孤立的作用域。
注意,通常情況下,即默認情況下scope:false,指令不會創建新的作用域。
ng-include
假設我們的控制器中有:
1
2
3
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" >$scope.myPrimitive = 50; $scope.myObject = {aNumber: 11}; </font></font></font> |
而且在我們的HTML中:
1
2
3
4
5
6
7
8
9
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" ><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> </font></font></font> |
每一個ng-include指令都生成一個新的子作用域,這些子作用域都原型繼承於其父作用域。
在第一個輸入框中輸入77,子作用域將會得到一個新的myPrimitive屬性,該屬性會遮蔽了父作用域的同名屬性。這可能不是你想要的。
在第二個輸入框中輸入99不會新建一個子作用域屬性。因爲tpl2.html綁定的數據是一個對象屬性。當ngModel指令查詢該對象,原型繼承起到了作用,最終在父作用域中查找到該對象。
如果我們不想將我們的數據從基本類型改爲對象,我們可以用$parent變量重寫第一個模版:
1
2
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" ><input ng-model= "$parent.myPrimitive" > </font></font></font> |
在該輸入框中輸入22不會生成一個新的子作用域屬性。現在,這個模型是綁定在父級作用域的一個屬性上(因爲$parent是子作用域上指向父作用域的屬性值)。
對於所有的作用域(無論是否原型繼承),Angular總會通過$parent、$$childHead`和`$$childTail記錄下父-子關係(即一種層級關係)。以上的圖表並沒有展示這些屬性值。
對於一些不涉及表單元素的情況,另一種解決方法是在父級作用域中定義一個函數用來修改基本類型數值。然後保證其子作用域都調用該函數,由於原型繼承,其子作用域都能夠訪問的該函數。比如:
1
2
3
4
5
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" > // in the parent scope $scope.setMyPrimitive = function (value) { $scope.myPrimitive = value; } </font></font></font> |
ng-switch
ng-switch指令的作用域繼承的運行原理就類似於ng-include指令。所以如果你需要對父級作用域中的一個基本類型值進行雙向版定,你可以使用$parent,或者將數據模型改成對象的形式,然後綁定該對象上的屬性。這可以避免子作用域遮蔽到了父作用域上的屬性。
更多閱讀:AngularJS, bind scope of a switch-case?
ng-repeat
ng-repeat指令的運行原理有點不一樣。假設我們控制器中有:
1
2
3
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" >$scope.myArrayOfPrimitives = [ 11, 22 ]; $scope.myArrayOfObjects = [{num: 101}, {num: 202}]; </font></font></font> |
而且我們的HMTL中:
01
02
03
04
05
06
07
08
09
10
11
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" ><ul> <li ng-repeat= "num in myArrayOfPrimitives" > <input ng-model= "num" ></input> </li> </ul> <ul> <li ng-repeat= "obj in myArrayOfObjects" > <input ng-model= "obj.num" ></input> </li> </ul> </font></font></font> |
每次迭代,ng-repeat指令都會創建一個新的作用域,該作用會原型繼承於其父級作用域,但是同時該指令會給這個新作用域的一個新的屬性分配本次迭代對應數值。(這個屬性的名稱就是循環變量的名字)。以下就Angular源碼中ng-repeat具體實現:
1
2
3
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" >childScope = scope.$ new (); // child scope prototypically inherits from parent scope ... childScope[valueIdent] = value; // creates a new childScope property </font></font></font> |