對AngularJS的作用域做深入剖析,該隨筆主要分為兩大板塊:JavaScript原型鏈、AngularJS作用域。 ...
一、概要 |
在AngularJS中,子作用域(child scope)基本上都要繼承自父作用域(parent scope)。
但,事無絕對,也有特例,那就是指令中scope設置項為對象時,即scope:{…},這將會讓指令創建一個並不繼承自父作用域的子作用域,我們稱之為隔離作用域(isolated scope)。
指令中的scope一共可以有三個值,下麵我們再來溫習下:
指令之scope |
|
scope: false |
預設值,指令不會新建一個作用域,使用父級作用域。 |
scope: true |
指令會創建一個新的子作用域,原型繼承於父級作用域。 |
scope: {…} |
指令會新建一個隔離作用域,不會原型繼承父作用域。 |
那麼,理解AngularJS中作用域繼承有什麼用呢?
原因之一就是,有利於我們使用“雙向綁定”(也就是在form表單元素中綁定ng-model),例如,在初學AngularJS時,我們常會遇到“雙向綁定”不起作用的時候,如下:
<!DOCTYPE html> <head> <meta charset="utf-8"/> <script src="angular.js"></script> </head> <body ng-app="myApp"> parent:<input type="text" ng-model="name"/> <div ng-controller="TestCtrl"> child: <input type="text" ng-model="name"/> </div> <script> var app = angular.module('myApp', []); app.controller('TestCtrl', function(){}); </script> </body> </html>
執行上述代碼,結果如下:
其實AngularJS的作用域繼承與JavaScript的原型繼承是一樣的邏輯,固,如果想要上述代碼實現雙向綁定,我們可以利用ng-model綁定對象屬性,來達到目的,如下:
<!DOCTYPE html> <head> <meta charset="utf-8"/> <script src="angular.js"></script> </head> <body ng-app="myApp"> parent:<input type="text" ng-model="obj.name"/> <div ng-controller="TestCtrl"> child: <input type="text" ng-model="obj.name"/> </div> <script> var app = angular.module('myApp', []); app.run(function($rootScope){ $rootScope.obj = {}; }); app.controller('TestCtrl', function(){}); </script> </body> </html>
執行上述代碼,結果如下:
二、JavaScript原型繼承 |
上面已經提到了AngularJS的作用域繼承與JavaScript的原型繼承是一樣兒一樣兒的,所以,我們首先來初步溫習下JavaScript的原型繼承。
假設,我們有父作用域(ParentScope),且其中包含了屬性aString、aNumber、anArray、anObject 以及aFunction。
好了,如果現在有一子作用域(ChildScope)繼承於這個父作用域(ParentScope),如下所示:
當我們通過ChildScope想訪問一個屬性時,JavaScript內部是如何為我們查找的呢?
答案:
首先JavaScript會第一時間在當前作用域(如這裡的ChildScope)中查找是否有這一屬性,如果在當前作用域中沒有找到,
那麼JavaScript就會沿著原型這條鏈(如這裡的:ChildScope-->ParentScope-->RootScope),一直找下去,倘若在某一父作用域中找到,就返回這個屬性值並停止原型鏈查找;
倘若一直找到根作用域(如這裡的RootScope)都沒有找到,則返undefined。
故而,下麵這些表達式,結果都為true:
childScope.aString === 'parent string' //true childScope.anArray[1] === 20 //true childScope.anObject.property1 === 'parent prop1' //true childScope.aFunction() === 'parent output' //true
好了,假如,我們這麼做呢:
childScope.aString = 'child string'
那麼只會在childScope中新建一個值為’child string’的aString屬性。在這之後,倘若我們還想通過childScope訪問parentScope中aString屬性時,就束手無策了。
因為childScope已經有了一個aString屬性。理解這一點是非常重要的,在我們討論ng-repeat 和ng-include之前。
接下來,我們再這麼做呢:
childScope.anArray[1] = '22'
childScope.anObject.property1 = 'child prop1'
這樣會沿著原型鏈查找的,並改變屬性中的值。
為什麼呢?
原因就是我們這次賦值的是對象中的屬性。
好了,接下來,我們再這麼做:
childScope.anArray = [100, 555]
childScope.anObject = {name: 'Mark', country: 'USA'}
這樣做的效果,如同上面childScope.aString = ‘child string’一樣,不會啟動原型鏈查找。
總結:
1、如果我們讀取子作用域的屬性時,且該子作用域有這個屬性,則不會啟動原型鏈查找;
2、如果我們賦值子作用域的屬性時,依然不會啟動原型鏈查找。
1、If we read childScope.propertyX, and childScope has propertyX, then the prototype chain is not consulted.
2、If we set childScope.propertyX, the prototype chain is not consulted.
EnglishExpression
通過上面的總結,如果我們想讓childScope在已有自己的anArray屬性後,仍然訪問parentScope中的anArray值呢?
哈哈,刪除childScope中的anArray屬性嘛,如下:
delete childScope.anArray childScope.anArray[1] === 22 //true
三、Angular作用域繼承 |
在前面“概要”部分已經說到Angular中子作用域基本上都繼承自父作用域,但也有例外(scope:{…}指令),但並沒有具體說明哪些指令會創建子作用域等,現在歸類如下:
創建子作用域,且繼承自父作用域 |
1、 ng-repeat 2、 ng-include 3、 ng-switch 4、 ng-controller 5、 directive (scope: true) 6、 directive(transclude: true) |
創建子作用域,但並不繼承自父作用域 |
directive(scope: {…}) |
不創建作用域 |
directive(scoep: false) |
下麵分別看看:
--ng-include--
假設我們有一控制器,內容如下:
module.controller('parentCtrl', function($scope){ $scope.myPrimitive = 50; $scope.myObject = {aNumber: 11}; });
有HTML代碼如下:
<div ng-controller="parentCtrl"> parent-myPrimitive:<input type="text" ng-model="myPrimitive"/><br/> parent-obj.aNumber:<input type="text" ng-model="myObject.aNumber"/><br/> <script type="text/ng-template" id="/tpl1.html"> includ-myPrimitive:<input ng-model = "myPrimitive"/> </script> <div ng-include src="'/tpl1.html'"></div> <script type="text/ng-template" id="/tpl2.html"> includ-obj.aNumber:<input ng-model="myObject.aNumber"/> </script> <div ng-include src="'/tpl2.html'"></div> </div>代碼稍長,請自行打開
因為每個ng-include指令,都會創建一個新的作用域,且繼承於父作用域。固,代碼中的關係圖如下:
在chrome(需加--disable-web-security)下,執行上述代碼後,得下:
從執行結果看,符合上面的關係圖。
好了,倘若我們在第三個input框中,敲入字元(如77)後,子作用域會創建一個自己的myPrimitive屬性,從而阻止原型鏈查找。
如下所示:
倘若,我們在第四個input框中,輸入字元(如99)呢?子作用域會沿著原型鏈查找並修改,因為在這個input框中,我們綁定的是對象屬性。
如下圖所示:
如果我們想讓第三個input框達到第四個input框的效果,並且不使用對象屬性的方法,那麼我們可以利用$parent來達到目的。
修改第三個input框的HTML代碼:
includ-myPrimitive:<input ng-model="$parent.myPrimitive"/>
好了,這個時候,我們再在第三個input框中輸入22時,就會沿著原型鏈查找了,達到與第四個input框一樣的效果(因為$parent是為了讓子作用域訪問父作用域設置的屬性)。
除開$parent,還有$$childHead和$$childTail都是為了子作用和父作用域通信服務的。註意,是所有子作用域哦,所以包括了scope:{…}指令的情況。
--ng-switch--
ng-switch會創建子作用域,效果與上面所述的ng-include一樣。倘若,我們想要實現子作用域與父作用域實現雙向綁定,就使用$parent或者對象屬性。
Demo如下:
<!DOCTYPE html> <head> <meta charset="utf-8"/> <script src="angular.js"></script> </head> <body ng-app="myModule"> <div ng-controller="parentCtrl"> <input type="text" ng-model="obj.something"/> <div ng-switch="name"> <div ng-switch-when="Monkey"> <h1>This is Monkey</h1> <input type="text" ng-model="obj.something"/> </div> <div ng-switch-default> <h1>This is Default</h1> </div> </div> </div> <script> var module = angular.module('myModule', []); module.controller('parentCtrl', function($scope){ $scope.obj = {}; $scope.name = "Monkey"; }); </script> </body> </html>代碼稍長,請自行打開
執行上述代碼,效果如下:
--ng-repeat--
ng-repeat也會創建子作用域,不過與上面講述的ng-include、ng-switch不同的是,ng-repeat會為每個遍歷的元素創建子作用域,且繼承自同一父作用域。
好了,下麵我們來具體看看,假如,現在我們有一控制器,如下:
module.controller('parentCtrl', function($scope){ $scope.myArrayOfPrimitives = [11, 22]; $scope.myArrayOfObjects = [{num: 101}, {num: 202}]; });
HTML代碼如下:
<div ng-controller="parentCtrl"> <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> </div>
因為,ng-repeat會為每個元素都創建一個子作用域,如上面代碼中<li ng-repeat=”num in myArrayOfPrimitives”>,ng-repeat指令會用num,去遍歷myArrayOfPrimitives:[11, 22]中的數據,即創建兩個繼承自父作用域的子作用域。且,在子作用域中會創建自己的屬性num,因此,倘若子作用域中的num值變動,肯定不會改變父作用域中myArrayOfPrimitives的相應數據咯。
然而,HTML代碼中第二個出現ng-repeat的地方<li ng-repeat=”obj in myArrayOfObjects”>,當子作用域變動時,會影響到父作用域中對應的元素。因為數組myArrayOfObjects:[{…}, {…}]中的元素為對象,固而,遍歷myArrayOfObjects中元素時,子作用域賦值的是對象,引用類型嘛,所以子作用域中變動對象屬性時,肯定會影響父作用域的相關值咯。
--ng-controller--
ng-controller指令與ng-include、ng-switch一樣,即創建子作用域,且繼承自父作用域。
--directives--
詳情見“初探指令”
需要註意的是,scope為對象的指令(scope:{…}),雖然該類指令的作用域為隔離作用域,但是,它任然可以通過$parent訪問父作用域。
Isolate scope's __proto__ references Object. Isolate scope's $parent references the parent scope, so although it is isolated and doesn't inherit prototypically from the parent scope, it is still a child scope.EnglishExpression
Demo如下:
<!DOCTYPE html> <head> <meta charset="utf-8"/> <script src="angular.js"></script> </head> <body ng-app="myApp"> <div ng-controller="TestCtrl"> <input type="text" ng-model="name"/> <test></test> </div> <script> var app = angular.module('myApp', []); app.controller('TestCtrl', function($scope){ $scope.name = 'Monkey'; }); app.directive('test', function(){ return { restrict: 'E', scope: {}, controller: function($scope){ $scope.name = $scope.$parent.name; }, template: '<input type="text" ng-model="$parent.name"/>' }; }); </script> </body> </html>代碼稍長,請自行打開
執行上述代碼,操作如下:
四、總結 |
有四種類型的子作用域:
1、要繼承於父作用域—ng-include, ng-switch, ng-controller, directive(scope: true).
2、要繼承於父作用域,但會為每個元素創建一個子作用域—ng-repeat.
3、隔離作用域—directive(scope:{…}).
4、要繼承於父作用域,且與任何的隔離作用的指令為兄弟關係—directive(transclude: true).
the directive creates a new "transcluded" child scope, which prototypically inherits from the parent scope. The transcluded and the isolated scope (if any) are siblings -- the $parent property of each scope references the same parent scope. When a transcluded and an isolate scope both exist, isolate scope property $$nextSibling will reference the transcluded scope.transclude:true
--Summary_in_English--
There are four types of scopes:
1、normal prototypal scope inheritance -- ng-include, ng-switch, ng-controller, directive with scope: true
2、normal prototypal scope inheritance with a copy/assignment -- ng-repeat. Each iteration of ng-repeat creates a new child scope, and that new child scope always gets a new property.
3、isolate scope -- directive with scope: {...}. This one is not prototypal, but '=', '@', and '&' provide a mechanism to access parent scope properties, via attributes.
4、transcluded scope -- directive with transclude: true. This one is also normal prototypal scope inheritance, but it is also a sibling of any isolate scope.
For all scopes (prototypal or not), Angular always tracks a parent-child relationship (i.e., a hierarchy), via properties $parent and $$childHead and $$childTail.
Summary
五、參考 |