AngularJS中范围原型/原型inheritance的细微差别是什么?

API参考范围页面说:

范围可以从父范围inheritance。

开发人员指南范围页面说:

范围(原型)从其父范围inheritance属性。

那么,子范围是否总是从其父范围原型inheritance? 有例外吗? 当它inheritance时,是否正常的JavaScript原型inheritance?

快速回答
子范围通常从其父范围通常原型inheritance,但并非总是如此。 此规则的一个例外是一个指令,其scope: { ... } – 这会创build一个不会原型inheritance的“隔离”范围。 创build“可重用组件”指令时经常使用此构造。

至于细微差别,范围inheritance通常是直截了当的,直到你需要在子范围内进行双向数据绑定 (即表单元素,ng-model)。 Ng-repeat,ng-switch和ng-include可能会让你失望,如果你试图绑定到子作用域内的父作用域中的一个原语 (例如,数字,string,布尔)。 它不能像大多数人所期望的那样工作。 子作用域获取自己的属性,隐藏/阴影相同名称的父属性。 你的解决方法是

  1. 在你的模型的父对象中定义对象,然后在子对象中引用该对象的属性:parentObj.someProp
  2. 使用$ parent.parentScopeProperty(并不总是可能的,但在可能的情况下比1更容易)
  3. 在父范围上定义一个函数,并从子项调用它(并不总是可能的)

新的AngularJS开发人员通常并不知道ng-repeatng-switchng-viewng-includeng-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。

ng-include子范围

在第一个input文本框中键入(比如“77”)会导致子作用域获得一个新的myPrimitive作用域属性,该属性隐藏/ myPrimitive的父作用域属性。 这可能不是你想要的/期望的。

ng-include包含一个原语

在第二个input文本框中键入(例如“99”)不会导致新的子属性。 由于tpl2.html将模型绑定到对象属性,因此当ngModel查找对象myObject时,原型inheritance将启动 – 它会在父范围中find它。

ng包含一个对象

我们可以重写第一个模板来使用$ parent,如果我们不想把我们的模型从一个基元改变为一个对象:

 <input ng-model="$parent.myPrimitive"> 

在此input文本框中键入(例如“22”)不会导致新的子属性。 该模型现在绑定到父作用域的属性(因为$ parent是引用父作用域的子作用域属性)。

ng-include与$ 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来共享数据,那么你不需要做任何事情,子作用域将可以访问所有的父作用域属性,参见当加载或导航时控制器的加载顺序不同 )

指令

  1. 默认( scope: false ) – 指令不会创build新的范围,所以在这里没有inheritance。 这很容易,但是也是危险的,因为例如一个指令可能认为它在范围上创build一个新的属性,而实际上它正在破坏一个现有的属性。 这不是编写用作可重用组件的指令的好select。
  2. scope: true – 该指令创build一个新的子范围,它从父范围原型inheritance。 如果多个指令(在同一个DOM元素上)请求一个新的作用域,则只创build一个新的子作用域。 既然我们有“正常的”原型inheritance,就像ng-include和ng-switch一样,所以要谨慎双向数据绑定到父范围基元,以及父范围属性的子范围隐藏/遮蔽。
  3. 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/
  4. transclude: true – 指令创build一个新的“transcluded”子范围,它从父范围原型inheritance。 跨越范围和隔离范围(如果有的话)是兄弟姐妹 – 每个范围的$ parent属性引用相同的父范围。 当transcluded和isolate隔离区都存在时,隔离scope属性$$ nextSibling将引用transcluded作用域。 我不知道任何与transcluded作用域的细微差别。
    对于下面的图片,假设与上面相同的指令: transclude: true
    跨越范围

这个小提琴有showScope()函数,可以用来检查隔离和transcluded作用域。 请参阅小提琴中的注释中的说明。


概要

有四种types的范围:

  1. 正常原型范围inheritance – ng-include,ng-switch,ng-controller, scope: true指令
  2. 正常的原型范围inheritance与复制/分配 – ng重复。 ng-repeat的每次迭代都会创build一个新的子作用域,而新的子作用域总是会获得一个新的属性。
  3. 隔离作用域 – 指令, scope: {...} 。 这个不是原型的,但是'=','@'和'&'提供了一个通过属性访问父范围属性的机制。
  4. 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