简介
在angularjs中,子作用域通常会原型继承于其父作用域。有一个例外是当指令使用scope:{…}来定义–这创建了一个没有原型继承的”独立”作用域,这会在创建”可重复使用的组件”的指令时经常使用。如果你设置了scope:true,这个指令会使用原型继承。
通常情况下作用域继承非常直白,你甚至不需要知道它在发生什么,直到在一个定义在父作用域上原始类型(例如:number,string,boolean)在子作用域中使用了双向数据绑定(即表单元素,ng-model)。这并不会像大多数人期望的那样工作,而是子作用域得到了它自己的属性,从而覆盖了父作用域上的同名属性。这不是angularjs做的事情-这是JavaScript的原型继承其作用了。新入门的angularjs开发者通常情况下不会意识到ng-repeat、ng-switch、ng-view和ng-include都创建了新的子作用域,所以当使用这些指令的时候,经常会有这种问题发生。
关于原始类型的这个问题通过下面的这个最佳建议最容易避免:在你的模型中始终使用’
在模型中使用’.’,会确保原型继承始终发生,所以,使用代码
1 | <input type="text" ng-model="someObj.prop1"/> |
如果你必须要使用原始类型,有以下两种解决方法:
1、在子作用域使用$parent.parentScopeProperty.这会阻止子作用域创建自己的属性。
2、在父作用域上定义一个函数,在子作用域上调用,通过该函数传递父作用域上的原始值。
JavaScript原型继承
首先我们应该对JavaScript原型继承有一个深入的了解很有必要,尤其你具有服务器端开发的背景,并且对于传统的继承很熟悉。让我们先来复习一下。
假设parentScope具有如下属性,aString,aNumber,anArray,anObject和一个aFunction.如果childScope原型继承于parentScope,如下:
(注意:为了节省空间,我把anArray展示成一个蓝色的三个值的对象,而不是一个蓝色的拥有三个分离的灰色的对象)
如果我们在childScope上获取parentScope上定义的对象,JavaScript会首先在childscope上查找,没有找到该属性,查找其继承的scope,找到这个属性(如果在parentScope上没有找到该属性,会继续查找原型链…直到到达rootScope)。所以,以下全都为真。
1 | childScope.aString === 'parent string' |
假设我们有如下代码:1
childScope.aString = 'child string'
原型链并没有被遍历,一个新的属性会被添加到childScope上。同时,这个新属性隐藏了和parentScope具有同样名称的属性。这对我们下面讨论ng-repeat和ng-include非常重要
假设我们又做了如下操作
1 | childScope.anArray[1]=22 |
原型链被访问了,因为对象(anArray和anObject)在childScope中没有被找到。这两个对象在parentScope中被找到,属性的值在原对象上别更新了。childScope上不会增加新的属性,没有新的对象创建(在JavaScript中数组和函数同样是对象)。
假设我们做如下操作:
1 | childScope.anArray = [100,555] |
原型链不会被访问,childScope会创建两个新的对象属性,隐藏了和parentScope具有相同名称的属性。
重要结论:
1、如果我们读取childScope的某个属性childScope.propertyX,并且childScope具有属性propertyX,那么原型链不会被访问。
2、如果我们设置childScope的某个属性propertyX,那么原型链不会被访问。
最后一个场景:
1 | delete childScope.anArray |
首先我们删除了childScope的属性anArray,然后我们尝试再次去获得该属性,原型链被访问了。
这个jsfiddle中你可看到javascript原型继承的例子和结果(打开你的浏览器的控制台查看输出)
angular scope继承
1、如下的指令创建了新的scope,并且基于原型继承:ng-repeat,ng-include,ng-switch,ng-controller,使用scope:true的指令,使用transclude:true指令。
2、如下的指令创建了新的scope,并且没有基于原型继承:使用scope:{…}的指令,这创建了”孤立”的scope.
ng-include
假设我们的controller中的代码如下:
1 | $scope.myPrimitive = 50; |
html如下:
1 | <script type="text/ng-template" id="/tpl1.html"> |
每一个ng-include生成了一个新的基于其父作用域原型继承的子作用域。
修改第一个textbox中的值’77’会导致子作用域创建一个新的myPrimitive属性,并且隐藏了父作用域的同名属性,这可能不是你所希望的。
修改第二个textbox的值为’99’不会导致创建一个新的子属性。因为tpl2.html绑定了一个对象的属性当ngModel查找对象myObject时原型继承起作用了,最终在parentScope中找到了该属性。
如果不想将model从原始类型改为对象类型的话,我们可以使用$parent来重写一个模板。
1 | <input ng-model="$parent.myprimitive"/> |
这次修改第一个textbox的值不会导致生成一个新的子属性。模型现在绑定了parentScope中的属性(因为$parent是子作用域指向父作用域的一个引用)。
对于所有作用域(不管是否是原型继承),angular会通过作用域上的属性
\$parent指向scope的父作用域
\$\$childHeader指向scope的第一个子作用域
\$\$childTail指向scope的最后一个子作用域
\$\$nextSibling指向scope的下一个相邻作用域
\$\$prevSibling指向scope的上一个相邻作用域
这些关系用于AngularJS内部历遍,如\$broadcast和\$emit事件广播,\$digest处理等。
对于不涉及表单元素的情况,另一个解决方案是在父作用域中定义一个函数来修改原始数据类型。子作用域总是调用这个函数,由于原型继承子作用域能够访问到该函数。例如:
1 | //in the parent scope |
这是一个使用了”parent function”的简单的jsfiddle
ng-switch
ng-switch scope 的继承和ng-include类似。因此如果你需要双向数据绑定到父作用域中的一个原始数据类型上,使用$parent或者将model改为对象的某个属性。这会避免子作用域隐藏了父作用域的属性。
ng-repeat
ng-repeat 和以上指令有点差别。假设我们的controller如下:
1 | $scope.myArrayOfPrimitives = [11,22]; |
html如下:
1 | <ul><li ng-repeat="num in myArrayOfPrimitives"> |
对于每次 item的iteration,ng-repeate创建了一个从父作用域原型继承的新的作用域,但是它也将item的值分配给新的子作用域上的一个新的属性(新的属性的名称是循环变量的名称)。如下是ng-repeate的源代码。
1 | childScope = scope.$new(); // child scope prototypically inherits from parent scope ... |
如果item是一个原始类型(例如上面的myArrayOfPrimitives),本质上该值的一个拷贝被分配给新的子scope。改变了子scope的属性值(即使用ng-model、也就是子scope属性num)并没有改变父scope引用的数组。所以,在上面第一个ng-repeate,每一个子scope会得到一个独立于myArrayOfPrimitives 的num属性。
因此这个ng-repeat不会像你希望的那样工作。在Angular1.0.2(包含)以前,修改textbox的值会改变上图中灰色框的值,并且只在child scope中可见。在Angular 1.0.3以上,修改textbox的值不会有任何影响(参考Artem在Stack Overflow的解释)(此处说法有点不太准确,在较新的Angular版本中,修改textbox的值会改变图中灰色框中的值--译者注)。我们所希望的是修改input的值能够改变数组myArrayOfPrimitives,而不是子scope的一个原始类型的属性。为了达到这个目的,我们需要将模型改为对象的数组(见第2个例子)。
因此,如果item是一个对象,原始对象的引用(非拷贝)会被分配成为新的子scope上的属性。修改子scope的属性值(例如,使用ng-model,obj.num)会修改父scope上的值。在上面的第二个ng-repeat中,我们有如下结论:
(注意图中的灰线,能清楚的看到发生了什么)
按照预期工作了。修改textbox的值改变了灰色框中的值,同时对子作用域和父作用域都可见。
ng-view
和ng-include类似
ng-controller
和ng-include、ng-switch的原理一致,使用ng-controller的嵌套的控制器会引起正常的原型继承。然而,“不建议在两个控制器中通过$scope的继承关系来共享信息“
在控制器中共享数据应该使用服务。
指令
1、默认(scope:false)-指令没有创建任何新的作用域,因此不存在任何的原型继承。这很简单,但是同样存在隐患,例如:一个指令可能以为它在作用域上创建了一个新的属性,但实际上它修改了一个现有的属性的值。这对于书写可重复使用的组件来说并不是一个好的选择。
2、scope: true-指令创建了一个从父作用域基于原型继承的子作用域。如果在同一个DOM上有多个指令需要创建新的作用域,那么只有一个新的子作用域会被创建。既然有“正常“的原型继承,和ng-include 、ng-switch类似,警惕在父作用域上的原始数据类型的双向数据绑定,子作用域会覆盖掉父作用域上的属性。
3、scope: { … }-指令创建了一个新的独立作用域。并且没有原型继承。当你创建可以复用的组件时这是一个好的选择,因为指令不能够直接读取或修改父作用域。然而,通常这种指令需要读取父作用域的某些属性。该对象可以在父作用域和独立作用域上使用“=“创建双向数据绑定,使用“@“创建单向绑定(父作用域改变会影响子作用域,子作用域改变并不会影响父作用域--译者注)。也可以使用“&“绑定父作用域上的表达式。所以,这些方法同样给子作用域创建了从父作用域衍生的属性。注意这些属性被用来帮助设置绑定--在对象中你不能直接引用父作用域的属性名称,你需要使用一个HTML属性。例如:如下,你想要在独立作用域上绑定父作用域的属性parentProp将不会起作用:代码
如下图片中:我们有代码
1 | <my-directive interpolated="{{parentProp1}}" twoway-binding="parentProp2"> 和 |

