在AngularJS中,子作用域通常会原型继承于父作用域。这种情况的唯一例外是当一个指令设置了scope:{ ... } -- 这会创建一个孤立的作用域,该作用域不会进行原型继承。这种设置通常用于创建可复用组件。在指令中,默认情况下直接使用父作用域,这意味着,你在指令中作的任何改动都会同时改变父作用域。如果你设置scope:true(而不是scope:{ ... }),那么该指令会进行原型继承。
一般来说,作用域继承是很简单的,通常你甚至不需要知道它正在运作...直到你试图从子作用域中对父级作用域的基本类型数据(比如,数字,字符串,布尔值)进行数据双向绑定(即表单元素,ng-model指令)。这种做法通常不会符合我们的预期。这是因为子作用域会创建自身的属性,从而隐藏/遮蔽了父级作用域的同名属性。这种特性是JavaScript原型链运作原理,而不是AngularJS本身实现造成的。AngularJS初学者通常没有意识到,ng-repeat、ng-switch、ng-view和ng-include所有这些指令都会创建一个子作用域,所以当执行这些指令时便会出现问题。
如果我们遵循记得在ng-model指令中使用'.'的“最佳实践”-- 值得花3分钟看看,我们能轻易地回避这个问题。Misko用ng-switch阐述了基本类型数据绑定的问题。
在你的ng-model指令中使用“.”能保证原型继承链起作用。所以,我们应该使用:
1
2
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" ><input type= "text" ng-model= "someObj.prop1" >
</font></font></font>
|
而不是:
1
2
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" ><input type= "text" ng-model= "prop1" >
</font></font></font>
|
如果你真的想或者真的需要用到基本类型数据,这里有两种变通方案:
- 在子作用域中使用$parent.parentScopeProperty,防止子作用域创建自身的属性
- 在父作用域中定义一个函数,并在子作用域中调用并传递基本类型数据给父作用域(并不是总能够做到)
JavaScript 原型继承
首先,我们要对JavaScript的原型继承有个良好的认知,这很重要,如果你有服务端编程的背景,更是如此。所以让我们先回顾一下原型继承的原理。
假设父级作用域有以下属性aString、aNumber、anArray、anObject 和 aFunction。如果子作用域原型继承于父作用域,
当我们试图从子作用域中访问父作用域上定义的属性,JavaScript会先在子作用域上查询该属性,如果没有找到该属性,再访问父级作用域并查询该属性。(如果在父作用域中依旧没有找到这个属性,JavaScript会继续顺着原型链往上查找... 直到根作用域)。因此,以下均为true:
1
2
3
4
5
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" >childScope.aString === 'parent string'
childScope.anArray[1] === 20
childScope.anObject.property1 === 'parent prop1'
childScope.aFunction() === 'parent output'
</font></font></font>
|
假设我们接下来进行以下操作:
1
2
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" >childScope.aString = 'child string' ;
</font></font></font>
|
原型链并未被查询,而子作用域中新增了一个 aString 属性。这个新的属性隐藏/遮蔽了父作用域的同名属性。当我们下面讨论到ng-repeat指令和ng-include指令时,这特性会变得非常重要。
接下来假设我们执行:
1
2
3
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" >childScope.anArray[1] = '22'
childScope.anObject.property1 = 'child prop1'
</font></font></font>
|
因为在子作用域中没有找到 anArray 和 anObject 对象,所以原型链被查询了。在父作用域中被找到这两个对象,所以属性值被更新到了原始的对象上。子作用域上没有添加新的属性,也没有创建新的对象。(注意,在JavaScript中数组和函数都是对象)。
接着,假设我们这么做:
1
2
|
childScope.anArray = [100, 555]
childScope.anObject = { name: 'Mark' , country: 'USA' }
|
原形链并未被访问,并且子作用域获得了两个新的对象属性,这两个属性也会遮蔽父作用域上的同名属性。
顺便提一下:
- 如果我们读取childScope.propertyX,并且子作用域有 propertyX 属性,那么原型链将不会被访问。
- 如果我们设置childScope.propertyX,那么原型链也不会被访问。
最后一种情况:
1
2
3
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" > delete childScope.anArray
childScope.anArray[1] === 22 // true
</font></font></font>
|
我们先删除子作用域的属性,然后当我们试图再次访问该属性,此时原型链会被访问。
Angular 作用域的继承
两种不同的情况:
- 以下指令会创建新的作用域,而且原型继承父级作用域:ng-repeat、 ng-include、ng-switch、ng-view、ng-controller、带scope: true的指令、设置了transclude:true的指令
- 以下指令会创建新的作用域,但不会原型继承:设置了scope: { ... }的指令。这指令创建的是孤立的作用域。
注意,通常情况下,即默认情况下scope:false,指令不会创建新的作用域。
ng-include
假设我们的控制器中有:
1
2
3
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" >$scope.myPrimitive = 50;
$scope.myObject = {aNumber: 11};
</font></font></font>
|
而且在我们的HTML中:
1
2
3
4
5
6
7
8
9
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" ><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>
</font></font></font>
|
每一个ng-include指令都生成一个新的子作用域,这些子作用域都原型继承于其父作用域。
在第一个输入框中输入77,子作用域将会得到一个新的myPrimitive属性,该属性会遮蔽了父作用域的同名属性。这可能不是你想要的。
在第二个输入框中输入99不会新建一个子作用域属性。因为tpl2.html绑定的数据是一个对象属性。当ngModel指令查询该对象,原型继承起到了作用,最终在父作用域中查找到该对象。
如果我们不想将我们的数据从基本类型改为对象,我们可以用$parent变量重写第一个模版:
1
2
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" ><input ng-model= "$parent.myPrimitive" >
</font></font></font>
|
在该输入框中输入22不会生成一个新的子作用域属性。现在,这个模型是绑定在父级作用域的一个属性上(因为$parent是子作用域上指向父作用域的属性值)。
对于所有的作用域(无论是否原型继承),Angular总会通过$parent、$$childHead`和`$$childTail记录下父-子关系(即一种层级关系)。以上的图表并没有展示这些属性值。
对于一些不涉及表单元素的情况,另一种解决方法是在父级作用域中定义一个函数用来修改基本类型数值。然后保证其子作用域都调用该函数,由于原型继承,其子作用域都能够访问的该函数。比如:
1
2
3
4
5
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" > // in the parent scope
$scope.setMyPrimitive = function (value) {
$scope.myPrimitive = value;
}
</font></font></font>
|
ng-switch
ng-switch指令的作用域继承的运行原理就类似于ng-include指令。所以如果你需要对父级作用域中的一个基本类型值进行双向版定,你可以使用$parent,或者将数据模型改成对象的形式,然后绑定该对象上的属性。这可以避免子作用域遮蔽到了父作用域上的属性。
更多阅读:AngularJS, bind scope of a switch-case?
ng-repeat
ng-repeat指令的运行原理有点不一样。假设我们控制器中有:
1
2
3
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" >$scope.myArrayOfPrimitives = [ 11, 22 ];
$scope.myArrayOfObjects = [{num: 101}, {num: 202}];
</font></font></font>
|
而且我们的HMTL中:
01
02
03
04
05
06
07
08
09
10
11
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" ><ul>
<li ng-repeat= "num in myArrayOfPrimitives" >
<input ng-model= "num" ></input>
</li>
</ul>
<ul>
<li ng-repeat= "obj in myArrayOfObjects" >
<input ng-model= "obj.num" ></input>
</li>
</ul>
</font></font></font>
|
每次迭代,ng-repeat指令都会创建一个新的作用域,该作用会原型继承于其父级作用域,但是同时该指令会给这个新作用域的一个新的属性分配本次迭代对应数值。(这个属性的名称就是循环变量的名字)。以下就Angular源码中ng-repeat具体实现:
1
2
3
|
<font style= "color:rgb(79, 79, 79)" ><font face= """ ><font style= "font-size:16px" >childScope = scope.$ new (); // child scope prototypically inherits from parent scope ...
childScope[valueIdent] = value; // creates a new childScope property
</font></font></font>
|