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




原型链阮一峰 (2)

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

至于细微差别,范围继承通常是直截了当的......直到您需要在子范围内使用双向数据绑定 (即表单元素,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 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-包括

假设我们在我们的控制器中有:

$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生成一个新的子范围,它从父范围原型继承。

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

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

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

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

在此输入文本框中键入(例如“22”)不会生成新的子属性。 模型现在绑定到父作用域的属性(因为$ 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/14104318/215945 。)

另请参阅https://stackoverflow.com/a/13782671/215945和https://github.com/angular/angular.js/issues/1267

NG-开关

ng-switch作用域继承与ng-include类似。 因此,如果需要双向数据绑定到父范围中的基元,请使用$ parent,或者将模型更改为对象,然后绑定到该对象的属性。 这将避免子作用域属性的子作用域隐藏/遮蔽。

另请参见AngularJS,绑定一个switch-case的作用域?

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创建一个新范围,该范围从父范围原型继承, 但它也将项目的值分配给新子范围上的新属性 。 (新属性的名称是循环变量的名称。)以下是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不起作用(就像你想要的那样)。 键入文本框会更改灰色框中的值,这些值仅在子范围中可见。 我们想要的是输入影响myArrayOfPrimitives数组,而不是子作用域原始属性。 为了实现这一点,我们需要将模型改为对象数组。

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

(我把一条线弄成灰色,这样就清楚了它要去的地方。)

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

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

NG-控制器

与ng-include和ng-switch一样,使用ng-controller的嵌套控制器也会产生正常的原型继承,所以同样的技术也适用。 然而,“它被认为是两个控制器通过$ scope继承共享信息的糟糕形式” - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/应该使用服务来共享数据控制器。

(如果你真的想通过控制器作用域继承来共享数据,你就不需要做任何事情,子作用域可以访问所有的父作用域属性,参见当加载或导航时控制器的加载顺序不同

指令

  1. 默认( scope: false ) - 指令不会创建新的范围,所以在这里没有继承。 这很容易,但也很危险,因为例如一条指令可能认为它在范围上创建一个新属性,而实际上它正在破坏一个现有属性。 这对编写用作可重用组件的指令不是一个好的选择。
  2. scope: true - 该指令创建一个新的子范围,该范围从父范围原型继承。 如果多个指令(在同一个DOM元素上)请求一个新的作用域,则只会创建一个新的子作用域。 由于我们有“正常”的原型继承,这就像ng-include和ng-switch一样,所以请谨慎对待父范围基元的双向数据绑定以及父范围属性的子范围隐藏/遮蔽。
  3. scope: { ... } - 该指令创建一个新的隔离/隔离范围。 它不是原型继承。 这通常是创建可重用组件时的最佳选择,因为该指令不会意外读取或修改父范围。 但是,这样的指令通常需要访问几个父范围属性。 对象散列用于设置父范围和隔离范围之间的双向绑定(使用'=')或单向绑定(使用'@')。 还有'&'绑定到父范围表达式。 所以,这些都创建派生自父范围的本地范围属性。 请注意,属性用于帮助设置绑定 - 您不能仅引用对象散列中的父级范围属性名称,您必须使用属性。 例如,如果要绑定到独立作用域中的父属性parentProp ,则这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"

    有关隔离范围的更多信息,请参阅http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true - 指令创建一个新的“transcluded”子范围,它从父范围原型继承。 transcluded和隔离范围(如果有的话)是兄弟姐妹 - 每个范围的$ parent属性引用相同的父范围。 当transcluded和isolate隔离区都存在时,隔离范围属性$$ nextSibling将引用transcluded作用域。 我不知道任何与transcluded范围的细微差别。
    对于下面的图片,假设与上述相同的指令: transclude: true

这个fiddle有一个showScope()函数,可以用来检查一个孤立和transcluded作用域。 请参阅小提琴中的评论中的说明。

概要

有四种类型的范围:

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

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

图表是用github上的graphviz “* .dot”文件生成的。 Tim Caswell的“ 用对象图学习JavaScript ”是使用GraphViz进行图表的灵感。

API参考范围页面显示

范围可以从父范围继承。

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

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

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


我决不想与马克的答案竞争,但只是想强调最终让所有东西都点击的那个片断,因为有人对Javascript继承和它的原型链有所了解。

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

myObject.prop = '123';

它不查找链条,但是当你设置

myObject.myThing.prop = '123';

在写入操作发生了一个微妙的读取,在写入其prop之前尝试查找myThing。 所以这就是为什么从孩子写入object.properties得到父对象的原因。







prototypal-inheritance