AngularJS中范围原型/原型继承的细微差别是什么?
API 参考范围页面显示:
作用域可以从父作用域继承。
作用域(典型地)从其父作用域继承属性。
- 那么,子作用域是否总是在原型上从其父作用域继承呢?
- 有例外吗?
- 当它继承时,它是否总是正常的JavaScript原型继承?
API 参考范围页面显示:
作用域可以从父作用域继承。
作用域(典型地)从其父作用域继承属性。
快速回答:
子作用域通常典型地从其父作用域继承,但并非总是如此。此规则的一个例外是带有 -- 的指令 - 这创建了一个“隔离”作用域,该作用域在原型上不会继承。此构造通常在创建“可重用组件”指令时使用。scope: { ... }
至于细微差别,范围继承通常是直的...直到在子作用域中需要双向数据绑定(即表单元素、ng 模型)。如果您尝试从子作用域内部绑定到父作用域中的基元(例如,数字、字符串、布尔值),则 Ng-repeat、ng-switch 和 ng-include 可能会让您绊倒。它没有按照大多数人期望的方式工作。子作用域获取自己的属性,该属性隐藏/隐藏同名的父属性。您的解决方法是
新的 AngularJS 开发人员通常没有意识到 、 、 和 都创建新的子作用域,因此当涉及这些指令时,问题经常会出现。(有关该问题的快速说明,请参阅此示例。ng-repeat
ng-switch
ng-view
ng-include
ng-if
通过遵循在ng模型中始终有一个“.”的“最佳实践”,可以很容易地避免基元的这个问题 - 观看3分钟的价值。Misko 演示了 与 的原始绑定问题。ng-switch
在模型中使用“.”将确保原型继承发挥作用。所以,使用
<input type="text" ng-model="someObj.prop1">
<!--rather than
<input type="text" ng-model="prop1">`
-->
也放在AngularJS维基上:https://github.com/angular/angular.js/wiki/Understanding-Scopes
首先要对原型继承有一个扎实的了解,这一点很重要,特别是如果你来自服务器端背景,并且你更熟悉类继承。因此,让我们先回顾一下。
假设 parentScope 具有属性 aString、aNumber、anArray、anObject 和 aFunction。如果 childScope 原型继承自 parentScope,则我们有:
(请注意,为了节省空间,我将对象显示为具有三个值的单个蓝色对象,而不是具有三个单独的灰色文本的单个蓝色对象。anArray
如果我们尝试从子作用域访问在 parentScope 上定义的属性,JavaScript 将首先在子作用域中查找,而不是查找该属性,然后在继承的作用域中查找该属性。(如果它没有在父级Scope中找到该属性,它将继续沿原型链向上移动...一直到根范围)。所以,这些都是真的:
childScope.aString === 'parent string'
childScope.anArray[1] === 20
childScope.anObject.property1 === 'parent prop1'
childScope.aFunction() === 'parent output'
假设我们这样做:
childScope.aString = 'child string'
不参考原型链,并将新的 aString 属性添加到子范围。此新属性隐藏/遮蔽具有相同名称的父级Scope 属性。当我们在下面讨论ng-repeat和ng-include时,这将变得非常重要。
假设我们这样做:
childScope.anArray[1] = '22'
childScope.anObject.property1 = 'child prop1'
之所以查询原型链,是因为在子类Scope中找不到对象(anArray 和 anObject)。对象位于父 Scope 中,属性值在原始对象上更新。不会向子范围添加任何新属性;不创建新对象。(请注意,在JavaScript中,数组和函数也是对象。
假设我们这样做:
childScope.anArray = [100, 555]
childScope.anObject = { name: 'Mark', country: 'USA' }
不参考原型链,子作用域获取两个新的对象属性,这些属性隐藏/隐藏具有相同名称的 parentScope 对象属性。
要点:
最后一种情况:
delete childScope.anArray
childScope.anArray[1] === 22 // true
我们首先删除了 childScope 属性,然后当我们尝试再次访问该属性时,会参考原型链。
竞争者:
scope: true
transclude: true
scope: { ... }
请注意,默认情况下,指令不会创建新作用域 ,即,缺省值为 。scope: false
假设我们在控制器中有:
$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 都会生成一个新的子作用域,该子作用域通常从父作用域继承。
在第一个输入文本框中键入(例如“77”)会导致子范围获取一个新的范围属性,该属性隐藏/隐藏同名的父范围属性。这可能不是你想要/期望的。myPrimitive
在第二个输入文本框中键入(例如“99”)不会生成新的子属性。由于 tpl2.html 将模型绑定到对象属性,因此当 ngModel 查找对象 myObject 时,原型继承将启动 - 它在父范围内找到它。
如果我们不想将模型从基元更改为对象,我们可以重写第一个模板以使用$parent:
<input ng-model="$parent.myPrimitive">
在此输入文本框中键入(例如“22”)不会生成新的子属性。模型现在绑定到父作用域的属性(因为$parent是引用父作用域的子作用域属性)。
对于所有作用域(原型或非原型),Angular 始终通过作用域属性($parent、$$childHead 和 $$childTail)跟踪父子关系(即层次结构)。我通常不会在关系图中显示这些作用域属性。
对于不涉及表单元素的方案,另一种解决方案是在父作用域上定义一个函数来修改基元。然后确保子函数始终调用此函数,由于原型继承,该函数将可用于子作用域。例如,
// in the parent scope
$scope.setMyPrimitive = function(value) {
$scope.myPrimitive = value;
}
下面是一个使用这种“父函数”方法的示例小提琴。(小提琴是作为这个答案的一部分写的:https://stackoverflow.com/a/14104318/215945。
另请参见 https://stackoverflow.com/a/13782671/215945 和 https://github.com/angular/angular.js/issues/1267。
ng 开关范围继承的工作方式与 ng 包含类似。因此,如果需要双向数据绑定到父作用域中的基元,请使用$parent,或将模型更改为对象,然后绑定到该对象的属性。这将避免父作用域属性的子作用域隐藏/阴影。
另请参阅 AngularJS,开关案例的绑定范围?
Ng-repeat的工作方式略有不同。假设我们在控制器中有:
$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>
对于每个项/迭代,ng-repeat 会创建一个新作用域,该作用域通常从父作用域继承,但它也会将项的值分配给新子作用域上的新属性。(新属性的名称是循环变量的名称。以下是ng-repeat的Angular源代码实际上是什么:
childScope = scope.$new(); // child scope prototypically inherits from parent scope
...
childScope[valueIdent] = value; // creates a new childScope property
如果 item 是基元(如 myArrayOfPrimitives),则实质上会将值的副本分配给新的子作用域属性。更改子作用域属性的值(即,使用 ng 模型,因此使用子作用域)不会更改父作用域引用的数组。因此,在上面的第一个 ng-repeat 中,每个子作用域都获得一个独立于 myArrayOfPrimitives 数组的属性:num
num
这个ng-repeat将不起作用(就像你想要/期望的那样)。在文本框中键入内容会更改灰色框中的值,这些值仅在子作用域中可见。我们想要的是让输入影响 myArrayOfPrimitives 数组,而不是子作用域基元属性。为此,我们需要将模型更改为对象数组。
因此,如果项是一个对象,则对原始对象(而不是副本)的引用将分配给新的子范围属性。更改子作用域属性的值(即,因此使用 ng 模型)确实会更改父作用域引用的对象。因此,在上面的第二个ng-重复中,我们有:obj.num
(我把一行涂成灰色,只是为了清楚它要去哪里。
这按预期工作。在文本框中键入内容会更改灰色框中的值,这些值对子范围和父范围都可见。
另请参阅 ng 模型、ng 重复以及输入和 https://stackoverflow.com/a/13782671/215945 的难度
使用ng控制器的嵌套控制器会导致正常的原型继承,就像ng-include和ng-switch一样,因此相同的技术也适用。但是,“两个控制器通过$scope继承共享信息被认为是不好的形式” - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ 应该使用A服务来代替控制器之间的数据共享。
(如果您确实想通过控制器范围继承共享数据,则无需执行任何操作。子作用域将有权访问所有父作用域属性。另请参阅控制器加载或导航时的加载顺序不同)
scope: false
scope: true
- 该指令创建一个原型从父作用域继承的新子作用域。如果多个指令(在同一 DOM 元素上)请求新作用域,则只会创建一个新的子作用域。由于我们有“正常”的原型继承,这就像ng-include和ng-switch一样,所以要警惕双向数据绑定到父作用域基元,以及父作用域属性的子作用域隐藏/阴影。scope: { ... }
- 该指令创建新的隔离/隔离作用域。它不是原型遗传。在创建可重用组件时,这通常是最佳选择,因为指令不会意外读取或修改父作用域。但是,此类指令通常需要访问一些父作用域属性。对象哈希用于在父作用域和隔离作用域之间设置双向绑定(使用'=')或单向绑定(使用'@')。还有“&”用于绑定到父作用域表达式。因此,这些属性都会创建从父作用域派生的本地作用域属性。请注意,特性用于帮助设置绑定 -- 不能只在对象哈希中引用父作用域属性名称,必须使用特性。例如,如果要绑定到隔离作用域中的父属性,这将不起作用:和 。必须使用一个特性来指定指令要绑定到的每个父属性:和 。parentProp
<div my-directive>
scope: { localProp: '@parentProp' }
<div my-directive the-Parent-Prop=parentProp>
scope: { localProp: '@theParentProp' }
__proto__
<my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">
scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
scope.someIsolateProp = "I'm isolated"
transclude: true
- 该指令创建一个新的“已包含”子作用域,该子作用域通常从父作用域继承。包含的作用域和隔离的作用域(如果有)是同级作用域 -- 每个作用域的 $parent 属性引用相同的父作用域。当包含的作用域和隔离作用域都存在时,隔离作用域属性 $$nextSibling 将引用排除的作用域。我不知道这个范围有任何细微差别。transclude: true
这个小提琴有一个功能,可以用来检查分离和排除的范围。请参阅小提琴注释中的说明。showScope()
有四种类型的作用域:
scope: true
scope: {...}
transclude: true
对于所有作用域(原型或非原型),Angular 始终通过属性$parent和 $$childHead 和 $$childTail 来跟踪父子关系(即层次结构)。
图表是使用graphviz“*.dot”文件生成的,这些文件位于github上。Tim Caswell的“Learning JavaScript with Object Graphs”是使用GraphViz绘制图表的灵感来源。
我绝不想与 Mark 的答案竞争,而只是想强调一下,作为 Javascript 继承及其原型链的新手,最终让一切都变得咔嚓一声。
只有属性读取搜索原型链,而不是写入。所以当你设置
myObject.prop = '123';
它不会查找链,但当您设置时
myObject.myThing.prop = '123';
在写入操作中正在进行一个微妙的读取,它试图在写入其道具之前查找myThing。这就是为什么从子对象写入 object.properties 会得到父对象的对象。