2013年8月2日, 嚴(yán)清 譯
譯文:理解AngularJS的作用域Scope
原文:Understanding Scopes
概敘:
AngularJS中,子作用域一般都會通過JavaScript原型繼承機(jī)制繼承其父作用域的屬性和方法。但有一個例外:在directive中使用scope: { ... },這種方式創(chuàng)建的作用域是一個獨立的"Isolate"作用域,它也有父作用域,但父作用域不在其原型鏈上,不會對父作用域進(jìn)行原型繼承。這種方式定義作用域通常用于構(gòu)造可復(fù)用的directive組件。作用域的原型繼承是非常簡單普遍的,甚至你不必關(guān)心它的運作。直到你在子作用域中向父作用域的原始類型屬性使用雙向數(shù)據(jù)綁定2-way data binding,比如Form表單的ng-model為父作用域中的屬性,且為原始類型,輸入數(shù)據(jù)后,它不會如你期望的那樣運行——AngularJS不會把輸入數(shù)據(jù)寫到你期望的父作用域?qū)傩灾腥ィ侵苯釉谧幼饔糜騽?chuàng)建同名屬性并寫入數(shù)據(jù)。這個行為符合JavaScript原型繼承機(jī)制的行為。AngularJS新手通常沒有認(rèn)識到ng-repeat、 ng-switch、ng-view和ng-include 都會創(chuàng)建子作用域, 所以經(jīng)常出問題。 (見 示例)避免這個問題的最佳實踐是在ng-model中總使用.,參見文章 always have a '.' in your ng-models。
比如:
<input type="text" ng-model="someObj.prop1">
優(yōu)于:
<input type="text" ng-model="prop1">
如果你一定要直接使用原始類型,要注意兩點:
在子作用域中使用 $parent.parentScopeProperty,這樣可以直接修改父作用域的屬性。在父作用域中定義函數(shù),子作用域通過原型繼承調(diào)用函數(shù)把值傳遞給父作用域(這種方式極少使用)。
正文:
JavaScript Prototypal Inheritance
Angular Scope Inheritanceng-include
ng-switch
ng-view
ng-repeat
ng-controller
directives
JavaScript 原型繼承機(jī)制
你必須完全理解JavaScript的原型繼承機(jī)制,尤其是當(dāng)你有后端開發(fā)背景和類繼承經(jīng)驗的時候。所以我們先來回顧一下原型繼承:
假設(shè)父作用域parentScope擁有以下屬性和方法:aString、aNumber、anArray、anObject、aFunction。子作用域childScope如果從父作用域parentScope進(jìn)行原型繼承,我們將看到:
(注:為節(jié)約空間,anArray使用了藍(lán)色方塊圖)
如果我們在子作用域中訪問一個父作用域中定義的屬性,JavaScript首先在子作用域中尋找該屬性,沒找到再從原型鏈上的父作用域中尋找,如果還沒找到會再往上一級原型鏈的父作用域?qū)ふ摇T贏ngularJS中,作用域原型鏈的頂端是$rootScope,JavaScript尋找到$rootScope為止。所以,以下表達(dá)式均為true:
childScope.aString === 'parent string'childScope.anArray[1] === 20childScope.anObject.property1 === 'parent prop1'childScope.aFunction() === 'parent output'
如果我們進(jìn)行如下操作:
childScope.aString = 'child string'
因為我們賦值目標(biāo)是子作用域的屬性,原型鏈將不會被查詢,一個新的與父作用域中屬性同名的屬性aString將被添加到當(dāng)前的子作用域childScope中。
如果我們進(jìn)行如下操作:
childScope.anArray[1] = '22'childScope.anObject.property1 = 'child prop1'
因為我們的賦值目標(biāo)是子作用域?qū)傩詀nArray和anObject的子屬性,也就是說JavaScript必須先要先尋找anArray和anObject
這兩個對象——它們必須為對象,否則不能寫入屬性,而這兩個對象不在當(dāng)前子作用域,原型鏈將被查詢,在父作用域中找到這兩個對象, 然后對這兩個對象的屬性[1]和property1進(jìn)行賦值操作。子作用域中不會不會創(chuàng)建兩個新的同名屬性!(注意JavaScript中數(shù)組和函數(shù)均是對象——引用類型)
如果我們進(jìn)行如下操作:
childScope.anArray = [100, 555]childScope.anObject = { name: 'Mark', country: 'USA' }
同樣因為我們賦值目標(biāo)是子作用域的屬性,原型鏈將不會被查詢,,JavaScript會直接在子作用域創(chuàng)建兩個同名屬性,其值分別為數(shù)組和對象。
要點:
如果我們讀取childScope.propertyX,并且childScope存在propertyX,原型鏈不會被查詢;
如果我們寫入childScope.propertyX, 原型鏈也不會被查詢;
如果我們寫入childScope.propertyX.subPropertyY, 并且childScope不存在propertyX,原型鏈將被查詢——查找propertyX。
最后一點:
delete childScope.anArraychildScope.anArray[1] === 22 // true
如果我們先刪除了子作用域childScope的屬性,然后再讀取該屬性,因為找不到該屬性,原型鏈將被查詢。
AngularJS 作用域Scope的繼承
**提示:
以下方式會創(chuàng)建新的子作用域,并且進(jìn)行原型繼承: ng-repeat、ng-include、ng-switch、ng-view、ng-controller, 用scope: true
和transclude: true 創(chuàng)建directive。以下方式會創(chuàng)建新的獨立作用域,不會進(jìn)行原型繼承:用scope: { ... }創(chuàng)建directive。這樣創(chuàng)建的作用域被稱為"Isolate"作用域。
注意:默認(rèn)情況下創(chuàng)建directive使用了scope: false,不會創(chuàng)建子作用域。進(jìn)行原型繼承即意味著父作用域在子作用域的原型鏈上,這是JavaScript的特性。AngularJS的作用域還存在如下內(nèi)部定義的關(guān)系:
scope.$parent指向scope的父作用域;
scope.$$childHead指向scope的第一個子作用域;
scope.$$childTail指向scope的最后一個子作用域;
scope.$$nextSibling指向scope的下一個相鄰作用域;
scope.$$prevSibling指向scope的上一個相鄰作用域;
這些關(guān)系用于AngularJS內(nèi)部歷遍,如$broadcast和$emit事件廣播,$digest處理等。
ng-include
In controller:
$scope.myPrimitive = 50;$scope.myObject = {aNumber: 11};
In 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>
每一個ng-include指令都創(chuàng)建一個子作用域, 并且會從父作用域進(jìn)行原型繼承。
在第一個input框輸入"77"將會導(dǎo)致子作用域中新建一個同名屬性,其值為77,這不是你想要的結(jié)果。
在第二個input框輸入"99"會直接修改父作用域的myObject對象,這就是JavaScript原型繼承機(jī)制的作用。
(注:上圖存在錯誤,紅色99因為是50,11應(yīng)該是99)
如果我們不想把model由原始類型改成引用類型——對象,我們也可以使用$parent直接操作父作用域:
<input ng-model="$parent.myPrimitive">
輸入"22"我們得到了想要的結(jié)果。
另一種方法就是使用函數(shù),在父作用域定義函數(shù),子作用域通過原型繼承可運行該函數(shù):
// in the parent scope
$scope.setMyPrimitive = function(value) { $scope.myPrimitive = value;}
請參考:
sample fiddle that uses this "parent function" approach. (This was part of a Stack Overflow post.)
http://stackoverflow.com/a/13782671/215945
https://github.com/angular/angular.js/issues/1267.
ng-switch
ng-switch與ng-include一樣。
參考: AngularJS, bind scope of a switch-case?
ng-view
ng-view與ng-include一樣。
ng-repeat
ng-repeat
也創(chuàng)建子作用域,但有些不同。
In controller:
$scope.myArrayOfPrimitives = [ 11, 22 ];$scope.myArrayOfObjects = [{num: 101}, {num: 202}]
In HTML:
<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>
ng-repeat
對每一個迭代項Item都會創(chuàng)建子作用域, 子作用域也從父作用域進(jìn)行原型繼承。 但它還是會在子作用域中新建同名屬性,把Item賦值給對應(yīng)的子作用域的同名屬性。 下面是AngularJS中ng-repeat的部分源代碼:
childScope = scope.$new(); // child scope prototypically inherits from parent scope ... childScope[valueIdent] = value; // creates a new childScope property
如果Item是原始類型(如myArrayOfPrimitives的11、22), 那么子作用域中有一個新屬性(如num),它是Item的副本(11、22). 修改子作用域num的值將不會改變父作用域myArrayOfPrimitives,所以在上一個ng-repeat,每一個子作用域都有一個num 屬性,該屬性與myArrayOfPrimitives無關(guān)聯(lián):
顯然這不會是你想要的結(jié)果。我們需要的是在子作用域中修改了值后反映到myArrayOfPrimitives數(shù)組。我們需要使用引用類型的Item,如上面第二個ng-repeat所示。
myArrayOfObjects的每一項Item都是一個對象——引用類型,ng-repeat對每一個Item創(chuàng)建子作用域,并在子作用域新建obj屬性,obj屬性就是該Item的一個引用,而不是副本。
我們修改子作用域的obj.num就是修改了myArrayOfObjects。這才是我們想要的結(jié)果。
**參考:
Difficulty with ng-model, ng-repeat, and inputs
ng-repeat and databinding
ng-controller
使用ng-controller與ng-include一樣也是創(chuàng)建子作用域,會從父級controller創(chuàng)建的作用域進(jìn)行原型繼承。但是,利用原型繼承來使父子controller共享數(shù)據(jù)是一個糟糕的辦法。 "it is considered bad form for two controllers to share information via $scope inheritance",controllers之間應(yīng)該使用 service進(jìn)行數(shù)據(jù)共享。
(如果一定要利用原型繼承來進(jìn)行父子controllers之間數(shù)據(jù)共享,也可以直接使用。 請參考: Controller load order differs when loading or navigating)
directives
默認(rèn) (scope: false) - directive使用原有作用域,所以也不存在原型繼承,這種方式很簡單,但也很容易出問題——除非該directive與html不存在數(shù)據(jù)綁定,否則一般情況建議使用第2條方式。
scope: true
- directive創(chuàng)建一個子作用域, 并且會從父作用域進(jìn)行原型繼承。 如果同一個DOM element存在多個directives要求創(chuàng)建子作用域,那么只有一個子作用域被創(chuàng)建,directives共用該子作用域。
scope: { ... } - directive創(chuàng)建一個獨立的“Isolate”作用域,沒有原型繼承。這是創(chuàng)建可復(fù)用directive組件的最佳選擇。因為它不會直接訪問/修改父作用域的屬性,不會產(chǎn)生意外的副作用。這種directive與父作用域進(jìn)行數(shù)據(jù)通信有如下四種方式(更詳細(xì)的內(nèi)容請參考Developer Guide):
= or =attr “Isolate”作用域的屬性與父作用域的屬性進(jìn)行雙向綁定,任何一方的修改均影響到對方,這是最常用的方式;
@ or @attr “Isolate”作用域的屬性與父作用域的屬性進(jìn)行單向綁定,即“Isolate”作用域只能讀取父作用域的值,并且該值永遠(yuǎn)的String類型;
& or &attr “Isolate”作用域把父作用域的屬性包裝成一個函數(shù),從而以函數(shù)的方式讀寫父作用域的屬性,包裝方法是$parse,詳情請見API-$parse;
“Isolate”作用域的proto是一個標(biāo)準(zhǔn)Scope object (the picture below needs to be updated to show an orange 'Scope' object instead of an 'Object'). “Isolate”作用域的$parent同樣指向父作用域。它雖然沒有原型繼承,但它仍然是一個子作用域。
如下directive:
<my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">
scope:
scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
link函數(shù)中:
scope.someIsolateProp = "I'm isolated"
請注意,我們在link函數(shù)中使用attrs.$observe('interpolated', function(value) { ... }來監(jiān)測@屬性的變化。
更多請參考: http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
transclude: true
- directive新建一個“transcluded”子作用域,并且會從父作用域進(jìn)行原型繼承。需要注意的是,“transcluded”作用域與“Isolate”作用域是相鄰的關(guān)系(如果“Isolate”作用域存在的話) -- 他們的$parent屬性指向同一個父作用域。“Isolate”作用域的$$nextSibling指向“transcluded”作用域。
更多請參考: AngularJS two way binding not working in directive with transcluded scope
transcluded scope
demo: fiddle
總結(jié)
AngularJS存在四種作用域:
普通的帶原型繼承的作用域 -- ng-include, ng-switch, ng-controller, directive with scope: true;普通的帶原型繼承的,并且有賦值行為的作用域 -- ng-repeat,ng-repeat為每一個迭代項創(chuàng)建一個普通的有原型繼承的子作用域,但同時在子作用域中創(chuàng)建新屬性存儲迭代項;“Isolate”作用域 -- directive with scope: {...}, 該作用域沒有原型繼承,但可以通過'=', '@', 和 '&'與父作用域通信。“transcluded”作用域 -- directive with transclude: true,它也是普通的帶原型繼承的作用域,但它與“Isolate”作用域是相鄰的好基友。
Diagrams were generated with GraphViz "*.dot" files, which are on github. Tim Caswell's "Learning JavaScript with Object Graphs" was the inspiration for using GraphViz for the diagrams.
The above was originally posted on StackOverflow.