深入探究AngularJS框架中Scope对象的超级教程
一、遇到的问题
问题发生在使用 angularjs 嵌套 controller 的时候。因为每个 controller 都有它对应的 scope(相当于作用域、控制范围),所以 controller 的嵌套,也就意味着 scope 的嵌套。这个时候如果两个 scope 内都有同名的 model 会发生什么呢?从子 scope 怎样更新父 scope 里的 model 呢?
这个问题很典型,比方说当前页面是一个产品列表,那么就需要定义一个 productlistcontroller
function productlistcontroller($scope, $http) { $http.get('/api/products.json') .success(function(data){ $scope.productlist = data; }); $scope.selectedproduct = {}; }
你大概看到了在 scope 里还定义了一个 selectedproduct 的 model,表示选中了某一个产品。这时会获取该产品详情,而页面通过 angularjs 中的 $routeprovider 自动更新,拉取新的详情页模板,模板中有一个 productdetailcontroller
function productdetailcontroller($scope, $http, $routeparams) { $http.get('/api/products/'+$routeparams.productid+'.json') .success(function(data){ $scope.selectedproduct = data; }); }
有趣的事情发生了,在这里也有一个 selectedproduct ,它会怎样影响 productlistcontroller 中的 selectedproduct 呢?
答案是没有影响。在 anuglarjs 里子 scope 确实会继承父 scope 中的对象,但当你试下对基本数据类型(string, number, boolean)的 双向数据绑定 时,就会发现一些奇怪的行为,继承并不像你想象的那样工作。子 scope 的属性隐藏(覆盖)了父 scope 中的同名属性,对子 scope 属性(表单元素)的更改并不更新父 scope 属性的值。这个行为实际上不是 angularjs 特有的,javascript 本身的原型链就是这样工作的。开发者通常都没有意识到 ng-repeat, ng-switch, ng-view 和 ng-include 统统都创建了他们新的子 scopes,所以在用到这些 directive 时也经常出问题。
二、解决的办法
解决的办法就是不使用基本数据类型,而在 model 里永远多加一个点.
使用
<input type="text" ng-model="someobj.prop1">
来替代
<input type="text" ng-model="prop1">
是不是很坑爹?下面这个例子很明确地表达了我所想表达的奇葩现象
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
但是我真的确实十分很非常需要使用 string number 等原始数据类型怎么办呢?2 个方法——
在子 scope 中使用 $parent.parentprimitive。 这将阻止子 scope 创建它自己的属性。
在父 scope 中定义一个函数,让子 scope 调用,传递原始数据类型的参数给父亲,从而更新父 scope 中的属性。(并不总是可行)
三、javascript 的原型链继承
吐槽完毕,我们来深入了解一下 javascript 的原型链。这很重要,特别是当你从服务器端开发转到前端,你应该会很熟悉经典的 class 类继承,我们来回顾一下。
假设父类 parentscope 有如下成员属性 astring, anumber, anarray, anobject, 以及 afunction。子类 childscope 原型继承父类 parentscope,于是我们有:
如果子 scope 尝试去访问 parentscope 中定义的属性,javascript 会先在子 scope 中查找,如果没有该属性,则找它继承的 scope 去获取属性,如果继承的原型对象 parentscope 中都没有该属性,那么继续在它的原型中寻找,从原型链一直往上直到到达 rootscope。所以,下面的表达式结果都是 ture:
childscope.astring === 'parent string' childscope.anarray[1] === 20 childscope.anobject.property1 === 'parent prop1' childscope.afunction() === 'parent output'
假设我们执行下面的语句
childscope.astring = 'child string'
原型链并没有被查询,反而是在 childscope 中增加了一个新属性 astring。这个新属性隐藏(覆盖)了 parentscope 中的同名属性。在下面我们讨论 ng-repeat 和 ng-include 时这个概念很重要。
假设我们执行这个操作:
childscope.anarray[1] = '22' childscope.anobject.property1 = 'child prop1'
原型链被查询了,因为对象 anarray 和 anobject 在 childscope 中没有找到。它们在 parentscope 中被找到了,并且值被更新。childscope 中没有增加新的属性,也没有任何新的对象被创建。(注:在 javascript 中,array 和 function 都是对象)
假设我们执行这个操作:
childscope.anarray = [100, 555] childscope.anobject = { name: 'mark', country: 'usa' }
原型链没有被查询,并且子 scope 新加入了两个新的对象属性,它们隐藏(覆盖)了 parentscope 中的同名对象属性。
应该可以总结
如果读取 childscope.propertyx,并且 childscope 有属性 propertyx,那么原型链没有被查询。
如果设置 childscope.propertyx,原型链不会被查询。
最后一种情况,
delete childscope.anarray childscope.anarray[1] === 22 // true
我们从 childscope 删除了属性,则当我们再次访问该属性时,原型链会被查询。删除对象的属性会让来自原型链中的属性浮现出来。
四、angularjs 的 scope 继承
创建新的 scope,并且原型继承:ng-repeat, ng-include, ng-switch, ng-view, ng-controller, directive with scope: true, directive with transclude: true
创建新的 scope,但不继承:directive with scope: { ... }。它会创建一个独立 scope。
注:默认情况下 directive 不创建新 scope,即默认参数是 scope: false。
ng-include
假设在我们的 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>
每一个 ng-include 会生成一个子 scope,每个子 scope 都继承父 scope。
输入(比如”77″)到第一个 input 文本框,则子 scope 将获得一个新的 myprimitive 属性,覆盖掉父 scope 的同名属性。这可能和你预想的不一样。
输入(比如”99″)到第二个 input 文本框,并不会在子 scope 创建新的属性,因为 tpl2.html 将 model 绑定到了一个对象属性(an object property),原型继承在这时发挥了作用,ngmodel 寻找对象 myobject 并且在它的父 scope 中找到了。
如果我们不想把 model 从 number 基础类型改为对象,我们可以用 $parent 改写第一个模板:
<input ng-model="$parent.myprimitive">
输入(比如”22″)到这个文本框也不会创建新属性了。model 被绑定到了父 scope 的属性上(因为 $parent 是子 scope 指向它的父 scope 的一个属性)。
对于所有的 scope (原型继承的或者非继承的),angular 总是会通过 scope 的 $parent, $$childhead 和 $$childtail 属性记录父-子关系(也就是继承关系),图中为简化而未画出这些属性。
在没有表单元素的情况下,另一种方法是在父 scope 中定义一个函数来修改基本数据类型。因为有原型继承,子 scope 确保能够调用这个函数。例如,
// 父 scope 中 $scope.setmyprimitive = function(value) { $scope.myprimitive = value;
ng-switch
ng-switch 的原型继承和 ng-include 一样。所以如果你需要对基本类型数据进行双向绑定,使用 $parent,或者将其改为 object 对象并绑定到对象的属性,防止子 scope 覆盖父 scope 的属性。
ng-repeat
ng-repeat 有一点不一样。假设在我们的 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>
对于每一个 item,ng-repeat 创建新的 scope,每一个 scope 都继承父 scope,但同时 item 的值也被赋给了新 scope 的新属性(新属性的名字为循环的变量名)。angular ng-repeat 的源码实际上是这样的:
childscope = scope.$new(); // 子 scope 原型继承父 scope ... childscope[valueident] = value; // 创建新的 childscope 属性
如果 item 是一个基础数据类型(就像 myarrayofprimitives),本质上它的值被复制了一份赋给了新的子 scope 属性。改变这个子 scope 属性值(比如用 ng-model,即 num)不会改变父 scope 引用的 array。所以上面第一个 ng-repeat 里每一个子 scope 获得的 num 属性独立于 myarrayofprimitives 数组:
这样的 ng-repeat 和你预想中的不一样。在 angular 1.0.2 及更早的版本,向文本框中输入会改变灰色格子的值,它们只在子 scope 中可见。angular 1.0.3+ 以后,输入文本不会再有任何作用了。
我们希望的是输入能改变 myarrayofprimitives 数组,而不是子 scope 里的属性。为此我们必须将 model 改为一个关于对象的数组(array of objects)。
所以如果 item 是一个对象,则对于原对象的一个引用(而非拷贝)被赋给了新的子 scope 属性。改变子 scope 属性的值(使用 ng-model,即 obj.num)也就改变了父 scope 所引用的对象。所以上面第二个 ng-repeat 可表示为:
这才是我们想要的。输入到文本框即会改变灰色格子的值,该值在父 scope 和子 scope 均可见。
ng-controller
使用 ng-controller 进行嵌套,结果和 ng-include 和 ng-switch 一样是正常的原型继承。所以做法也一样不再赘述。然而“两个 controller 使用 $scope 继承来共享信息被认为是不好的做法”
应该使用 service 在 controller 间共享数据。
如果你确实要通过继承来共享数据,那么也没什么特殊要做的,子 scope 可以直接访问所有父 scope 的属性。
directives
这个要分情况来讨论。
默认 scope: false – directive 不会创建新的 scope,所以没有原型继承。这看上去很简单,但也很危险,因为你会以为 directive 在 scope 中创建了一个新的属性,而实际上它只是用到了一个已存在的属性。这对编写可复用的模块和组件来说并不好。
scope: true – 这时 directive 会创建一个新的子 scope 并继承父 scope。如果在同一个 dom 节点上有多个 directive 都要创建新 scope,则只有一个新 scope 会创建。因为有正常的原型继承,所以和 ng-include, ng-switch 一样要注意基础类型数据的双向绑定,子 scope 属性会覆盖父 scope 同名属性。
scope: { ... } – 这时 directive 创建一个独立的 scope,没有原型继承。这在编写可复用的模块和组件时是比较好的选择,因为 directive 不会不小心读写父 scope。然而,有时候这类 directives 又经常需要访问父 scope 的属性。对象散列(object hash)被用来建立这个独立 scope 与父 scope 间的双向绑定(使用 ‘=')或单向绑定(使用 ‘@')。还有一个 ‘&' 用来绑定父 scope 的表达式。这些统统从父 scope 派生创建出本地的 scope 属性。注意,html 属性被用来建立绑定,你无法在对象散列中引用父 scope 的属性名,你必须使用一个 html 属性。例如,<div my-directive> 和 scope: { localprop: '@parentprop' } 是无法绑定父属性 parentprop 到独立 scope的,你必须这样指定: <div my-directive the-parent-prop=parentprop> 以及 scope: { localprop: '@theparentprop' }。独立的 scope 中 __proto__ 引用了一个 scope 对象(下图中的桔黄色 object),独立 scope 的 $parent 指向父 scope,所以尽管它是独立的而且没有从父 scope 原型继承,它仍然是一个子 scope。
下面的图中,我们有 <my-directive interpolated="{{parentprop1}}" twowaybinding="parentprop2"> 和 scope:
{ interpolatedprop: '@interpolated', twowaybindingprop: '=twowaybinding' }。
同时,假设 directive 在它的 link 函数里做了 scope.someisolateprop = "i'm isolated"
注意:在 link 函数中使用 attrs.$observe('attr_name', function(value) { ... } 来获取独立 scope 用 ‘@' 符号替换的属性值。例如,在 link 函数中有 attrs.$observe('interpolated', function(value) { ... } 值将被设为 11. (scope.interpolatedprop 在 link 函数中是 undefined,相反scope.twowaybindingprop 在 link 函数中定义了,因为用了 ‘=' 符号)
transclude: true – 这时 directive 创建了一个新的 “transcluded” 子 scope,同时继承父 scope。所以如果模板片段中的内容(例如那些将要替代 ng-transclude 的内容)要求对父 scope 的基本类型数据进行双向绑定,使用 $parent,或者将 model 一个对象的属性,防止子 scope 属性覆盖父 scope 属性。
transcluded 和独立 scope (如果有)是兄弟关系,每个 scope 的 $parent 指向同一个父 scope。当模板中的 scope 和独立 scope 同时存在,独立 scope 属性 $$nextsibling 将会指向模板中的 scope。
在下图中,假设 directive 和上个图一样,只是多了 transclude: true
查看 在线 demo,例子里有一个 showscope() 函数可以用来检查独立 scope 和它关联的 transcluded scope。
总结
一共有四种 scope:
普通进行原型继承的 scope —— ng-include, ng-switch, ng-controller, directive with scope: true
普通原型继承的 scope 但拷贝赋值 —— ng-repeat。 每个 ng-repeat 的循环都创建新的子 scope,并且子 scope 总是获得新的属性。
独立的 isolate scope —— directive with scope: {...}。它不是原型继承,但 ‘=', ‘@' 和 ‘&' 提供了访问父 scope 属性的机制。
transcluded scope —— directive with transclude: true。它也遵循原型继承,但它同时是任何 isolate scope 的兄弟。
对于所有的 scope,angular 总是会通过 scope 的 $parent, $$childhead 和 $$childtail 属性记录父-子关系。
ps:scope和rootscope的区别
scope是html和单个controller之间的桥梁,数据绑定就靠他了。rootscope是各个controller中scope的桥梁。用rootscope定义的值,可以在各个controller中使用。下面用实例详细的说明一下。
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,显示结果
i set the global variable.this is test 1,get global variable .this is test 2,get global variable .this is test
由结果可以看出来,$rootscope.name设置的变量,在所有controller里面都是可以直接用{{$root.name}}来显示的,很强大。那当然也可以赋值给scope.