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

API 参考范围页面显示:

作用域可以从父作用域继承。

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

作用域(典型地)从其父作用域继承属性。

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

答案 1

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

至于细微差别,范围继承通常是直的...直到在子作用域中需要双向数据绑定(即表单元素、ng 模型)。如果您尝试从子作用域内部绑定到父作用域中的基元(例如,数字、字符串、布尔值),则 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">`
-->


L-o-n-g 答案

JavaScript 原型继承

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

首先要对原型继承有一个扎实的了解,这一点很重要,特别是如果你来自服务器端背景,并且你更熟悉类继承。因此,让我们先回顾一下。

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

prototypal inheritance

(请注意,为了节省空间,我将对象显示为具有三个值的单个蓝色对象,而不是具有三个单独的灰色文本的单个蓝色对象。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时,这将变得非常重要。

property hiding

假设我们这样做:

childScope.anArray[1] = '22'
childScope.anObject.property1 = 'child prop1'

之所以查询原型链,是因为在子类Scope中找不到对象(anArray 和 anObject)。对象位于父 Scope 中,属性值在原始对象上更新。不会向子范围添加任何新属性;不创建新对象。(请注意,在JavaScript中,数组和函数也是对象。

follow the prototype chain

假设我们这样做:

childScope.anArray = [100, 555]
childScope.anObject = { name: 'Mark', country: 'USA' }

不参考原型链,子作用域获取两个新的对象属性,这些属性隐藏/隐藏具有相同名称的 parentScope 对象属性。

more property hiding

要点:

  • 如果我们阅读childScope.propertyX,并且childScope具有deatureX,则不会参考原型链。
  • 如果我们设置childScope.propertyX,则不会参考原型链。

最后一种情况:

delete childScope.anArray
childScope.anArray[1] === 22  // true

我们首先删除了 childScope 属性,然后当我们尝试再次访问该属性时,会参考原型链。

after removing a child property


角度范围继承

竞争者:

  • 下面创建新的作用域,并典型地继承:ng-repeat、ng-include、ng-switch、ng-controller、指令 with 、指令 with 。scope: truetransclude: true
  • 下面将创建一个不继承原型的新作用域:带有 的指令。这将创建一个“隔离”作用域。scope: { ... }

请注意,默认情况下,指令不会创建新作用域 ,即,缺省值为 。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 都会生成一个新的子作用域,该子作用域通常从父作用域继承。

ng-include child scopes

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

ng-include with a primitive

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

ng-include with an object

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

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

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

ng-include with $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 开关范围继承的工作方式与 ng 包含类似。因此,如果需要双向数据绑定到父作用域中的基元,请使用$parent,或将模型更改为对象,然后绑定到该对象的属性。这将避免父作用域属性的子作用域隐藏/阴影。

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

ng-重复

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 数组的属性:numnum

ng-repeat with primitives

这个ng-repeat将不起作用(就像你想要/期望的那样)。在文本框中键入内容会更改灰色框中的值,这些值仅在子作用域中可见。我们想要的是让输入影响 myArrayOfPrimitives 数组,而不是子作用域基元属性。为此,我们需要将模型更改为对象数组。

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

ng-repeat with objects

(我把一行涂成灰色,只是为了清楚它要去哪里。

这按预期工作。在文本框中键入内容会更改灰色框中的值,这些值对子范围和父范围都可见。

另请参阅 ng 模型、ng 重复以及输入和 https://stackoverflow.com/a/13782671/215945 的难度

ng 控制器

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

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

指令

  1. default () - 指令不会创建新作用域,因此此处没有继承。这很容易,但也很危险,因为,例如,指令可能认为它在作用域上创建了一个新属性,而实际上它正在破坏现有属性。对于编写旨在作为可重用组件的指令,这不是一个好的选择。scope: false
  2. scope: true- 该指令创建一个原型从父作用域继承的新子作用域。如果多个指令(在同一 DOM 元素上)请求新作用域,则只会创建一个新的子作用域。由于我们有“正常”的原型继承,这就像ng-include和ng-switch一样,所以要警惕双向数据绑定到父作用域基元,以及父作用域属性的子作用域隐藏/阴影。
  3. scope: { ... }- 该指令创建新的隔离/隔离作用域。它不是原型遗传。在创建可重用组件时,这通常是最佳选择,因为指令不会意外读取或修改父作用域。但是,此类指令通常需要访问一些父作用域属性。对象哈希用于在父作用域和隔离作用域之间设置双向绑定(使用'=')或单向绑定(使用'@')。还有“&”用于绑定到父作用域表达式。因此,这些属性都会创建从父作用域派生的本地作用域属性。请注意,特性用于帮助设置绑定 -- 不能只在对象哈希中引用父作用域属性名称,必须使用特性。例如,如果要绑定到隔离作用域中的父属性,这将不起作用:和 。必须使用一个特性来指定指令要绑定到的每个父属性:和 。
    隔离作用域的引用对象。隔离作用域的$parent引用父作用域,因此尽管它是孤立的,并且通常不会从父作用域继承,但它仍然是子作用域。
    对于下面的图片,我们有


    并且假设指令在其链接函数中执行此操作:
    isolated scope
    有关隔离作用域的详细信息,请参阅 http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-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"
  4. transclude: true- 该指令创建一个新的“已包含”子作用域,该子作用域通常从父作用域继承。包含的作用域和隔离的作用域(如果有)是同级作用域 -- 每个作用域的 $parent 属性引用相同的父作用域。当包含的作用域和隔离作用域都存在时,隔离作用域属性 $$nextSibling 将引用排除的作用域。我不知道这个范围有任何细微差别。
    对于下图,假设指令与上面相同,并添加此内容:transclude: true
    transcluded scope

这个小提琴有一个功能,可以用来检查分离和排除的范围。请参阅小提琴注释中的说明。showScope()


总结

有四种类型的作用域:

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

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

图表是使用“*.dot”文件生成的,这些文件位于github上。Tim Caswell的“Learning JavaScript with Object Graphs”是使用GraphViz绘制图表的灵感来源。


答案 2

我绝不想与 Mark 的答案竞争,而只是想强调一下,作为 Javascript 继承及其原型链的新手,最终让一切都变得咔嚓一声。

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

myObject.prop = '123';

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

myObject.myThing.prop = '123';

在写入操作中正在进行一个微妙的读取,它试图在写入其道具之前查找myThing。这就是为什么从子对象写入 object.properties 会得到父对象的对象。