摘要
在AngularJS中,子作用域通常會(huì)原型繼承于其父作用域。有一個(gè)例外是當(dāng)指令使用scope: { ... }來定義--這創(chuàng)建了一個(gè)沒有原型繼承的“獨(dú)立“作用域,這會(huì)在創(chuàng)建“可重復(fù)使用的組件“的指令時(shí)經(jīng)常使用。如果你設(shè)置了scope:true(而不是scope: { ... }),這個(gè)指令會(huì)使用原型繼承。
通常情況下作用域繼承非常直白,你甚至不需要知道它正在發(fā)生。。。直到在一個(gè)定義在父作用域上原始類型(例如:number, string, boolean)在子作用域中使用了雙向數(shù)據(jù)綁定(即:表單元素,ng-model)。這并不會(huì)像大多數(shù)人期望的那樣工作,而是子作用域得到了它自己的屬性,從而覆蓋了父作用域上的同名屬性。這不是AngularJS做的事情-這是JavaScript的原型繼承起作用了。新入門的AngularJS開發(fā)者通常情況下不會(huì)意識到ng-repeat、 ng-switch、 ng-view 和 ng-include都創(chuàng)建了新的子作用域,所以當(dāng)使用這些指令的時(shí)候,經(jīng)常會(huì)有這種問題發(fā)生。
關(guān)于原始類型的這個(gè)問題通過下面的這個(gè)最佳建議很容易避免:在你的模型中始終使用'.'
在模型中使用'.' 會(huì)確保原型繼承始終發(fā)生。所以,使用代碼<input type="text" ng-model="someObj.prop1">而不是<input type="text" ng-model="prop1">。
如果你必須要使用原始類型,有以下兩種解決方法:
- 在子作用域上使用$parent.parentScopeProperty。這會(huì)阻止子作用域創(chuàng)建自己的屬性。
- 在父作用域上定義一個(gè)函數(shù),在子作用域上調(diào)用,通過該函數(shù)傳遞父作用域上的原始值。
JavaScript原型繼承
首先對JavaScript原型繼承有一個(gè)深入的了解很有必要,尤其你具有服務(wù)器端開發(fā)的背景,并且對于傳統(tǒng)的繼承很熟悉。讓我們先來復(fù)習(xí)一下。
假設(shè)parentScope具有如下屬性, aString, aNumber, anArray, anObject, and aFunction。如果childScope 原型繼承于parentScope,如下:
(注意:為了節(jié)省空間,我把a(bǔ)nArray展示成一個(gè)藍(lán)色的有三個(gè)值的對象,而不是一個(gè)藍(lán)色的擁有三個(gè)分離的灰色的對象)
如果我們在childScope上獲取parentScope上定義的對象,JavaScript會(huì)首先在childscope上查找,沒有找到該屬性,查找其繼承的scope,找到這個(gè)屬性(如果在parentScope上沒有找到該屬性,會(huì)繼續(xù)查找原型鏈。。。直到到達(dá)root scope)。所以,以下全都為真:
childScope.aString === 'parent string'
childScope.anArray[1] === 20
childScope.anObject.property1 === 'parent prop1'
childScope.aFunction() === 'parent output'
假設(shè)我們有如下代碼:
childScope.aString = 'child string'
原型鏈并沒有被遍歷,一個(gè)新的屬性會(huì)被添加到childScope上。同時(shí),這個(gè)新屬性隱藏了和parentScope具有同樣名稱的屬性。這對我們下面討論ng-repeat 和 ng-include非常重要
假設(shè)我們又做了如下操作
childScope.anArray[1] = 22
childScope.anObject.property1 = 'child prop1'
原型鏈被訪問了,因?yàn)閷ο螅╝nArray和anObject)在childScope中沒有被找到。這兩個(gè)對象在parentScope中被找到了,屬性的值在原對象上被更新了。childScope上不會(huì)增加新的屬性,沒有新的對象被創(chuàng)建(注意:在JavaScript中數(shù)組和函數(shù)同樣是對象)
假設(shè)我們做如下操作:
childScope.anArray = [100, 555]
childScope.anObject = { name: 'Mark', country: 'USA' }
原型鏈不會(huì)被訪問,childScope會(huì)創(chuàng)建兩個(gè)新的對象屬性,隱藏了和parentScope具有相同名稱的屬性。
重要結(jié)論:
- 如果我們讀取childScope的某個(gè)屬性childScope.propertyX,
并且childScope具有屬性propertyX,那么原型鏈不會(huì)被訪問。 - 如果我們設(shè)置childScope的某個(gè)屬性propertyX,那么原型鏈不會(huì)被訪問。
最后一個(gè)場景:
delete childScope.anArray
childScope.anArray[1] === 22 // true
首先我們刪除了childScope的屬性anArray,然后我們嘗試再次去獲得該屬性,原型鏈被訪問了。
這個(gè)jsfiddle中你可以看到JavaScript原型繼承的例子和結(jié)果(打開你的瀏覽器的控制臺查看輸出)
Angular Scope 繼承
如下的指令創(chuàng)建了新的scope,并且基于原型繼承:ng-repeat, ng-include, ng-switch, ng-view, ng-controller,使用scope:true的指令,使用transclude: true的指令。
如下的指令創(chuàng)建了新的scope,并且沒有基于原型繼承:使用scope: { ... }的指令。這創(chuàng)建了“孤立”的scope
(注意: 默認(rèn)情況下,指令不創(chuàng)建新的scope,默認(rèn)值是scope: false)
ng-include
假設(shè)我們的controller中的代碼如下:
$scope.myPrimitive = 50;
$scope.myObject = {aNumber: 11};
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>
每一個(gè)ng-include生成了一個(gè)新的基于其父作用域原型繼承的子作用域。
修改第一個(gè)textbox中的值為‘77’會(huì)導(dǎo)致子作用域創(chuàng)建一個(gè)新的myPrimitive 屬性,并且隱藏了父作用域的同名屬性。這可能不是你所希望的。
修改第二個(gè)textbox中的值為‘99’不會(huì)導(dǎo)致創(chuàng)建一個(gè)新的子屬性。因?yàn)閠pl2.html 綁定了一個(gè)對象的屬性,當(dāng)ngModel查找對象myObject時(shí)原型繼承起作用了,最終在parentScope中找到了該屬性。
如果不想將model從原始類型改為對象類型的話,我們可以使用$parent來重寫第一個(gè)模板。
<input ng-model="$parent.myPrimitive">
這次修改第一個(gè)textbox的值不會(huì)導(dǎo)致生成一個(gè)新的子屬性。模型現(xiàn)在綁定了parentScope中的屬性(因?yàn)?parent是子作用域指向父作用域的一個(gè)引用)。
對于所有的作用域(不管是否是基于原型繼承),Angular會(huì)通過作用域上的屬性$parent, $$childHead 和 $$\childTail 始終跟蹤其父-子關(guān)系(即層次結(jié)構(gòu))。在圖中我并沒有展示出這些屬性。
對于不涉及表單元素的情況,另一個(gè)解決方案是在父作用域中定義一個(gè)函數(shù)來修改原始數(shù)據(jù)類型。然后保證子作用域總是調(diào)用這個(gè)函數(shù),由于原型繼承子作用域能夠訪問到該函數(shù)。例如,
// in the parent scope
$scope.setMyPrimitive = function(value) {
$scope.myPrimitive = value;
}
這是一個(gè)使用了“parent function”的簡單的jsfiddle(部分來自于Stack OverFlow)
還可以參考 http://stackoverflow.com/a/13782671/215945 和
https://github.com/angular/angular.js/issues/1267.
ng-switch
ng-switch scope 的繼承和ng-include類似。因此如果你需要雙向數(shù)據(jù)綁定到父作用域中的一個(gè)原始數(shù)據(jù)類型上,使用$parent或者將model改為對象的某個(gè)屬性。這會(huì)避免子作用域隱藏了父作用域的屬性。
可以查看Stack Overflow
ng-repeat
ng-repeat 和以上指令有點(diǎn)差別。假設(shè)我們的controller如下:
$scope.myArrayOfPrimitives = [ 11, 22 ];
$scope.myArrayOfObjects = [{num: 101}, {num: 202}]
HTML 如下:
<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>
對于每次 item的iteration,ng-repeate創(chuàng)建了一個(gè)從父作用域原型繼承的新的作用域,但是它也將item的值分配給新的子作用域上的一個(gè)新的屬性(新的屬性的名稱是循環(huán)變量的名稱)。如下是ng-repeate的源代碼。
childScope = scope.$new(); // child scope prototypically inherits from parent scope ...
childScope[valueIdent] = value; // creates a new childScope property
如果item是一個(gè)原始類型(例如上面的myArrayOfPrimitives),本質(zhì)上該值的一個(gè)拷貝被分配給新的子scope。改變了子scope的屬性值(即使用ng-model、也就是子scope屬性num)并沒有改變父scope引用的數(shù)組。所以,在上面第一個(gè)ng-repeate,每一個(gè)子scope會(huì)得到一個(gè)獨(dú)立于myArrayOfPrimitives 的num屬性。
因此這個(gè)ng-repeat不會(huì)像你希望的那樣工作。在Angular1.0.2(包含)以前,修改textbox的值會(huì)改變上圖中灰色框的值,并且只在child scope中可見。在Angular 1.0.3以上,修改textbox的值不會(huì)有任何影響(參考Artem在Stack Overflow的解釋)(此處說法有點(diǎn)不太準(zhǔn)確,在較新的Angular版本中,修改textbox的值會(huì)改變圖中灰色框中的值--譯者注)。我們所希望的是修改input的值能夠改變數(shù)組myArrayOfPrimitives,而不是子scope的一個(gè)原始類型的屬性。為了達(dá)到這個(gè)目的,我們需要將模型改為對象的數(shù)組(見第2個(gè)例子)。
因此,如果item是一個(gè)對象,原始對象的引用(非拷貝)會(huì)被分配成為新的子scope上的屬性。修改子scope的屬性值(例如,使用ng-model,obj.num)會(huì)修改父scope上的值。在上面的第二個(gè)ng-repeat中,我們有如下結(jié)論:
(注意圖中的灰線,能清楚的看到發(fā)生了什么)
按照預(yù)期工作了。修改textbox的值改變了灰色框中的值,同時(shí)對子作用域和父作用域都可見。
可以參考 Difficulty with ng-model, ng-repeat, and inputs 和ng-repeat and databinding
ng-view
和ng-include類似
ng-controller
和ng-include、ng-switch的原理一致,使用ng-controller的嵌套的控制器會(huì)引起正常的原型繼承。然而,“不建議在兩個(gè)控制器中通過$scope的繼承關(guān)系來共享信息“--http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/。在控制器中共享數(shù)據(jù)應(yīng)該使用服務(wù)。
(如果你必須通過控制器的scope的繼承關(guān)系共享數(shù)據(jù),你不需要做任何操作。子scope能夠取到父scope的所有屬性。參考Controller load order differs when loading or navigating
指令
- 默認(rèn)(
scope:false)-指令沒有創(chuàng)建任何新的作用域,因此不存在任何的原型繼承。這很簡單,但是同樣存在隱患,例如:一個(gè)指令可能以為它在作用域上創(chuàng)建了一個(gè)新的屬性,但實(shí)際上它修改了一個(gè)現(xiàn)有的屬性的值。這對于書寫可重復(fù)使用的組件來說并不是一個(gè)好的選擇。 -
scope: true-指令創(chuàng)建了一個(gè)從父作用域基于原型繼承的子作用域。如果在同一個(gè)DOM上有多個(gè)指令需要?jiǎng)?chuàng)建新的作用域,那么只有一個(gè)新的子作用域會(huì)被創(chuàng)建。既然有“正?!暗脑屠^承,和ng-include 、ng-switch類似,警惕在父作用域上的原始數(shù)據(jù)類型的雙向數(shù)據(jù)綁定,子作用域會(huì)覆蓋掉父作用域上的屬性。 -
scope: { ... }-指令創(chuàng)建了一個(gè)新的獨(dú)立作用域。并且沒有原型繼承。當(dāng)你創(chuàng)建可以復(fù)用的組件時(shí)這是一個(gè)好的選擇,因?yàn)橹噶畈荒軌蛑苯幼x取或修改父作用域。然而,通常這種指令需要讀取父作用域的某些屬性。該對象可以在父作用域和獨(dú)立作用域上使用“=“創(chuàng)建雙向數(shù)據(jù)綁定,使用“@“創(chuàng)建單向綁定(父作用域改變會(huì)影響子作用域,子作用域改變并不會(huì)影響父作用域--譯者注)。也可以使用“&“綁定父作用域上的表達(dá)式。所以,這些方法同樣給子作用域創(chuàng)建了從父作用域衍生的屬性。注意這些屬性被用來幫助設(shè)置綁定--在對象中你不能直接引用父作用域的屬性名稱,你需要使用一個(gè)HTML屬性。例如:如下,你想要在獨(dú)立作用域上綁定父作用域的屬性parentProp將不會(huì)起作用:代碼<div my-directive>和scope: { localProp: '@parentProp' }。指令想要綁定的父屬性必須要有明確的HTML屬性名:代碼<div my-directive the-Parent-Prop=parentProp>和scope: { localProp: '@theParentProp' }。獨(dú)立作用域的proto 引用了一個(gè)Scope對象。獨(dú)立作用域的$parent引用了父作用域,盡管這是一個(gè)沒有原型繼承的獨(dú)立作用域,但他還是一個(gè)子作用域。
如下圖片中:我們有代碼<my-directive interpolated="{{parentProp1}}" twoway-binding="parentProp2">和
scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }。同樣假設(shè)在指令的link函數(shù)中有代碼scope.someIsolateProp = "I'm isolated"
圖11
最后注意:使用link函數(shù)中attrs.$observe('attr_name', function(value) { ... })來得到獨(dú)立作用域中使用‘@‘綁定的屬性的值。例如:在link函數(shù)中有代碼--attrs.$observe('interpolated', function(value) { ... })--value會(huì)被設(shè)置為11。(scope.interpolatedProp在link函數(shù)中沒有定義(該文章寫的時(shí)間較早,譯者通過測試Angular1.4.7發(fā)現(xiàn)在該版本中,這個(gè)屬性已經(jīng)有定義了,值為11)。而scope.twowayBindingProp有定義,因?yàn)樗褂昧恕健?)。
關(guān)于獨(dú)立作用域的更多信息請查看:http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/ -
transclude: true-指令創(chuàng)建了一個(gè)新的 "transcluded" 子作用域,并且原型繼承于父作用域。因此,如果你的嵌入的內(nèi)容(即ng-transclude將被替換的內(nèi)容)需要雙向數(shù)據(jù)綁定到父作用域上的一個(gè)原始類型上,使用$parent,或者將模型改為對象,綁定到改對象的某個(gè)屬性上。這會(huì)避免子作用域覆蓋父作用域的屬性。
內(nèi)嵌作用域和獨(dú)立作用域是同胞的--每個(gè)scope的$parent屬性指向同一個(gè)父作用域。當(dāng)內(nèi)嵌作用域和獨(dú)立作用域同時(shí)存在,獨(dú)立作用域的$$nextSibling 屬性會(huì)指向內(nèi)嵌作用域。
關(guān)于內(nèi)嵌作用域的更多信息,請查看AngularJS two way binding not working in directive with transcluded scope
假設(shè)上面的指令增加了屬性transclude: true,scope的示意圖如下:
圖12
這個(gè)jsfiddle有一個(gè)用來檢查獨(dú)立作用域和他相關(guān)的內(nèi)嵌作用域的showScope()函數(shù)。參考該fiddle中的注釋中的說明。
總結(jié)
有四種類型的作用域:
- 普通原型繼承作用域--ng-include、ng-switch、ng-controller和使用
scope: true定義的指令 - 含有拷貝屬性的普通原型繼承作用域--ng-repeat。每次迭代ng-repeat都會(huì)創(chuàng)建一個(gè)新的子作用域,同時(shí)新的子作用域會(huì)得到一個(gè)新的屬性。
- 獨(dú)立作用域--使用
scope: {...}定義的指令。這次沒有原型繼承,但是 '=', '@', and '&'提供了一種通過HTML屬性獲取父作用域?qū)傩缘臋C(jī)制。 - 內(nèi)嵌作用域--使用
transclude: true定義的指令。這次依舊是正常的基于原型的繼承,但是同時(shí)他也是任意獨(dú)立作用域的兄弟作用域。
對于所有的作用域(不論是否原型繼承),Angular總是通過$parent、$$childHead 和$$childTail追蹤父-子關(guān)系(即層級結(jié)構(gòu))