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

IT技术 javascript angularjs inheritance prototype prototypal-inheritance
2020-12-23 22:59:12

API参考范围页面说:

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

开发者指南范围页说:

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

  • 那么,子作用域是否总是原型继承自其父作用域?
  • 有例外吗?
  • 当它确实继承时,它总是正常的 JavaScript 原型继承吗?
3个回答

快速回答
子作用域通常原型继承自其父作用域,但并非总是如此。此规则的一个例外是指令 with 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-models 中——观看 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'

不参考原型链,在 childScope 中添加了一个新的 aString 属性。 这个新属性隐藏/隐藏了同名的 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' }

不咨询原型链,子作用域获得两个新的对象属性,它们隐藏/隐藏具有相同名称的父作用域对象属性。

更多的财产隐藏

要点:

  • 如果我们读取childScope.propertyX,childScope有propertyX,那么就不用参考原型链了。
  • 如果我们设置childScope.propertyX,则不参考原型链。

最后一个场景:

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

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

删除子属性后


Angular 范围继承

参赛者:

  • 以下创建新的作用域,并原型继承:ng-repeat、ng-include、ng-switch、ng-controller、directive with scope: true、directive with 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 都会生成一个新的子作用域,该子作用域通常从父作用域继承。

ng-include 子作用域

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

带有原语的 ng-include

在第二个输入文本框中键入(例如“99”)不会产生新的子属性。因为 tpl2.html 将模型绑定到一个对象属性,当 ngModel 寻找对象 myObject 时,原型继承就会启动——它在父作用域中找到它。

ng-包含一个对象

如果我们不想将模型从原始模型更改为对象,我们可以重写第一个模板以使用 $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-switch

ng-switch 作用域继承就像 ng-include 一样工作。因此,如果您需要对父作用域中的原语进行 2 向数据绑定,请使用 $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-模型,因此子范围num)并不会改变阵列父范围引用。所以在上面的第一个 ng-repeat 中,每个子作用域都获得一个num独立于 myArrayOfPrimitives 数组属性:

ng-repeat 与原语

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

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

ng-repeat 对象

(我将一条线涂成灰色,以便清楚它的去向。)

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

另请参阅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. default ( scope: false) - 该指令不会创建新的作用域,因此这里没有继承。这很容易,但也很危险,因为例如,指令可能认为它正在范围内创建一个新属性,而实际上它正在破坏现有属性。对于编写旨在作为可重用组件的指令,这不是一个好的选择。
  2. scope: true- 该指令创建一个新的子作用域,该子作用域原型继承自父作用域。如果多个指令(在同一个 DOM 元素上)请求一个新的作用域,则只会创建一个新的子作用域。由于我们有“正常”的原型继承,这就像 ng-include 和 ng-switch,所以要小心 2-way 数据绑定到父作用域原语,以及父作用域属性的子作用域隐藏/阴影。
  3. scope: { ... }- 该指令创建一个新的隔离/隔离范围。它不典型地继承。这通常是创建可重用组件时的最佳选择,因为指令不会意外读取或修改父作用域。但是,此类指令通常需要访问一些父作用域属性。对象哈希用于在父作用域和隔离作用域之间设置双向绑定(使用“=”)或单向绑定(使用“@”)。还有 '&' 可以绑定到父作用域表达式。因此,这些都创建了从父作用域派生的本地作用域属性。请注意,属性用于帮助设置绑定——您不能只在对象哈希中引用父作用域属性名称,您必须使用属性。例如,如果您想绑定到父属性,这将不起作用parentProp在隔离范围内:<div my-directive>scope: { localProp: '@parentProp' }必须使用属性来指定指令要绑定到的每个父属性:<div my-directive the-Parent-Prop=parentProp>scope: { localProp: '@theParentProp' }
    隔离作用域的__proto__引用对象。Isolate 作用域的 $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- 该指令创建一个新的“transcluded”子作用域,该子作用域原型继承自父作用域。嵌入的和隔离的作用域(如果有)是同级的——每个作用域的 $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 上Tim Caswell 的“ Learning JavaScript with Object Graphs ”是将 GraphViz 用于图表的灵感来源。

维基让我感到困惑,首先它写着:“参考原型链,因为在 childScope 中找不到对象。” 然后它写着:“如果我们设置childScope.propertyX,则不咨询原型链。”。第二个暗示了一个条件,而第一个则没有。
2021-02-21 22:59:12
更正:“隔离作用域的__proto__引用对象。” 应该改为“隔离范围的__proto__引用范围对象。” 因此,在最后两张图片中,橙色的“对象”框应该是“范围”框。
2021-02-23 22:59:12
这个答案应该包含在 angularjs 指南中。这更说教...
2021-02-23 22:59:12
很棒的文章,对于 SO 答案来说太长了,但无论如何都非常有用。请在编辑将其缩小之前将其放在您的博客上。
2021-03-01 22:59:12
我在AngularJS wiki上放了一份副本
2021-03-05 22:59:12

我绝不想与 Mark 的答案竞争,而只是想突出显示最终使所有内容都被点击的部分,因为Javascript 继承及其原型链的新手

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

myObject.prop = '123';

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

myObject.myThing.prop = '123';

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

虽然这是一个非常简单的概念,但它可能不是很明显,因为我相信很多人都错过了它。说得好。
2021-02-09 22:59:12
如果您添加了一个真正简单的示例,那就太好了。
2021-02-11 22:59:12
请注意,它确实在原型链搜索setter如果什么也没找到,它会在接收器上创建一个属性。
2021-02-13 22:59:12
为什么?属性写入不上升到原型链的动机是什么?好像疯了...
2021-02-21 22:59:12
很棒的评论。我带走了,非对象属性的解析不涉及读取,而对象属性的解析则涉及。
2021-03-07 22:59:12

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

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

function Parent(){

}

然后给“Parent”对象函数添加一个原型

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

创建“子”对象函数

function Child(){

}

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

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

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

Child.prototype.constructor = Child;

将方法“changeProps”添加到子原型,这将重写子对象中的“原始”属性值并更改子对象和父对象中的“object.one”值

Child.prototype.changeProps = function(){
    this.primitive = 2;
    this.object.one = 2;
};

启动 Parent(爸爸)和 Child(儿子)对象。

var dad = new Parent();
var son = new Child();

调用Child(son)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

关于原型继承的这三个答案,我最喜欢的部分是答案本身是相互继承的
2021-03-04 22:59:12