1. 程式人生 > >理解AngularJS的作用域Scope

理解AngularJS的作用域Scope

概敘:

AngularJS中,子作用域一般都會通過JavaScript原型繼承機制繼承其父作用域的屬性和方法。但有一個例外:在directive中使用scope: { ... },這種方式建立的作用域是一個獨立的"Isolate"作用域,它也有父作用域,但父作用域不在其原型鏈上,不會對父作用域進行原型繼承。這種方式定義作用域通常用於構造可複用的directive元件。

作用域的原型繼承是非常簡單普遍的,甚至你不必關心它的運作。直到你在子作用域中向父作用域的原始型別屬性使用雙向資料繫結2-way data binding,比如Form表單的ng-model為父作用域中的屬性,且為原始型別,輸入資料後,它不會如你期望的那樣執行——AngularJS不會把輸入資料寫到你期望的父作用域屬性中去,而是直接在子作用域建立同名屬性並寫入資料。這個行為符合JavaScript原型繼承機制的行為。AngularJS新手通常沒有認識到ng-repeat

、 ng-switchng-viewng-include 都會建立子作用域, 所以經常出問題。 (見 示例)

比如:

<inputtype="text"ng-model="someObj.prop1">

優於:

<inputtype="text"ng-model="prop1">

如果你一定要直接使用原始型別,要注意兩點:

  1. 在子作用域中使用 $parent.parentScopeProperty,這樣可以直接修改父作用域的屬性。
  2. 在父作用域中定義函式,子作用域通過原型繼承呼叫函式把值傳遞給父作用域(這種方式極少使用)。

正文:

JavaScript 原型繼承機制

你必須完全理解JavaScript的原型繼承機制,尤其是當你有後端開發背景和類繼承經驗的時候。所以我們先來回顧一下原型繼承:

假設父作用域parentScope擁有以下屬性和方法:aStringaNumberanArrayanObjectaFunction。子作用域childScope如果從父作用域parentScope進行原型繼承,我們將看到:

normal prototypal inheritance

(注:為節約空間,anArray使用了藍色方塊圖)

如果我們在子作用域中訪問一個父作用域中定義的屬性,JavaScript首先在子作用域中尋找該屬性,沒找到再從原型鏈上的父作用域中尋找,如果還沒找到會再往上一級原型鏈的父作用域尋找。在AngularJS中,作用域原型鏈的頂端是$rootScope

,JavaScript尋找到$rootScope為止。所以,以下表達式均為true

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

如果我們進行如下操作:

childScope.aString ='child string'

因為我們賦值目標是子作用域的屬性,原型鏈將不會被查詢,一個新的與父作用域中屬性同名的屬性aString將被新增到當前的子作用域childScope中。

shadowing

如果我們進行如下操作:

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

因為我們的賦值目標是子作用域屬性anArrayanObject的子屬性,也就是說JavaScript必須先要先尋找anArrayanObject這兩個物件——它們必須為物件,否則不能寫入屬性,而這兩個物件不在當前子作用域,原型鏈將被查詢,在父作用域中找到這兩個物件, 然後對這兩個物件的屬性[1]property1進行賦值操作。子作用域中不會不會建立兩個新的同名屬性!(注意JavaScript中陣列和函式均是物件——引用型別)

follow the chain

如果我們進行如下操作:

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

同樣因為我們賦值目標是子作用域的屬性,原型鏈將不會被查詢,,JavaScript會直接在子作用域建立兩個同名屬性,其值分別為陣列和物件。

not following the chain

要點:

  • 如果我們讀取childScope.propertyX,並且childScope存在propertyX,原型鏈不會被查詢;
  • 如果我們寫入childScope.propertyX, 原型鏈也不會被查詢;
  • 如果我們寫入childScope.propertyX.subPropertyY, 並且childScope不存在propertyX,原型鏈將被查詢——查詢propertyX

最後一點:

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

如果我們先刪除了子作用域childScope的屬性,然後再讀取該屬性,因為找不到該屬性,原型鏈將被查詢。

after deleting a property

AngularJS 作用域Scope的繼承

提示:

  • 以下方式會建立新的子作用域,並且進行原型繼承: ng-repeatng-includeng-switchng-viewng-controller, 用scope: truetransclude: true建立directive。
  • 以下方式會建立新的獨立作用域,不會進行原型繼承:用scope: { ... }建立directive。這樣建立的作用域被稱為"Isolate"作用域。

注意:預設情況下建立directive使用了scope: false,不會建立子作用域。

進行原型繼承即意味著父作用域在子作用域的原型鏈上,這是JavaScript的特性。AngularJS的作用域還存在如下內部定義的關係:

  • scope.$parent指向scope的父作用域;
  • scope.$$childHead指向scope的第一個子作用域;
  • scope.$$childTail指向scope的最後一個子作用域;
  • scope.$$nextSibling指向scope的下一個相鄰作用域;
  • scope.$$prevSibling指向scope的上一個相鄰作用域;