最后注意:使用link函数中attrs.$observe(‘attr_name’, function(value) { … })来得到独立作用域中使用‘@‘绑定的属性的值。例如:在link函数中有代码–attrs.$observe(‘interpolated’, function(value) { … }) – value会被设置为11。(scope.interpolatedProp在link函数中没有定义(该文章写的时间较早,译者通过测试Angular1.4.7发现在该版本中,这个属性已经有定义了,值为11)。而scope.twowayBindingProp有定义,因为他使用了‘=‘ )。
4、transclude: true-指令创建了一个新的 “transcluded” 子作用域,并且原型继承于父作用域。因此,如果你的嵌入的内容(即ng-transclude将被替换的内容)需要双向数据绑定到父作用域上的一个原始类型上,使用$parent,或者将模型改为对象,绑定到改对象的某个属性上。这会避免子作用域覆盖父作用域的属性。
内嵌作用域和独立作用域是同胞的--每个scope的\$parent属性指向同一个父作用域。当内嵌作用域和独立作用域同时存在,独立作用域的\$\$nextSibling 属性会指向内嵌作用域。
假设上面的指令增加了属性transclude: true ,scope的示意图如下:
这个jsfiddle有一个用来检查独立作用域和他相关的内嵌作用域的showScope()函数。参考该fiddle中的注释中的说明。
总结
有四种类型的作用域:
1、普通原型继承作用域--ng-include、ng-switch、ng-controller和使用scope: true定义的指令
2、含有拷贝属性的普通原型继承作用域--ng-repeat。每次迭代ng-repeat都会创建一个新的子作用域,同时新的子作用域会得到一个新的属性。
3、 独立作用域--使用scope: {…}定义的指令。这次没有原型继承,但是 ‘=’, ‘@’, and ‘&’提供了一种通过HTML属性获取父作用域属性的机制。
4、内嵌作用域--使用transclude: true定义的指令。这次依旧是正常的基于原型的继承,但是同时他也是任意独立作用域的兄弟作用域。