理解AngularJS的做用域Scope

原文:Understanding Scopes

概敘:

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

做用域的原型繼承是很是簡單廣泛的,甚至你沒必要關心它的運做。直到你在子做用域中向父做用域的原始類型屬性使用雙向數據綁定2-way data binding,好比Form表單的ng-model爲父做用域中的屬性,且爲原始類型,輸入數據後,它不會如你指望的那樣運行——AngularJS不會把輸入數據寫到你指望的父做用域屬性中去,而是直接在子做用域建立同名屬性並寫入數據。這個行爲符合JavaScript原型繼承機制的行爲。AngularJS新手一般沒有認識到ng-repeat、 ng-switchng-viewng-include 都會建立子做用域, 因此常常出問題。 (見 示例)git

避免這個問題的最佳實踐是在ng-model中總使用.,參見文章 always have a '.' in your ng-modelsangularjs

好比:github

<input type="text" ng-model="someObj.prop1">

優於:後端

<input type="text" ng-model="prop1">

若是你必定要直接使用原始類型,要注意兩點:api

  1. 在子做用域中使用 $parent.parentScopeProperty,這樣能夠直接修改父做用域的屬性。
  2. 在父做用域中定義函數,子做用域經過原型繼承調用函數把值傳遞給父做用域(這種方式極少使用)。

正文:

JavaScript 原型繼承機制

你必須徹底理解JavaScript的原型繼承機制,尤爲是當你有後端開發背景和類繼承經驗的時候。因此咱們先來回顧一下原型繼承:數組

假設父做用域parentScope擁有如下屬性和方法:aStringaNumberanArrayanObjectaFunction。子做用域childScope若是從父做用域parentScope進行原型繼承,咱們將看到:app

normal prototypal inheritance

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

若是咱們在子做用域中訪問一個父做用域中定義的屬性,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:

<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指令都建立一個子做用域, 而且會從父做用域進行原型繼承。

ng-include

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

ng-include primitive

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

ng-include object

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

若是咱們不想把model由原始類型改爲引用類型——對象,咱們也可使用$parent直接操做父做用域:

<input ng-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.)

http://stackoverflow.com/a/13782671/215945

https://github.com/angular/angular.js/issues/1267.

ng-switch

ng-switchng-include同樣。

參考: AngularJS, bind scope of a switch-case?

ng-view

ng-viewng-include同樣。

ng-repeat

Ng-repeat也建立子做用域,但有些不一樣。

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都會建立子做用域, 子做用域也從父做用域進行原型繼承。 但它仍是會在子做用域中新建同名屬性,把Item賦值給對應的子做用域的同名屬性。 下面是AngularJS中ng-repeat的部分源代碼:

childScope = scope.$new(); // child scope prototypically inherits from parent scope ...  childScope[valueIdent] = value; // creates a new childScope property

若是Item是原始類型(如myArrayOfPrimitives的十一、22), 那麼子做用域中有一個新屬性(如num),它是Item的副本(十一、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。這纔是咱們想要的結果。

參考:

Difficulty with ng-model, ng-repeat, and inputs

ng-repeat and databinding

ng-controller

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

(若是必定要利用原型繼承來進行父子controllers之間數據共享,也能夠直接使用。 請參考: Controller load order differs when loading or navigating)

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-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">

    scope:

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

    link函數中:

     scope.someIsolateProp = "I'm isolated"

    isolate scope

    請注意,咱們在link函數中使用attrs.$observe('interpolated', function(value) { ... }來監測@屬性的變化。

    更多請參考: http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/

  4. transclude: true - directive新建一個「transcluded」子做用域,而且會從父做用域進行原型繼承。須要注意的是,「transcluded」做用域與「Isolate」做用域是相鄰的關係(若是「Isolate」做用域存在的話) -- 他們的$parent屬性指向同一個父做用域。「Isolate」做用域的$$nextSibling指向「transcluded」做用域。

    更多請參考: AngularJS two way binding not working in directive with transcluded scope

    transcluded scope

    demo: fiddle

總結

AngularJS存在四種做用域:

  1. 普通的帶原型繼承的做用域 -- ng-includeng-switchng-controller, directive with scope: true
  2. 普通的帶原型繼承的,而且有賦值行爲的做用域 -- ng-repeat,ng-repeat爲每個迭代項建立一個普通的有原型繼承的子做用域,但同時在子做用域中建立新屬性存儲迭代項;
  3. 「Isolate」做用域 -- directive with scope: {...}, 該做用域沒有原型繼承,但能夠經過'=', '@', 和 '&'與父做用域通訊。
  4. 「transcluded」做用域 -- directive with transclude: true,它也是普通的帶原型繼承的做用域,但它與「Isolate」做用域是相鄰的好基友。
相關文章
相關標籤/搜索