這些關係用於AngularJS內部歷遍,如$broadcast和$emit事件廣播,$digest處理等。

ng-include

In controller:

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

In HTML:

<scripttype="text/ng-template"id="/tpl1.html"><input ng-model="myPrimitive"></script><divng-includesrc="'/tpl1.html'"></div><scripttype="text/ng-template"id="/tpl2.html"><input ng-model="myObject.aNumber"></script><divng-includesrc="'/tpl2.html'"></div>

每一個ng-include指令都建立一個子作用域, 並且會從父作用域進行原型繼承。

ng-include

在第一個input框輸入"77"將會導致子作用域中新建一個同名屬性,其值為77,這不是你想要的結果。

ng-include primitive

在第二個input框輸入"99"會直接修改父作用域的myObject物件,這就是JavaScript原型繼承機制的作用。

ng-include object

(注:上圖存在錯誤,紅色99因為是50,11應該是99)

如果我們不想把model由原始型別改成引用型別——物件,我們也可以使用$parent直接操作父作用域:

<inputng-model="$parent.myPrimitive">

輸入"22"我們得到了想要的結果。

ng-include $parent

另一種方法就是使用函式,在父作用域定義函式,子作用域通過原型繼承可執行該函式:

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

請參考:

sample fiddle that uses this "parent function" approach. (This was part of aStack Overflow post.)

ng-switch

ng-switchng-include一樣。

ng-view

ng-viewng-include一樣。

ng-repeat

Ng-repeat也建立子作用域,但有些不同。

In controller:

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

In HTML:

<ul><ling-repeat="num in myArrayOfPrimitives"><inputng-model="num"></li></ul><ul><ling-repeat="obj in myArrayOfObjects"><inputng-model="obj.num"></li></ul>

ng-repeat對每一個迭代項Item都會建立子作用域, 子作用域也從父作用域進行原型繼承。 但它還是會在子作用域中新建同名屬性,把Item賦值給對應的子作用域的同名屬性。 下面是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無關聯:

ng-repeat primitive

顯然這不會是你想要的結果。我們需要的是在子作用域中修改了值後反映到myArrayOfPrimitives陣列。我們需要使用引用型別的Item,如上面第二個ng-repeat所示。

myArrayOfObjects的每一項Item都是一個物件——引用型別,ng-repeat對每一個Item建立子作用域,並在子作用域新建obj屬性,obj屬性就是該Item的一個引用,而不是副本。

ng-repeat object

我們修改子作用域的obj.num就是修改了myArrayOfObjects。這才是我們想要的結果。

參考:

ng-controller

使用ng-controllerng-include一樣也是建立子作用域,會從父級controller建立的作用域進行原型繼承。但是,利用原型繼承來使父子controller共享資料是一個糟糕的辦法。 "it is considered bad form for two controllers to share information via $scope inheritance",controllers之間應該使用 service進行資料共享。

directives

  1. 預設 (scope: false) - directive使用原有作用域,所以也不存在原型繼承,這種方式很簡單,但也很容易出問題——除非該directive與html不存在資料繫結,否則一般情況建議使用第2條方式。
  2. scope: true - directive建立一個子作用域, 並且會從父作用域進行原型繼承。 如果同一個DOM element存在多個directives要求建立子作用域,那麼只有一個子作用域被建立,directives共用該子作用域。
  3. scope: { ... } - directive建立一個獨立的“Isolate”作用域,沒有原型繼承。這是建立可複用directive元件的最佳選擇。因為它不會直接訪問/修改父作用域的屬性,不會產生意外的副作用。這種directive與父作用域進行資料通訊有如下四種方式(更詳細的內容請參考Developer Guide):

    1. = or =attr “Isolate”作用域的屬性與父作用域的屬性進行雙向繫結,任何一方的修改均影響到對方,這是最常用的方式;
    2. @ or @attr “Isolate”作用域的屬性與父作用域的屬性進行單向繫結,即“Isolate”作用域只能讀取父作用域的值,並且該值永遠的String型別
    3. & or &attr “Isolate”作用域把父作用域的屬性包裝成一個函式,從而以函式的方式讀寫父作用域的屬性,包裝方法是$parse,詳情請見API-$parse

    “Isolate”作用域的__proto__是一個標準Scope object (the picture below needs to be updated to show an orange 'Scope' object instead of an 'Object'). “Isolate”作用域的$parent同樣指向父作用域。它雖然沒有原型繼承,但它仍然是一個子作用域。

    如下directive:

    <my-directiveinterpolated="{{parentProp1}}"twowayBinding="parentProp2">

    scope:

     scope:{ interpolatedProp:'@interpolated', twowayBindingProp:'=twowayBinding'}

    link函式中:

     scope.someIsolateProp ="I'm isolated"

    isolate scope

    請注意,我們在link函式中使用attrs.$observe('interpolated', func