AngularJS中范围原型/原型inheritance的细微差别是什么?
API参考范围页面说:
范围可以从父范围inheritance。
开发人员指南范围页面说:
范围(原型)从其父范围inheritance属性。
那么,子范围是否总是从其父范围原型inheritance? 有例外吗? 当它inheritance时,是否正常的JavaScript原型inheritance?
快速回答 :
子范围通常从其父范围通常原型inheritance,但并非总是如此。 此规则的一个例外是一个指令,其scope: { ... }
– 这会创build一个不会原型inheritance的“隔离”范围。 创build“可重用组件”指令时经常使用此构造。
至于细微差别,范围inheritance通常是直截了当的,直到你需要在子范围内进行双向数据绑定 (即表单元素,ng-model)。 Ng-repeat,ng-switch和ng-include可能会让你失望,如果你试图绑定到子作用域内的父作用域中的一个原语 (例如,数字,string,布尔)。 它不能像大多数人所期望的那样工作。 子作用域获取自己的属性,隐藏/阴影相同名称的父属性。 你的解决方法是
- 在你的模型的父对象中定义对象,然后在子对象中引用该对象的属性:parentObj.someProp
- 使用$ parent.parentScopeProperty(并不总是可能的,但在可能的情况下比1更容易)
- 在父范围上定义一个函数,并从子项调用它(并不总是可能的)
新的AngularJS开发人员通常并不知道ng-repeat
, ng-switch
, ng-view
, ng-include
和ng-if
全部都创build新的子范围,所以当涉及这些指令时,问题经常出现。 (请参阅此示例以便快速说明问题。)
这个与原始问题可以很容易地避免遵循“最佳做法” 总是有一个'。 在你的ng模型 – 看3分钟的价值。 Misko演示了ng-switch
的原始绑定问题。
有一个 '。' 在你的模型中将确保原型inheritance的发挥。 所以,使用
<input type="text" ng-model="someObj.prop1"> <!--rather than <input type="text" ng-model="prop1">` -->
很长的回答 :
JavaScript原型inheritance
也放在AngularJS wiki上: https : //github.com/angular/angular.js/wiki/Understanding-Scopes
首先要对原型inheritance有一个很好的理解,特别是如果你来自服务器端的背景,并且更熟悉类inheritance。 所以我们先来回顾一下。
假设parentScope具有属性aString,aNumber,anArray,anObject和aFunction。 如果childScope从parentScope原型inheritance,我们有:
(请注意,为了节省空间,我将anArray
对象显示为具有三个值的单个蓝色对象,而不是具有三个单独灰色文本的单个蓝色对象。)
如果我们尝试从子范围访问parentScope中定义的属性,JavaScript将首先查找子范围,找不到属性,然后查看inheritance的范围,然后查找属性。 (如果它没有在parentScope中find属性,它将继续原型链…一直到根作用域)。 所以这些都是真的
childScope.aString === 'parent string' childScope.anArray[1] === 20 childScope.anObject.property1 === 'parent prop1' childScope.aFunction() === 'parent output'
假设我们这样做:
childScope.aString = 'child string'
未查询原型链,并将新的aString属性添加到childScope。 这个新属性隐藏/遮蔽具有相同名称的parentScope属性。 当我们在下面讨论ng-repeat和ng-include时,这将变得非常重要。
假设我们这样做:
childScope.anArray[1] = '22' childScope.anObject.property1 = 'child prop1'
查询原型链是因为在childScope中找不到对象(anArray和anObject)。 在parentScope中find对象,并在原始对象上更新属性值。 没有新的属性被添加到childScope; 没有新的对象被创build。 (请注意,JavaScript中的数组和函数也是对象。)
假设我们这样做:
childScope.anArray = [100, 555] childScope.anObject = { name: 'Mark', country: 'USA' }
未查询原型链,子范围获取两个新的对象属性,用于隐藏/隐藏具有相同名称的parentScope对象属性。
小贴士:
- 如果我们读取childScope.propertyX,并且childScope具有propertyX,则不会查阅原型链。
- 如果我们设置了childScope.propertyX,则不会查询原型链。
最后一个场景:
delete childScope.anArray childScope.anArray[1] === 22 // true
我们先删除了childScope属性,然后当我们再次访问属性时,查询了原型链。
angular度范围的inheritance
竞争者:
- 下面创build新的作用域,并inheritance原型:ng-repeat,ng-include,ng-switch,ng-controller,指令的
scope: true
,指令的transclude: true
。 - 以下内容将创build一个不会inheritance原型的新作用域:指令的
scope: { ... }
。 这会创build一个“隔离”范围。
请注意,默认情况下,指令不会创build新的作用域,即默认为scope: false
。
NG-包括
假设我们有我们的控制器:
$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都会生成一个新的子作用域,它从父作用域中原型inheritance。
在第一个input文本框中键入(比如“77”)会导致子作用域获得一个新的myPrimitive
作用域属性,该属性隐藏/ myPrimitive
的父作用域属性。 这可能不是你想要的/期望的。
在第二个input文本框中键入(例如“99”)不会导致新的子属性。 由于tpl2.html将模型绑定到对象属性,因此当ngModel查找对象myObject时,原型inheritance将启动 – 它会在父范围中find它。
我们可以重写第一个模板来使用$ parent,如果我们不想把我们的模型从一个基元改变为一个对象:
<input ng-model="$parent.myPrimitive">
在此input文本框中键入(例如“22”)不会导致新的子属性。 该模型现在绑定到父作用域的属性(因为$ parent是引用父作用域的子作用域属性)。
对于所有范围(原型或不),Angular始终通过范围属性$ parent,$$ childHead和$$ childTail来跟踪父子关系(即层次结构)。 我通常不会在图中显示这些范围属性。
对于不涉及表单元素的情况,另一种解决scheme是在父作用域上定义一个函数来修改原语。 然后确保孩子总是调用这个函数,由于原型inheritance,这个函数将可用于子范围。 例如,
// in the parent scope $scope.setMyPrimitive = function(value) { $scope.myPrimitive = value; }
这是一个使用这种“父function”方法的示例小提琴 。 (小提琴是作为这个答案的一部分写的: https : //stackoverflow.com/a/14104318/215945 。)
另请参阅https://stackoverflow.com/a/13782671/215945和https://github.com/angular/angular.js/issues/1267 。
NG-开关
ng-switch作用域inheritance与ng-include类似。 因此,如果需要双向数据绑定到父范围中的基元,请使用$ parent,或将模型更改为对象,然后绑定到该对象的属性。 这将避免子作用域属性的子作用域隐藏/遮蔽。
另请参见AngularJS,绑定开关柜的范围?
NG-重复
吴重复有点不同。 假设我们有我们的控制器:
$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创build一个新的作用域,它从父作用域原型inheritance, 但也将该项的值赋给新的子作用域的新属性 。 (新属性的名称是循环variables的名称。)以下是ng-repeat的Angular源代码实际上是:
childScope = scope.$new(); // child scope prototypically inherits from parent scope ... childScope[valueIdent] = value; // creates a new childScope property
如果item是一个原语(如在myArrayOfPrimitives中),则实质上将该值的一个副本分配给新的子范围属性。 更改子范围属性的值(即,使用ng-model,因此子范围num
)不会更改父范围引用的数组。 所以在上面的第一个ng-repeat中,每个子范围都得到一个独立于myArrayOfPrimitives数组的num
属性:
这个ng-repeat不起作用(就像你想要的那样)。 input文本框会更改灰色框中的值,这些值仅在子范围中可见。 我们想要的是input影响myArrayOfPrimitives数组,而不是一个子范围原始属性。 为了实现这一点,我们需要将模型更改为一个对象数组。
因此,如果item是一个对象,则将对原始对象(而不是副本)的引用分配给新的子范围属性。 更改子范围属性的值(即,使用ng-model,因此obj.num
) 会更改父范围引用的对象。 所以在上面的第二个重复,我们有:
(我把一条线弄成灰色,这样就清楚了它要去的地方。)
这按预期工作。 在文本框中键入可以更改灰色框中的值,这些灰色框对于子范围和父范围都是可见的。
另请参见ng-model,ng-repeat和input的难度和https://stackoverflow.com/a/13782671/215945
NG-控制器
与ng-include和ng-switch一样,使用ng-controller的嵌套控制器也会产生正常的原型inheritance,所以应用相同的技术。 然而,“它被认为是两个控制器通过$范围inheritance共享信息的糟糕forms” – http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/应该使用服务来共享数据控制器。;
(如果你真的想通过控制器作用域inheritance来共享数据,那么你不需要做任何事情,子作用域将可以访问所有的父作用域属性,参见当加载或导航时控制器的加载顺序不同 )
指令
- 默认(
scope: false
) – 指令不会创build新的范围,所以在这里没有inheritance。 这很容易,但是也是危险的,因为例如一个指令可能认为它在范围上创build一个新的属性,而实际上它正在破坏一个现有的属性。 这不是编写用作可重用组件的指令的好select。 -
scope: true
– 该指令创build一个新的子范围,它从父范围原型inheritance。 如果多个指令(在同一个DOM元素上)请求一个新的作用域,则只创build一个新的子作用域。 既然我们有“正常的”原型inheritance,就像ng-include和ng-switch一样,所以要谨慎双向数据绑定到父范围基元,以及父范围属性的子范围隐藏/遮蔽。 -
scope: { ... }
– 该指令创build一个新的隔离/隔离范围。 它不是原型inheritance。 这通常是创build可重用组件时的最佳select,因为该指令不会意外读取或修改父范围。 但是,这样的指令通常需要访问几个父范围属性。 对象散列用于设置父范围和隔离范围之间的双向绑定(使用'=')或单向绑定(使用'@')。 还有'&'绑定到父范围expression式。 所以,这些都创build派生自父范围的本地范围属性。 请注意,属性用于帮助设置绑定 – 您不能仅仅引用对象哈希中的父级范围属性名称,您必须使用属性。 例如,如果要绑定到隔离范围的父属性parentProp
,那么这将不起作用:<div my-directive>
和scope: { localProp: '@parentProp' }
。 必须使用属性来指定指令要绑定到的每个父属性:<div my-directive the-Parent-Prop=parentProp>
和scope: { localProp: '@theParentProp' }
。
隔离作用域的__proto__
引用对象。 隔离范围的$父级引用父级范围,因此虽然它是孤立的,并且不能从父级范围原型inheritance,但它仍然是子范围。
对于下面的图片,我们有
<my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">
和
scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
另外,假设该指令在链接函数中执行此操作:scope.someIsolateProp = "I'm isolated"
有关隔离范围的更多信息,请参阅http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/ -
transclude: true
– 指令创build一个新的“transcluded”子范围,它从父范围原型inheritance。 跨越范围和隔离范围(如果有的话)是兄弟姐妹 – 每个范围的$ parent属性引用相同的父范围。 当transcluded和isolate隔离区都存在时,隔离scope属性$$ nextSibling将引用transcluded作用域。 我不知道任何与transcluded作用域的细微差别。
对于下面的图片,假设与上面相同的指令:transclude: true
这个小提琴有showScope()
函数,可以用来检查隔离和transcluded作用域。 请参阅小提琴中的注释中的说明。
概要
有四种types的范围:
- 正常原型范围inheritance – ng-include,ng-switch,ng-controller,
scope: true
指令 - 正常的原型范围inheritance与复制/分配 – ng重复。 ng-repeat的每次迭代都会创build一个新的子作用域,而新的子作用域总是会获得一个新的属性。
- 隔离作用域 – 指令,
scope: {...}
。 这个不是原型的,但是'=','@'和'&'提供了一个通过属性访问父范围属性的机制。 - transcluded作用域 – 指令
transclude: true
。 这也是正常的原型范围inheritance,但它也是任何隔离范围的兄弟姐妹。
对于所有范围(原型或不),Angular始终通过属性$ parent和$$ childHead和$$ childTail来跟踪父子关系(即层次结构)。
图表是用graphviz “* .dot”文件生成的,这些文件位于github上 。 Tim Caswell的“ 用对象图学习JavaScript ”是使用GraphViz进行图表的灵感。
我绝不希望与Mark的答案竞争,但只是想突出最终使所有的一切点击作为Javascript新的inheritance和它的原型链的一块。
只有财产读取search原型链,而不是写道。 所以当你设置
myObject.prop = '123';
它不查找链条,但是当你设置
myObject.myThing.prop = '123';
在这个写操作中有一个微妙的读取,在写入其prop之前试图查找myThing。 所以这就是为什么写入来自子对象的object.properties获取父对象的原因。
我想添加一个与JavaScript的原型inheritance的例子@Scott Driscoll答案。 我们将使用Object.create()(这是EcmaScript 5规范的一部分)中的经典inheritance模式。
首先我们创build“Parent”对象的function
function Parent(){ }
然后添加一个原型到“Parent”对象函数
Parent.prototype = { primitive : 1, object : { one : 1 } }
创build“子”对象function
function Child(){ }
分配子模型(使子模型inheritance父模型)
Child.prototype = Object.create(Parent.prototype);
指定适当的“子”原型构造函数
Child.prototype.constructor = Child;
将方法“changeProps”添加到子原型中,该原型将重写Child对象中的“primitive”属性值,并在Child和Parent对象中更改“object.one”值
Child.prototype.changeProps = function(){ this.primitive = 2; this.object.one = 2; };
启动父(父)和子(儿)对象。
var dad = new Parent(); var son = new Child();
调用Child(儿子)changeProps方法
son.changeProps();
检查结果。
父原始属性没有改变
console.log(dad.primitive); /* 1 */
子原始属性改变(重写)
console.log(son.primitive); /* 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