摘要
在AngularJS中, ChildScope通常以原型链的方式继承自ParentScope. 但有一个特殊的情况, 当你在指令中指定了scope:{}
, 这会创建一个IsolateScope, 它不从DOM元素所在的Scope上继承属性, 这对于需要创建一个可复用的指令很有帮助.
Scope的继承通常很简单, 以至于大多数情况下你不会意识到它的存在, 但当你尝试在ChildScope里双向数据绑定(例如ng-model
)一个ParentScope上的原生变量的时候(例如number, string, boolean
), 它会在ChildScope上创建一个同名的属性, 并使你无法再访问到ParentScope上的该属性, 这并不是Angular施了什么魔法, 这是由Javascrip原型继承决定的. 刚入门的AngularJS 开发者通常不会意识到ng-repeat, ng-switch, ng-view, ng-include, ng-if
这些指令都会创建一个新的ChildScope, 所以当牵扯到这些指令的时候, 经常会出现一些奇怪的问题. 看看这个例子.
一个最佳实践可以轻松的避免上面提到的原生变量的问题, 就是永远在ng-model中加上一个".", 这样可以确保原型链继承正常工作. 所以
//use
<input type="text" ng-model="someObj.prop1">
//rather than
<input type="text" ng-model="prop1">.
如果你确实需要绑定原生变量, 有两个解决办法:
- 在ChildScope里绑定
$parent.parentScopeProperty
, 这样ChildScope不会自己创建一个同名的变量. - 在ParentScope里定义一个函数, 在ChildScope里调用, 把原生变量作为参数传入
JavaScript 原型继承
充分理解JavaScript原型继承很重要, 尤其如果你是后端开发人员, 更熟悉类(Class)继承. 下面我们回顾一下原型继承.
假设ParentScope有如下属性, aString, aNumber, anArray, anObject, aFunction
. ChildScope继承自它.
当我们试图从ChildScope中去访问一个ParentScope上的属性, JavaScript会从ChildScope开始一级一级沿着原型链往上找, 直到找到该属性为止. 所以下面这些表达式都为TRUE.
ChildScope.aString === 'parent string'
ChildScope.anArray[1] === 20
ChildScope.anObject.property1 === 'parent prop1'
ChildScope.aFunction() === 'parent output'
假设接下类我们执行如下语句
ChildScope.aString = 'child string'
这句代码会在ChildScope创建一个叫aString
的属性, 而ParentScope上的aString不会受到任何影响. 明白这点对接下来讨论ng-repeat
和ng-include
很重要.
接下来我们执行
ChildScope.anArray[1] = 22
ChildScope.anObject.property1 = 'child prop1'
JavaScript会先在ChildScope上寻找anArray
和anObject
属性, 找寻无果后, 会继续往原型链上一级也就是ParentScope寻找, 所以ParentScope上的这两个属性就被修改了.
继续执行
ChildScope.anArray = [100, 555]
ChildScope.anObject = { name: 'Mark', country: 'USA' }
仔细比较和上一步的区别, 这次是直接对anArray
和anObject
赋值, 会在ChildScope新增两个对象属性.
这个jsfiddle 是对上述例子的演示.
Angular Scope 继承
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
都会创建一个ChildScope, 如下图关系:
在第一个输入框中输入"77", 会得到图4结果, 因为这相当于执行了
ChildScope.myPrimitive = 77
在第二个输入框中输入"99", 会得到图5结果, 因为这相当于执行了
ChildScope.myObject.aNumber = 99
参考上面说道的JavaScript原型继承, 相信这不难理解.
如摘要中提到的, 如果你确实需要用原生变量, 你可以修改tp11.html
为
<input ng-model="$parent.myPrimitive">
输入"22", 得到图6结果, 因为我们直接引用$parent(对ParentScope的引用)上的属性. 所以不会在ChildScope上创建属性.
或者你可以使用绑定函数的形式, 参考jsfiddle
ng-switch
和
ng-include
类似, 不再赘述.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"></input>
</li>
</ul>
<ul>
<li ng-repeat="obj in myArrayOfObjects">
<input ng-model="obj.num"></input>
</li>
</ul>
对每一次循环, 每一个ng-repeat
会创建一个继承当前Scope的ChildScope. 然后在ChildScope上创建一个属性并赋值(属性的名字就是循环的变量名). 上述代码的运行结果请看图7和8.
ChildScope = scope.$new(); // child scope prototypically inherits from parent scope ...
ChildScope[valueIdent] = value; // creates a new childScope property
他们的区别在于数组中每一项的值是原生类型还是对象. 如果是原生类型, 则会复制一份然后赋值给ChildScope上的新属性(
num
), 如果是对象, 则把对象的引用赋值给ChildScope上的新属性(obj
).ng-view
TBD, but I think it acts just like ng-include.
ng-controller
互相嵌套的Controller, 就像
ng-include, ng-switch
一样, 会产生继承关机的Scope. 但是通过$scope在Controller之间共享数据是不好的做法, 应该用Service替代.directive
-
scope: false
default
不会创建ChildScope, 所以也存在什么继承, 很简单但也很危险, 指令可以直接操作Scope -
scope: true
创建一个ChildScope继承自ParentScope, 如果一个DOM元素上同时有多个指令需要新的Scope, 只有一个会被创建. -
scope: {}
创建一个IsolateScope, 如果需要访问ParentScope上的属性, 可以使用@, =, <, &
, 详情参考Directives -
transclude: true
创建一个TranscludeChildScope继承自指令外部的ParentScope(不是指令的Scope), Angular1.3版本以前, TranscludeChildScope的$parent属性指向的是ParentScope. Angular1.3版本以后, TranscludeChildScope的$parent属性指向的是指令的Scope. jsfiddle
https://github.com/angular/angular.js/wiki/Understanding-Scopes