What are the nuances of scope prototypal / prototypical inheritance in AngularJS?
API参考范围页面显示:
A scope can inherit from a parent scope.
开发者指南范围页面显示:
A scope (prototypically) inherits properties from its parent scope.
那么,子范围是否总是原型地从其父范围继承?有例外吗?当它确实继承时,它总是普通的JavaScript原型继承吗?
快速回答:子作用域通常从其父作用域原型继承,但并非总是如此。此规则的一个例外是使用
至于细微差别,范围继承通常是直接的。直到在子范围中需要双向数据绑定(即表单元素、ng模型)。如果试图从子作用域内部绑定到父作用域中的基元(例如,数字、字符串、布尔值),则ng repeat、ng switch和ng include可能会使您绊倒。它不像大多数人期望的那样工作。子作用域获取自己的属性,该属性隐藏/隐藏同名的父属性。你的解决方法是好的。
新的AngularJS开发人员往往没有意识到
在您的NG模型中,遵循"总是有一个"的"最佳实践",可以很容易地避免与原语有关的这个问题——注意3分钟的价值。misko证明了与
在模型中使用"."将确保原型继承发挥作用。所以,使用好的。
1 2 3 4 5 | <input type="text" ng-model="someObj.prop1"> <!--rather than <input type="text" ng-model="prop1">` --> |
L- O-N-G回答:好的。javascript原型继承
也放在AngularJS wiki上:https://github.com/angular/angular.js/wiki/understanding-scopes好的。
首先对原型继承有一个扎实的理解是很重要的,特别是如果您来自服务器端背景,并且更熟悉类继承。我们先回顾一下。好的。
假设ParentScope具有Astring、Anumber、Anarray、AnObject和Afunction属性。如果ChildScope原型继承自ParentScope,我们有:好的。
好的。
(注意,为了节省空间,我将
如果我们试图从子作用域访问ParentScope上定义的属性,javascript将首先查找子作用域,而不是查找属性,然后查找继承的作用域,并查找属性。(如果它在ParentScope中找不到属性,那么它将继续向原型链上游……一直到根作用域)。所以,这些都是真的:好的。
1 2 3 4 | childScope.aString === 'parent string' childScope.anArray[1] === 20 childScope.anObject.property1 === 'parent prop1' childScope.aFunction() === 'parent output' |
假设我们这样做:好的。
1 | childScope.aString = 'child string' |
不查询原型链,并向ChildScope添加新的Astring属性。此新属性隐藏/隐藏同名的ParentScope属性。当我们讨论ng repeat和ng include时,这将变得非常重要。好的。
好的。
假设我们这样做:好的。
1 2 | childScope.anArray[1] = '22' childScope.anObject.property1 = 'child prop1' |
查询原型链是因为在ChildScope中找不到对象(Anarray和AnObject)。对象在ParentScope中找到,属性值在原始对象上更新。不会向ChildScope添加新属性;不会创建新对象。(请注意,在javascript数组和函数中也是对象。)好的。
好的。
假设我们这样做:好的。
1 2 | childScope.anArray = [100, 555] childScope.anObject = { name: 'Mark', country: 'USA' } |
不参考原型链,子作用域获取两个新的对象属性,这些属性隐藏/隐藏具有相同名称的ParentScope对象属性。好的。
好的。
外卖:好的。
- 如果我们读取了childscope.propertyx,而childscope具有propertyx,则不参考原型链。
- 如果我们设置childscope.propertyx,则不会参考原型链。
最后一个场景:好的。
1 2 | delete childScope.anArray childScope.anArray[1] === 22 // true |
我们首先删除了ChildScope属性,然后当我们再次尝试访问该属性时,会参考原型链。好的。
好的。角度范围继承
竞争者:好的。
- 下面创建新的作用域,并继承原型:ng repeat、ng include、ng switch、ng controller、带有
scope: true 的指令、带有transclude: true 的指令。 - 下面创建了一个不继承原型的新范围:带有
scope: { ... } 的指令。这将创建一个"隔离"范围。
注意,默认情况下,指令不会创建新的作用域——即,默认值是
假设我们的控制器中有:好的。
1 2 | $scope.myPrimitive = 50; $scope.myObject = {aNumber: 11}; |
在我们的HTML中:好的。
1 2 3 4 5 6 7 | <script type="text/ng-template" id="/tpl1.html"> <input ng-model="myPrimitive"> <script type="text/ng-template" id="/tpl2.html"> <input ng-model="myObject.aNumber"> |
每个ng-include都会生成一个新的子作用域,原型继承自父作用域。好的。
好的。
在第一个输入文本框中键入(例如,"77")将导致子作用域获得一个新的
好的。
在第二个输入文本框中键入(例如"99")不会产生新的子属性。因为tpl2.html将模型绑定到对象属性,所以当ngmodel查找对象myObject时,原型继承就开始了——它在父范围中找到了它。好的。
好的。
如果不想将模型从原语更改为对象,我们可以重写第一个使用$parent的模板:好的。
1 | <input ng-model="$parent.myPrimitive"> |
在此输入文本框中键入(例如"22")不会产生新的子属性。模型现在绑定到父作用域的属性(因为$parent是引用父作用域的子作用域属性)。好的。
好的。
对于所有范围(无论是否为原型),Angular始终通过范围属性$parent、$$childhead和$$childtail跟踪父子关系(即层次结构)。我通常不会在图表中显示这些作用域属性。好的。
对于不涉及表单元素的场景,另一个解决方案是在父作用域上定义一个函数来修改原语。然后确保子级总是调用这个函数,由于原型继承,子级作用域可以使用这个函数。例如。,好的。
1 2 3 4 | // 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 include类似。因此,如果需要将双向数据绑定到父范围中的基元,请使用$parent,或者将模型更改为对象,然后绑定到该对象的属性。这将避免父作用域属性的子作用域隐藏/隐藏。好的。
另请参见AngularJS,交换机外壳的绑定范围?好的。NG重复
ng repeat的工作方式略有不同。假设我们的控制器中有:好的。
1 2 | $scope.myArrayOfPrimitives = [ 11, 22 ]; $scope.myArrayOfObjects = [{num: 101}, {num: 202}] |
在我们的HTML中:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <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的角度源代码实际是:好的。
1 2 3 | childScope = scope.$new(); // child scope prototypically inherits from parent scope ... childScope[valueIdent] = value; // creates a new childScope property |
如果项是原语(如MyArrayOfPrimitives中的原语),则基本上会将值的副本分配给新的子范围属性。更改子范围属性的值(即使用ng模型,因此子范围
好的。
这个NG重复将不起作用(如您希望/期望的那样)。在文本框中键入内容会更改灰色框中的值,这些值仅在子范围中可见。我们希望输入影响myArrayOfPrimitives数组,而不是子范围原语属性。为了实现这一点,我们需要将模型更改为对象数组。好的。
因此,如果项是对象,则对原始对象(而不是副本)的引用将分配给新的子范围属性。更改子范围属性的值(即使用ng模型,因此
好的。
(我把一条线涂成灰色,这样就可以清楚地看到它的去向。)好的。
这按预期工作。在文本框中键入内容会更改灰色框中的值,这些值对子范围和父范围都可见。好的。
另请参见ng模型、ng重复和输入的困难以及https://stackoverflow.com/a/13782671/215945好的。NG控制器
使用ng控制器嵌套控制器会导致正常的原型继承,就像ng include和ng开关一样,所以同样的技术也适用。但是,"两个控制器通过$scope继承共享信息被认为是不好的形式"-http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/应该使用服务在控制器之间共享数据。好的。
(如果您真的想通过控制器范围继承来共享数据,则无需做任何事情。子作用域将可以访问所有父作用域属性。另请参见控制器加载顺序在加载或导航时的不同)好的。指令
这个小提琴有一个
作用域有四种类型:好的。
对于所有范围(无论是否为原型),Angular始终通过属性$parent和$childhead和$childtail跟踪父子关系(即层次结构)。好的。
图表是用Github上的graphviz"*.dot"文件生成的。TimCaswell的"用对象图学习JavaScript"是在图表中使用graphviz的灵感来源。好的。好啊。
我不想和马克的答案竞争,但我只想强调一点,那就是最终让所有东西都像一个新的javascript继承和原型链一样点击。
只有属性读取搜索原型链,而不是写入。所以当你设定
1 | myObject.prop = '123'; |
它不查链子,但当你设置
1 | myObject.myThing.prop = '123'; |
在这个写操作中有一个微妙的阅读,它试图在写到它的道具之前查找神话。这就是为什么从子对象写入object.properties会得到父对象的原因。
我想在@scott driscoll answer中添加一个带有javascript的原型继承示例。我们将使用带有object.create()的经典继承模式,这是ecmascript 5规范的一部分。
首先我们创建"父"对象函数
1 2 3 | function Parent(){ } |
然后将原型添加到"父"对象函数
1 2 3 4 5 6 | Parent.prototype = { primitive : 1, object : { one : 1 } } |
创建"子"对象函数
1 2 3 | function Child(){ } |
指定子原型(使子原型从父原型继承)
1 | Child.prototype = Object.create(Parent.prototype); |
指定适当的"子"原型构造函数
1 | Child.prototype.constructor = Child; |
将方法"changeprops"添加到子原型中,该方法将重写子对象中的"primitive"属性值,并更改子对象和父对象中的"object.one"值。
1 2 3 4 | Child.prototype.changeProps = function(){ this.primitive = 2; this.object.one = 2; }; |
启动父对象(DAD)和子对象(SON)。
1 2 | var dad = new Parent(); var son = new Child(); |
调用child(son)changeProps方法
1 | son.changeProps(); |
检查结果。
父基元属性未更改
1 | console.log(dad.primitive); /* 1 */ |
子基元属性已更改(重写)
1 | console.log(son.primitive); /* 2 */ |
父对象和子对象.one属性已更改
1 2 | console.log(dad.object.one); /* 2 */ console.log(son.object.one); /* 2 */ |
这里的工作示例http://jsbin.com/xexurukiso/1/edit/
有关object.create的详细信息,请访问:https://developer.mozilla.org/en/docs/web/javascript/reference/global_objects/object/create