深入探究AngularJS框架中Scope對(duì)象的超級(jí)教程
一、遇到的問(wèn)題
問(wèn)題發(fā)生在使用 AngularJS 嵌套 Controller 的時(shí)候。因?yàn)槊總€(gè) Controller 都有它對(duì)應(yīng)的 Scope(相當(dāng)于作用域、控制范圍),所以 Controller 的嵌套,也就意味著 Scope 的嵌套。這個(gè)時(shí)候如果兩個(gè) Scope 內(nèi)都有同名的 Model 會(huì)發(fā)生什么呢?從子 Scope 怎樣更新父 Scope 里的 Model 呢?
這個(gè)問(wèn)題很典型,比方說(shuō)當(dāng)前頁(yè)面是一個(gè)產(chǎn)品列表,那么就需要定義一個(gè) ProductListController
function ProductListController($scope, $http) { $http.get('/api/products.json') .success(function(data){ $scope.productList = data; }); $scope.selectedProduct = {}; }
你大概看到了在 Scope 里還定義了一個(gè) selectedProduct 的 Model,表示選中了某一個(gè)產(chǎn)品。這時(shí)會(huì)獲取該產(chǎn)品詳情,而頁(yè)面通過(guò) AngularJS 中的 $routeProvider 自動(dòng)更新,拉取新的詳情頁(yè)模板,模板中有一個(gè) ProductDetailController
function ProductDetailController($scope, $http, $routeParams) { $http.get('/api/products/'+$routeParams.productId+'.json') .success(function(data){ $scope.selectedProduct = data; }); }
有趣的事情發(fā)生了,在這里也有一個(gè) selectedProduct ,它會(huì)怎樣影響 ProductListController 中的 selectedProduct 呢?
答案是沒(méi)有影響。在 AnuglarJS 里子 Scope 確實(shí)會(huì)繼承父 Scope 中的對(duì)象,但當(dāng)你試下對(duì)基本數(shù)據(jù)類(lèi)型(string, number, boolean)的 雙向數(shù)據(jù)綁定 時(shí),就會(huì)發(fā)現(xiàn)一些奇怪的行為,繼承并不像你想象的那樣工作。子 Scope 的屬性隱藏(覆蓋)了父 Scope 中的同名屬性,對(duì)子 Scope 屬性(表單元素)的更改并不更新父 Scope 屬性的值。這個(gè)行為實(shí)際上不是 AngularJS 特有的,JavaScript 本身的原型鏈就是這樣工作的。開(kāi)發(fā)者通常都沒(méi)有意識(shí)到 ng-repeat, ng-switch, ng-view 和 ng-include 統(tǒng)統(tǒng)都創(chuàng)建了他們新的子 scopes,所以在用到這些 directive 時(shí)也經(jīng)常出問(wèn)題。
二、解決的辦法
解決的辦法就是不使用基本數(shù)據(jù)類(lèi)型,而在 Model 里永遠(yuǎn)多加一個(gè)點(diǎn).
使用
<input type="text" ng-model="someObj.prop1">
來(lái)替代
<input type="text" ng-model="prop1">
是不是很坑爹?下面這個(gè)例子很明確地表達(dá)了我所想表達(dá)的奇葩現(xiàn)象
app.controller('ParentController',function($scope){ $scope.parentPrimitive = "some primitive" $scope.parentObj = {}; $scope.parentObj.parentProperty = "some value"; }); app.controller('ChildController',function($scope){ $scope.parentPrimitive = "this will NOT modify the parent" $scope.parentObj.parentProperty = "this WILL modify the parent"; });
查看 在線演示 DEMO
但是我真的確實(shí)十分很非常需要使用 string number 等原始數(shù)據(jù)類(lèi)型怎么辦呢?2 個(gè)方法——
在子 Scope 中使用 $parent.parentPrimitive。 這將阻止子 Scope 創(chuàng)建它自己的屬性。
在父 Scope 中定義一個(gè)函數(shù),讓子 Scope 調(diào)用,傳遞原始數(shù)據(jù)類(lèi)型的參數(shù)給父親,從而更新父 Scope 中的屬性。(并不總是可行)
三、JavaScript 的原型鏈繼承
吐槽完畢,我們來(lái)深入了解一下 JavaScript 的原型鏈。這很重要,特別是當(dāng)你從服務(wù)器端開(kāi)發(fā)轉(zhuǎn)到前端,你應(yīng)該會(huì)很熟悉經(jīng)典的 Class 類(lèi)繼承,我們來(lái)回顧一下。
假設(shè)父類(lèi) parentScope 有如下成員屬性 aString, aNumber, anArray, anObject, 以及 aFunction。子類(lèi) childScope 原型繼承父類(lèi) parentScope,于是我們有:
如果子 Scope 嘗試去訪問(wèn) parentScope 中定義的屬性,JavaScript 會(huì)先在子 Scope 中查找,如果沒(méi)有該屬性,則找它繼承的 scope 去獲取屬性,如果繼承的原型對(duì)象 parentScope 中都沒(méi)有該屬性,那么繼續(xù)在它的原型中尋找,從原型鏈一直往上直到到達(dá) rootScope。所以,下面的表達(dá)式結(jié)果都是 ture:
childScope.aString === 'parent string' childScope.anArray[1] === 20 childScope.anObject.property1 === 'parent prop1' childScope.aFunction() === 'parent output'
假設(shè)我們執(zhí)行下面的語(yǔ)句
childScope.aString = 'child string'
原型鏈并沒(méi)有被查詢(xún),反而是在 childScope 中增加了一個(gè)新屬性 aString。這個(gè)新屬性隱藏(覆蓋)了 parentScope 中的同名屬性。在下面我們討論 ng-repeat 和 ng-include 時(shí)這個(gè)概念很重要。
假設(shè)我們執(zhí)行這個(gè)操作:
childScope.anArray[1] = '22' childScope.anObject.property1 = 'child prop1'
原型鏈被查詢(xún)了,因?yàn)閷?duì)象 anArray 和 anObject 在 childScope 中沒(méi)有找到。它們?cè)?parentScope 中被找到了,并且值被更新。childScope 中沒(méi)有增加新的屬性,也沒(méi)有任何新的對(duì)象被創(chuàng)建。(注:在 JavaScript 中,array 和 function 都是對(duì)象)
假設(shè)我們執(zhí)行這個(gè)操作:
childScope.anArray = [100, 555] childScope.anObject = { name: 'Mark', country: 'USA' }
原型鏈沒(méi)有被查詢(xún),并且子 Scope 新加入了兩個(gè)新的對(duì)象屬性,它們隱藏(覆蓋)了 parentScope 中的同名對(duì)象屬性。
應(yīng)該可以總結(jié)
如果讀取 childScope.propertyX,并且 childScope 有屬性 propertyX,那么原型鏈沒(méi)有被查詢(xún)。
如果設(shè)置 childScope.propertyX,原型鏈不會(huì)被查詢(xún)。
最后一種情況,
delete childScope.anArray childScope.anArray[1] === 22 // true
我們從 childScope 刪除了屬性,則當(dāng)我們?cè)俅卧L問(wèn)該屬性時(shí),原型鏈會(huì)被查詢(xún)。刪除對(duì)象的屬性會(huì)讓來(lái)自原型鏈中的屬性浮現(xiàn)出來(lái)。
四、AngularJS 的 Scope 繼承
創(chuàng)建新的 Scope,并且原型繼承:ng-repeat, ng-include, ng-switch, ng-view, ng-controller, directive with scope: true, directive with transclude: true
創(chuàng)建新的 Scope,但不繼承:directive with scope: { ... }。它會(huì)創(chuàng)建一個(gè)獨(dú)立 Scope。
注:默認(rèn)情況下 directive 不創(chuàng)建新 Scope,即默認(rèn)參數(shù)是 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 會(huì)生成一個(gè)子 Scope,每個(gè)子 Scope 都繼承父 Scope。
輸入(比如”77″)到第一個(gè) input 文本框,則子 Scope 將獲得一個(gè)新的 myPrimitive 屬性,覆蓋掉父 Scope 的同名屬性。這可能和你預(yù)想的不一樣。
輸入(比如”99″)到第二個(gè) input 文本框,并不會(huì)在子 Scope 創(chuàng)建新的屬性,因?yàn)?tpl2.html 將 model 綁定到了一個(gè)對(duì)象屬性(an object property),原型繼承在這時(shí)發(fā)揮了作用,ngModel 尋找對(duì)象 myObject 并且在它的父 Scope 中找到了。
如果我們不想把 model 從 number 基礎(chǔ)類(lèi)型改為對(duì)象,我們可以用 $parent 改寫(xiě)第一個(gè)模板:
<input ng-model="$parent.myPrimitive">
輸入(比如”22″)到這個(gè)文本框也不會(huì)創(chuàng)建新屬性了。model 被綁定到了父 scope 的屬性上(因?yàn)?$parent 是子 Scope 指向它的父 Scope 的一個(gè)屬性)。
對(duì)于所有的 scope (原型繼承的或者非繼承的),Angular 總是會(huì)通過(guò) Scope 的 $parent, $$childHead 和 $$childTail 屬性記錄父-子關(guān)系(也就是繼承關(guān)系),圖中為簡(jiǎn)化而未畫(huà)出這些屬性。
在沒(méi)有表單元素的情況下,另一種方法是在父 Scope 中定義一個(gè)函數(shù)來(lái)修改基本數(shù)據(jù)類(lèi)型。因?yàn)橛性屠^承,子 Scope 確保能夠調(diào)用這個(gè)函數(shù)。例如,
// 父 Scope 中 $scope.setMyPrimitive = function(value) { $scope.myPrimitive = value;
ng-switch
ng-switch 的原型繼承和 ng-include 一樣。所以如果你需要對(duì)基本類(lèi)型數(shù)據(jù)進(jìn)行雙向綁定,使用 $parent,或者將其改為 object 對(duì)象并綁定到對(duì)象的屬性,防止子 Scope 覆蓋父 Scope 的屬性。
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"> </li> <ul> <ul> <li ng-repeat="obj in myArrayOfObjects"> <input ng-model="obj.num"> </li> <ul>
對(duì)于每一個(gè) Item,ng-repeat 創(chuàng)建新的 Scope,每一個(gè) Scope 都繼承父 Scope,但同時(shí) item 的值也被賦給了新 Scope 的新屬性(新屬性的名字為循環(huán)的變量名)。Angular ng-repeat 的源碼實(shí)際上是這樣的:
childScope = scope.$new(); // 子 scope 原型繼承父 scope ... childScope[valueIdent] = value; // 創(chuàng)建新的 childScope 屬性
如果 item 是一個(gè)基礎(chǔ)數(shù)據(jù)類(lèi)型(就像 myArrayOfPrimitives),本質(zhì)上它的值被復(fù)制了一份賦給了新的子 scope 屬性。改變這個(gè)子 scope 屬性值(比如用 ng-model,即 num)不會(huì)改變父 scope 引用的 array。所以上面第一個(gè) ng-repeat 里每一個(gè)子 scope 獲得的 num 屬性獨(dú)立于 myArrayOfPrimitives 數(shù)組:
這樣的 ng-repeat 和你預(yù)想中的不一樣。在 Angular 1.0.2 及更早的版本,向文本框中輸入會(huì)改變灰色格子的值,它們只在子 Scope 中可見(jiàn)。Angular 1.0.3+ 以后,輸入文本不會(huì)再有任何作用了。
我們希望的是輸入能改變 myArrayOfPrimitives 數(shù)組,而不是子 Scope 里的屬性。為此我們必須將 model 改為一個(gè)關(guān)于對(duì)象的數(shù)組(array of objects)。
所以如果 item 是一個(gè)對(duì)象,則對(duì)于原對(duì)象的一個(gè)引用(而非拷貝)被賦給了新的子 Scope 屬性。改變子 Scope 屬性的值(使用 ng-model,即 obj.num)也就改變了父 Scope 所引用的對(duì)象。所以上面第二個(gè) ng-repeat 可表示為:
這才是我們想要的。輸入到文本框即會(huì)改變灰色格子的值,該值在父 Scope 和子 Scope 均可見(jiàn)。
ng-controller
使用 ng-controller 進(jìn)行嵌套,結(jié)果和 ng-include 和 ng-switch 一樣是正常的原型繼承。所以做法也一樣不再贅述。然而“兩個(gè) controller 使用 $scope 繼承來(lái)共享信息被認(rèn)為是不好的做法”
應(yīng)該使用 service 在 controller 間共享數(shù)據(jù)。
如果你確實(shí)要通過(guò)繼承來(lái)共享數(shù)據(jù),那么也沒(méi)什么特殊要做的,子 Scope 可以直接訪問(wèn)所有父 Scope 的屬性。
directives
這個(gè)要分情況來(lái)討論。
默認(rèn) scope: false – directive 不會(huì)創(chuàng)建新的 Scope,所以沒(méi)有原型繼承。這看上去很簡(jiǎn)單,但也很危險(xiǎn),因?yàn)槟銜?huì)以為 directive 在 Scope 中創(chuàng)建了一個(gè)新的屬性,而實(shí)際上它只是用到了一個(gè)已存在的屬性。這對(duì)編寫(xiě)可復(fù)用的模塊和組件來(lái)說(shuō)并不好。
scope: true – 這時(shí) directive 會(huì)創(chuàng)建一個(gè)新的子 scope 并繼承父 scope。如果在同一個(gè) DOM 節(jié)點(diǎn)上有多個(gè) directive 都要?jiǎng)?chuàng)建新 scope,則只有一個(gè)新 Scope 會(huì)創(chuàng)建。因?yàn)橛姓5脑屠^承,所以和 ng-include, ng-switch 一樣要注意基礎(chǔ)類(lèi)型數(shù)據(jù)的雙向綁定,子 Scope 屬性會(huì)覆蓋父 Scope 同名屬性。
scope: { ... } – 這時(shí) directive 創(chuàng)建一個(gè)獨(dú)立的 scope,沒(méi)有原型繼承。這在編寫(xiě)可復(fù)用的模塊和組件時(shí)是比較好的選擇,因?yàn)?directive 不會(huì)不小心讀寫(xiě)父 scope。然而,有時(shí)候這類(lèi) directives 又經(jīng)常需要訪問(wèn)父 scope 的屬性。對(duì)象散列(object hash)被用來(lái)建立這個(gè)獨(dú)立 Scope 與父 Scope 間的雙向綁定(使用 ‘=')或單向綁定(使用 ‘@')。還有一個(gè) ‘&' 用來(lái)綁定父 Scope 的表達(dá)式。這些統(tǒng)統(tǒng)從父 Scope 派生創(chuàng)建出本地的 Scope 屬性。注意,HTML 屬性被用來(lái)建立綁定,你無(wú)法在對(duì)象散列中引用父 Scope 的屬性名,你必須使用一個(gè) HTML 屬性。例如,<div my-directive> 和 scope: { localProp: '@parentProp' } 是無(wú)法綁定父屬性 parentProp 到獨(dú)立 scope的,你必須這樣指定: <div my-directive the-Parent-Prop=parentProp> 以及 scope: { localProp: '@theParentProp' }。獨(dú)立的 scope 中 __proto__ 引用了一個(gè) Scope 對(duì)象(下圖中的桔黃色 Object),獨(dú)立 scope 的 $parent 指向父 scope,所以盡管它是獨(dú)立的而且沒(méi)有從父 Scope 原型繼承,它仍然是一個(gè)子 scope。
下面的圖中,我們有 <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2"> 和 scope:
{ interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }。
同時(shí),假設(shè) directive 在它的 link 函數(shù)里做了 scope.someIsolateProp = "I'm isolated"
注意:在 link 函數(shù)中使用 attrs.$observe('attr_name', function(value) { ... } 來(lái)獲取獨(dú)立 Scope 用 ‘@' 符號(hào)替換的屬性值。例如,在 link 函數(shù)中有 attrs.$observe('interpolated', function(value) { ... } 值將被設(shè)為 11. (scope.interpolatedProp 在 link 函數(shù)中是 undefined,相反scope.twowayBindingProp 在 link 函數(shù)中定義了,因?yàn)橛昧?‘=' 符號(hào))
transclude: true – 這時(shí) directive 創(chuàng)建了一個(gè)新的 “transcluded” 子 scope,同時(shí)繼承父 scope。所以如果模板片段中的內(nèi)容(例如那些將要替代 ng-transclude 的內(nèi)容)要求對(duì)父 Scope 的基本類(lèi)型數(shù)據(jù)進(jìn)行雙向綁定,使用 $parent,或者將 model 一個(gè)對(duì)象的屬性,防止子 Scope 屬性覆蓋父 Scope 屬性。
transcluded 和獨(dú)立 scope (如果有)是兄弟關(guān)系,每個(gè) Scope 的 $parent 指向同一個(gè)父 Scope。當(dāng)模板中的 scope 和獨(dú)立 Scope 同時(shí)存在,獨(dú)立 Scope 屬性 $$nextSibling 將會(huì)指向模板中的 Scope。
在下圖中,假設(shè) directive 和上個(gè)圖一樣,只是多了 transclude: true
查看 在線 DEMO,例子里有一個(gè) showScope() 函數(shù)可以用來(lái)檢查獨(dú)立 Scope 和它關(guān)聯(lián)的 transcluded scope。
總結(jié)
一共有四種 Scope:
普通進(jìn)行原型繼承的 Scope —— ng-include, ng-switch, ng-controller, directive with scope: true
普通原型繼承的 Scope 但拷貝賦值 —— ng-repeat。 每個(gè) ng-repeat 的循環(huán)都創(chuàng)建新的子 Scope,并且子 Scope 總是獲得新的屬性。
獨(dú)立的 isolate scope —— directive with scope: {...}。它不是原型繼承,但 ‘=', ‘@' 和 ‘&' 提供了訪問(wèn)父 Scope 屬性的機(jī)制。
transcluded scope —— directive with transclude: true。它也遵循原型繼承,但它同時(shí)是任何 isolate scope 的兄弟。
對(duì)于所有的 Scope,Angular 總是會(huì)通過(guò) Scope 的 $parent, $$childHead 和 $$childTail 屬性記錄父-子關(guān)系。
PS:scope和rootscope的區(qū)別
scope是html和單個(gè)controller之間的橋梁,數(shù)據(jù)綁定就靠他了。rootscope是各個(gè)controller中scope的橋梁。用rootscope定義的值,可以在各個(gè)controller中使用。下面用實(shí)例詳細(xì)的說(shuō)明一下。
1,js代碼
phonecatApp.controller('TestCtrl',['$scope','$rootScope', function($scope,$rootScope) { $rootScope.name = 'this is test'; } ]); phonecatApp.controller('Test111Ctrl',['$scope','$rootScope', function($scope,$rootScope) { $scope.name = $rootScope.name; } ]);
2,html代碼
<div ng-controller="TestCtrl"> I set the global variable.<strong>{{$root.name}}</strong> </div> <div ng-controller="Test111Ctrl"> 1,get global variable .<strong>{{name}}</strong><br> 2,get global variable .<strong>{{$root.name}}</strong> </div>
3,顯示結(jié)果
I set the global variable.this is test 1,get global variable .this is test 2,get global variable .this is test
由結(jié)果可以看出來(lái),$rootScope.name設(shè)置的變量,在所有controller里面都是可以直接用{{$root.name}}來(lái)顯示的,很強(qiáng)大。那當(dāng)然也可以賦值給scope.
- AngularJS指令與控制器之間的交互功能示例
- AngularJS中directive指令使用之事件綁定與指令交互用法示例
- AngularJS 指令的交互詳解及實(shí)例代碼
- AngularJS實(shí)現(xiàn)與Java Web服務(wù)器交互操作示例【附demo源碼下載】
- AngularJS入門(mén)教程之與服務(wù)器(Ajax)交互操作示例【附完整demo源碼下載】
- 詳解AngularJs中$resource和restfu服務(wù)端數(shù)據(jù)交互
- angularjs實(shí)現(xiàn)與服務(wù)器交互分享
- 關(guān)于angularJs指令的Scope(作用域)介紹
- 淺談AngularJs指令之scope屬性詳解
- angularJS 中$scope方法使用指南
- 深入解析AngularJS框架中$scope的作用與生命周期
- AngularJs Scope詳解及示例代碼
- AngularJS指令與指令之間的交互功能示例
相關(guān)文章
Angular2 PrimeNG分頁(yè)模塊學(xué)習(xí)
這篇文章主要為大家詳細(xì)介紹了Angular2 PrimeNG分頁(yè)模塊學(xué)習(xí)教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01AngularJS 入門(mén)教程之HTML DOM實(shí)例詳解
本文主要介紹AngularJS HTML DOM,這里幫大家整理了詳細(xì)的資料,并附實(shí)例代碼詳細(xì)講解,有需要的小伙伴可以參考下2016-07-07AngularJS實(shí)現(xiàn)按鈕提示與點(diǎn)擊變色效果
這篇文章給大家介紹了如何利用AngularJS實(shí)現(xiàn)按鈕提示與點(diǎn)擊變色的效果,文中提供了實(shí)例代碼,對(duì)大家學(xué)習(xí)AngularJS具有一定的參考借鑒價(jià)值,下面來(lái)一起看看吧。2016-09-09AngularJS constant和value區(qū)別詳解
angularJS可以通過(guò)constant(name,value)和value(name,value)對(duì)于創(chuàng)建服務(wù)也是很重要的。他們之間有什么不同呢?今天小編給大家分享AngularJS constant和value區(qū)別詳解,需要的朋友參考下2017-02-02AngularJS實(shí)現(xiàn)的回到頂部指令功能實(shí)例
這篇文章主要介紹了AngularJS實(shí)現(xiàn)的回到頂部指令功能,結(jié)合實(shí)例形式分析了AngularJS返回到頂部功能的具體步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-05-05淺談Angular2 ng-content 指令在組件中嵌入內(nèi)容
本篇文章主要介紹了淺談Angular2 ng-content 指令在組件中嵌入內(nèi)容,具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08AngularJS基礎(chǔ) ng-model 指令詳解及示例代碼
本文主要介紹AngularJS ng-model 指令,這里幫大家整理了ng-model的基本資料,并附有示例代碼,有需要的小伙伴可以參考下2016-08-08使用angular寫(xiě)一個(gè)hello world
這篇文章主要介紹了使用angular寫(xiě)一個(gè)hello world的方法及示例,需要的朋友可以參考下2015-01-01AngularJS深入探討scope,繼承結(jié)構(gòu),事件系統(tǒng)和生命周期
這篇文章主要介紹了AngularJS的scope,繼承結(jié)構(gòu),事件系統(tǒng)和生命周期,較為詳細(xì)的分析了scope的作用域、層次結(jié)構(gòu)、繼承及生命周期相關(guān)概念與使用技巧,需要的朋友可以參考下2016-11-11