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


API参考范围页面 说:

范围 能够 从父范围继承。

开发者指南范围页面 说:

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

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


967
2017-12-27 04:48


起源


原型 实际上是一个词。 - Peter Mortensen


答案:


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

至于细微差别,范围继承通常是直接的...直到你需要 双向数据绑定 在子范围内(即表格元素,ng-模型)。如果你尝试绑定到一个,Ng-repeat,ng-switch和ng-include会让你失望 原始 (例如,number,string,boolean)父范围内的子范围内的数字,字符串,布尔值。它不像大多数人期望的那样工作。子作用域获取其自己的属性,该属性隐藏/隐藏同名的父属性。你的解决方法是

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

新的AngularJS开发人员通常没有意识到这一点 ng-repeatng-switchng-viewng-include 和 ng-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将首先查看子作用域,而不是找到该属性,然后查看继承的作用域,并找到该属性。 (如果它没有在parentScope中找到该属性,它将继续原型链......一直到根范围)。所以,这些都是真的:

childScope.aString === 'parent string'
childScope.anArray[1] === 20
childScope.anObject.property1 === 'parent prop1'
childScope.aFunction() === 'parent output'

假设我们这样做:

childScope.aString = 'child string'

不参考原型链,并且向childScope添加了新的aString属性。 此新属性使用相同的名称隐藏/隐藏parentScope属性。  当我们在下面讨论ng-repeat和ng-include时,这将变得非常重要。

property hiding

假设我们这样做:

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

查询原型链是因为在childScope中找不到对象(anArray和anObject)。可以在parentScope中找到对象,并在原始对象上更新属性值。没有新的属性添加到childScope;没有创建新对象。 (注意,在JavaScript数组和函数中也是对象。)

follow the prototype chain

假设我们这样做:

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

未查询原型链,子范围获取两个新对象属性,这些属性隐藏/隐藏具有相同名称的parentScope对象属性。

more property hiding

小贴士:

  • 如果我们读取childScope.propertyX,并且childScope具有propertyX,则不会查询原型链。
  • 如果我们设置childScope.propertyX,则不会查询原型链。

最后一个场景:

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

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

after removing a child property


角度范围继承

竞争者:

  • 以下创建新范围,并继承原型:ng-repeat,ng-include,ng-switch,ng-controller,指令 scope: true,指令 transclude: true
  • 下面创建一个新范围,它不会继承原型:directive with 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 scope属性,用于隐藏/隐藏同名的父作用域属性。这可能不是你想要/期望的。

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

NG-开关

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

也可以看看 AngularJS,绑定switch-case的范围?

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-model,因此使用子范围 num 更改父作用域引用的数组。所以在上面的第一个ng-repeat中,每个子范围都得到一个 num 独立于myArrayOfPrimitives数组的属性:

ng-repeat with primitives

这种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/ 应该使用服务来代替控制器之间共享数据。

(如果您确实希望通过控制器范围继承来共享数据,则无需执行任何操作。子范围将可以访问所有父范围属性。 也可以看看 加载或导航时控制器加载顺序不同

指令

  1. 默认(scope: false) - 该指令不创建新范围,因此这里没有继承。这很容易,但也很危险,因为,例如,指令可能认为它正在范围内创建新属性,而实际上它正在破坏现有属性。对于编写旨在作为可重用组件的指令,这不是一个好的选择。
  2. scope: true  - 该指令创建一个新的子作用域,它原型继承自父作用域。如果多个指令(在同一DOM元素上)请求新范围,则只创建一个新的子范围。由于我们具有“正常”原型继承,这类似于ng-include和ng-switch,因此要警惕双向数据绑定到父作用域基元,以及子作用域隐藏/遮蔽父作用域属性。
  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"
      isolated scope
    有关隔离范围的更多信息,请参阅 http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true - 该指令创建一个新的“transcluded”子作用域,它原型继承自父作用域。 transcluded和隔离范围(如果有)是兄弟 - 每个范围的$ parent属性引用相同的父范围。当transcluded和isolate范围都存在时,隔离范围属性$$ nextSibling将引用transcluded范围。我不知道转换范围有任何细微差别。
    对于下图,假设与上述相同的指令: transclude: true
    transcluded scope

这个 小提琴 有一个 showScope() 可用于检查隔离和转换范围的函数。请参阅小提琴中的注释中的说明。


概要

范围有四种类型:

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

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

用图表生成图表  “* .dot”文件已打开 github上。蒂姆卡斯韦尔的“使用对象图学习JavaScript“是图表使用GraphViz的灵感来源。


1696
2017-12-27 04:48



很棒的文章,对于SO答案来说太长了,但无论如何都非常有用。请在编辑将其缩小到适当大小之前将其放在您的博客上。 - iwein
我把副本放在了 AngularJS维基。 - Mark Rajcok
更正:“隔离范围 __proto__ 引用对象。“应该是”隔离范围 __proto__ 引用一个Scope对象。“因此,在最后两张图片中,橙色的”对象“框应该是”范围“框。 - Mark Rajcok
这个asnwer应该包含在angularjs指南中。这更加残酷...... - Marcelo De Zen
维基让我感到困惑,首先它写道:“原型链被咨询,因为在childScope中找不到该物体。”然后它写道:“如果我们设置childScope.propertyX,则不会查询原型链。”第二个意味着一个条件,而第一个意味着没有。 - Stephane


我绝不想与马克的答案竞争,但只想强调最终使所有内容都像新手一样点击的作品 Javascript继承及其原型链

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

myObject.prop = '123';

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

myObject.myThing.prop = '123';

在写操作中有一个微妙的读取 试图在写入其道具之前查找myThing。这就是为什么从子节点写入object.properties会获取父节点的对象。


136
2018-05-16 15:22



虽然这是一个非常简单的概念,但它可能不是很明显,因为我相信很多人都会错过它。说得好。 - moljac024
优秀的评论。我带走了,非对象属性的解析不涉及读取,而对象属性的解析则涉及读取。 - Stephane
为什么?财产写作没有上升到原型链的动机是什么?这看起来很疯狂...... - Jonathan.
@Jonathan。这符合Javascript原型的工作原理。你的问题适用于JS的设计,而不是Angular的设计 - Greg
如果添加一个真实的简单示例,那就太好了。 - tylik


我想用javascript向@Scott Driscoll回答添加一个原型继承的例子。我们将使用经典的继承模式与Object.create(),这是EcmaScript 5规范的一部分。

首先我们创建“父”对象函数

function Parent(){

}

然后将原型添加到“父”对象功能

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

创建“Child”对象功能

function Child(){

}

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

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

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

Child.prototype.constructor = Child;

将方法“changeProps”添加到子原型,它将重写Child对象中的“原始”属性值,并在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


18
2017-11-08 22:45