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


1028

API参考范围”页面显示

范围可以从父范围继承。

开发者指南范围页说:

范围(典型地)从其父范围继承属性。

  • 那么,子作用域是否总是从原型上继承自其父作用域?
  • 有例外吗?
  • 当它继承时,是否总是正常的JavaScript原型继承?

Answers:


1740

快速解答
子作用域通常从其父作用域继承原型,但并非总是如此。该规则的一个例外是带有的指令scope: { ... }-这会创建一个“隔离”作用域,该作用域不原型继承。创建“可重用组件”指令时,经常使用此构造。

对于细微差别,作用域继承通常是直截了当的……直到您需要在子作用域中进行2路数据绑定(即表单元素,ng-model)。如果您尝试从子作用域内部绑定到父作用域中的原语(例如,数字,字符串,布尔值),则Ng-repeat,ng-switch和ng-include可能会使您绊倒。它不能像大多数人期望的那样工作。子作用域具有其自己的属性,该属性隐藏/阴影相同名称的父属性。您的解决方法是

  1. 在模型的父级中定义对象,然后在子级中引用该对象的属性:parentObj.someProp
  2. 使用$ parent.parentScopeProperty(并非总是可能,但比1容易)
  3. 在父作用域上定义一个函数,然后从子作用域调用它(并非总是可能的)

新AngularJS开发商往往没有意识到ng-repeatng-switchng-viewng-includeng-if所有新创建子作用域,因此问题常常显示出来时,这些指令都参与其中。(有关问题的快速说明,请参见此示例。)

通过遵循始终具有“' ”的“最佳实践”,可以很容易避免原语的此问题在您的ng模型中 -观看3分钟值得。Misko用演示了原始绑定问题ng-switch

有一个 '。' 在您的模型中,将确保原型继承在起作用。所以用

<input type="text" ng-model="someObj.prop1">

<!--rather than
<input type="text" ng-model="prop1">`
-->


长答案

JavaScript原型继承

也放在AngularJS Wiki上: https : //github.com/angular/angular.js/wiki/Understanding-Scopes

首先,要对原型继承有深刻的了解,这一点很重要,尤其是如果您来自服务器端背景并且对类继承有更深的了解时。因此,让我们先回顾一下。

假设parentScope具有属性aString,aNumber,anArray,anObject和aFunction。如果childScope原型继承自parentScope,则我们具有:

原型继承

(请注意,为了节省空间,我将anArray对象显示为具有三个值的单个蓝色对象,而不是显示具有三个单独的灰色文字的单个蓝色对象。)

如果我们尝试从子作用域访问在parentScope上定义的属性,则JavaScript将首先在子作用域中查找,而不是查找该属性,然后在继承的作用域中查找并找到该属性。(如果没有在parentScope中找到该属性,它将在原型链中一直延续到根范围)。所以,这些都是对的:

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中找到,并且属性值在原始对象上更新。没有向childScope添加任何新属性;没有创建新对象。(请注意,在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属性,然后再次尝试访问该属性时,将查询原型链。

删除子财产后


角范围继承

竞争者:

  • 以下代码创建新的作用域并进行原型继承:ng-repeat,ng-include,ng-switch,ng-controller,带scope: true指令的伪指令,带指令的伪指令transclude: true
  • 以下内容创建了一个不原型继承的新作用域:带的指令scope: { ... }。而是创建一个“隔离”范围。

请注意,默认情况下,指令不会创建新的作用域-即默认值为scope: false

ng-include

假设我们在控制器中:

$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都会生成一个新的子作用域,该子作用域通常是从父作用域继承的。

ng-include子作用域

在第一个输入文本框中键入(例如“ 77”)会导致子范围获得一个新的myPrimitive范围属性,该属性将隐藏/阴影相同名称的父范围属性。这可能不是您想要/期望的。

ng-include与原语

在第二个输入文本框中键入(例如“ 99”)不会导致新的子属性。因为tpl2.html将模型绑定到对象属性,所以当ngModel查找对象myObject时,原型继承就开始了-它在父作用域中找到它。

ng-include一个对象

如果我们不想将模型从基本类型更改为对象,则可以重写第一个模板以使用$ parent:

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

在此输入文本框中键入(例如“ 22”)不会产生新的子属性。现在,该模型已绑定到父范围的属性(因为$ parent是引用父范围的子范围属性)。

ng-include和$ 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/215945https://github.com/angular/angular.js/issues/1267

ng-开关

ng-switch作用域继承的工作方式与ng-include一样。因此,如果您需要与父作用域中的原语进行双向数据绑定,请使用$ parent或将模型更改为对象,然后绑定至该对象的属性。这将避免子范围隐藏/隐藏父范围属性。

另请参阅AngularJS,绑定开关盒的范围?

ng-repeat

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-模型,因此子范围num)并不会改变阵列父范围引用。因此,在上面的第一个ng-repeat中,每个子范围都获得一个num独立于myArrayOfPrimitives数组的属性:

用原语进行ng-repeat

此ng-repeat无法正常工作(就像您想要/期望的那样)。在文本框中键入将更改灰色框中的值,这些值仅在子作用域中可见。我们想要的是使输入影响myArrayOfPrimitives数组,而不是子作用域原始属性。为此,我们需要将模型更改为对象数组。

因此,如果item是对象,则将对原始对象(不是副本)的引用分配给新的子范围属性。更改子范围属性的值(即使用ng-model obj.num确实会更改父范围引用的对象。因此,在上面的第二个ng-repeat中,我们有:

ng-repeat with objects

(我只是将一行涂成灰色,以便清楚地知道行进路线。)

这按预期工作。键入文本框会更改灰色框中的值,这对子作用域和父作用域都是可见的。

另请参见ng-model,ng-repeat和输入的难度以及 https://stackoverflow.com/a/13782671/215945

ng控制器

使用ng-controller嵌套控制器会导致正常的原型继承,就像ng-include和ng-switch一样,因此适用相同的技术。但是,“两个控制器通过$ scope继承共享信息被认为是错误的形式” -http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ 应该使用服务在两个之间共享数据控制器代替。

(如果您确实想通过Controller作用域继承共享数据,则无需执行任何操作。子作用域将有权访问所有父作用域属性。另请参见加载或导航时Controller的加载顺序有所不同

指令

  1. default(scope: false)-指令不会创建新的作用域,因此此处没有继承。这很容易,但也很危险,因为例如,一条指令可能认为它正在作用域上创建新属性,而实际上却在破坏现有属性。对于编写旨在用作可重用组件的指令,这不是一个好的选择。
  2. scope: true-伪指令创建了一个新的子范围,该子范围原型地从父范围继承。如果一个以上的指令(在同一DOM元素上)请求一个新范围,则仅创建一个新的子范围。由于我们具有“常规”原型继承,因此就像ng-include和ng-switch一样,因此请谨慎使用2路数据绑定到父作用域原语,以及子作用域隐藏/遮蔽父作用域属性。
  3. scope: { ... }-指令创建了一个新的隔离/隔离范围。它不是原型继承的。在创建可重用组件时,这通常是最佳选择,因为该指令不会意外读取或修改父作用域。但是,此类指令通常需要访问一些父范围属性。对象哈希用于在父作用域和隔离作用域之间建立双向绑定(使用'=')或单向绑定(使用'@')。还有“&”绑定到父作用域表达式。因此,所有这些都创建了从父范围派生的本地范围属性。请注意,属性用于帮助设置绑定-您不能仅在对象哈希中引用父范围属性名称,而必须使用属性。例如,如果您要绑定到父属性,则此方法将无效parentProp在隔离范围内:<div my-directive>scope: { localProp: '@parentProp' }。必须使用属性来指定指令要绑定到的每个父属性:<div my-directive the-Parent-Prop=parentProp>scope: { localProp: '@theParentProp' }
    隔离范围的__proto__引用对象。隔离作用域的$ parent引用了父作用域,因此,尽管它是隔离的并且不会从父作用域继承原型,但它仍然是子作用域。
    对于下面的图片中,我们有
    <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-伪指令创建一个新的“已包含”子作用域,该子作用域典型地从父作用域继承。超越范围和隔离范围(如果有的话)是同级的-每个范围的$ parent属性引用相同的父范围。当同时存在一个被隐含作用域和一个被隔离的作用域时,隔离作用域属性$$ nextSibling将引用该被隐含作用域。我不知道这个被包含在范围之外的细微差别。
    对于下面的图片,假定与上述相同的指令与此附加内容:transclude: true
    超越范围

小提琴具有一项showScope()功能,可用于检查隔离和超越的示波器。请参阅小提琴中注释中的说明。


摘要

范围有四种类型:

  1. 正常的原型作用域继承-ng-include,ng-switch,ng-controller,带有 scope: true
  2. 正常的原型范围继承,带有复制/赋值-ng-repeat。ng-repeat的每次迭代都会创建一个新的子范围,并且该新的子范围始终会获得一个新的属性。
  3. 隔离作用域-使用scope: {...}。这不是原型,但是'=','@'和'&'提供了一种通过属性访问父范围属性的机制。
  4. 超越范围-使用transclude: true。这也是正常的原型作用域继承,但它也是任何隔离作用域的同级对象。

对于所有作用域(是否是原型),Angular始终通过属性$ parent和$$ childHead和$$ childTail跟踪父子关系(即层次结构)。

生成图 “ * .dot”文件,位于github上。蒂姆·卡斯韦尔(T​​im Caswell)的“ 用对象图学习JavaScript ”是为图表使用GraphViz的灵感。


48
很棒的文章,对于一个SO答案来说太长了,但是还是很有用的。请在编辑器将其缩小之前将其放在您的博客上。
iwein 2012年

43
我将副本放在AngularJS Wiki上
Mark Rajcok

3
更正:“隔离范围的__proto__引用对象。” 而是应为“隔离范围的__proto__引用是Scope对象”。因此,在最后两张图片中,橙色的“对象”框应改为“范围”框。
Mark Rajcok

15
该请求应包含在angularjs指南中。这要更具说服力……
Marcelo De Zen

2
Wiki让我感到困惑,首先它显示为:“查阅了原型链,因为在childScope中找不到对象。” 然后它显示为:“如果设置childScope.propertyX,则不会查询原型链。”。第二个隐含条件,而第一个隐含条件。
2014年

140

我绝不想与Mark的答案抗衡,而只是想强调一下这件作品,最终成为Java继承及其原型链的新手,使一切都点击

只有属性读取会搜索原型链,而不会写入。所以当你设定

myObject.prop = '123';

它不会查找链,但是当您设置

myObject.myThing.prop = '123';

在该写入操作中进行了一个微妙的读取,该写入尝试在写入其道具之前查找myThing。因此,这就是为什么从子级写入object.properties会到达父级的对象的原因。


12
尽管这是一个非常简单的概念,但它可能不是很明显,因为我相信很多人会错过它。说得好。
moljac024 2014年

3
很好的评论。我带走的是,非对象属性的分辨率不涉及读取,而对象属性的分辨率涉及。
2014年

1
为什么?不写原型链的财产写入的动机是什么?似乎疯了……
乔纳森。

1
如果您添加了一个简单的示例,那就太好了。
tylik 2014年

2
注意,它确实在原型链搜索二传手。如果未找到任何内容,它将在接收方上创建一个属性。
Bergi

21

我想在@Scott Driscoll答案中添加一个使用javascript进行原型继承的示例。我们将对Object.create()使用经典继承模式,这是EcmaScript 5规范的一部分。

首先我们创建“父母”对象功能

function Parent(){

}

然后将原型添加到“父对象”对象函数

 Parent.prototype = {
 primitive : 1,
 object : {
    one : 1
   }
}

创建“子”对象功能

function Child(){

}

分配子原型(使子原型从父原型继承)

Child.prototype = Object.create(Parent.prototype);

分配适当的“ Child”原型构造函数

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(son)changeProps方法

son.changeProps();

检查结果。

父原语属性未更改

console.log(dad.primitive); /* 1 */

子基本属性已更改(已重写)

console.log(son.primitive); /* 2 */

父级和子级object.one属性已更改

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.