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

API参考范围页面表示:

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

开发者指南范围页面表示:

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

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

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

至于细微差别,作用域继承通常很简单……直到你在子作用域中需要双向数据绑定(即表单元素、ng-model)。如果你尝试从子作用域内绑定到父作用域中的原始(例如,数字、字符串、布尔值),则NG重复、ng开关和ng包含会让你出错。它的工作方式并不像大多数人期望的那样。子作用域有自己的属性,隐藏/阴影同名的父属性。你的解决方法是

  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 wiki上:https://github.com/angular/angular.js/wiki/Understanding-Scopes

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

假设父母范围具有属性aString、aNumber、anArray、anObject和aFunction。如果子范围原型通常继承自父母范围,我们有:

原型继承

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

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

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

假设我们这样做:

childScope.aString = 'child string'

不参考原型链,并将一个新的aString属性添加到子范围中。这个新属性隐藏/阴影具有相同名称的父母范围属性。当我们在下面讨论ng-重复和ng-包含时,这将变得非常重要。

隐藏属性

假设我们这样做:

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

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

跟随原型链

假设我们这样做:

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

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

更多属性隐藏

外卖:

  • 如果我们读childScope.propertyX,而ChilScope有属性X,那么就不会参考原型链。
  • 如果我们设置childScope.propertyX,则不会参考原型链。

最后一个场景:

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

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

删除子属性后


角范围继承

竞争者:

  • 以下代码创建新的作用域,并继承原型:ng-重复、ng-包含、ng-开关、ng-控制器、带有scope: true的指令、带有transclude: true的指令。
  • 下面创建了一个不继承原型的新范围:带有scope: { ... }的指令。这将创建一个“隔离”范围。

请注意,默认情况下,指令不创建新范围——即默认值为scope: false

ng-包含

假设我们在控制器中:

$scope.myPrimitive = 50;$scope.myObject    = {aNumber: 11};

在我们的超文本标记语言中:

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

ng包含子作用域

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

ng-包含一个基元

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

ng包含一个对象

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

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

在这个输入文本框中键入(例如,"22")不会导致新的子属性。模型现在绑定到父作用域的属性(因为$父是引用父作用域的子作用域属性)。

ng-包括$父元素

对于所有的作用域(无论原型与否),Angular总是通过作用域属性$父,$子和$子跟踪父子关系(即层次结构)。我通常不会在图表中显示这些作用域属性。

对于不涉及表单元素的场景,另一种解决方案是在父作用域上定义一个函数来修改原语。然后确保子始终调用此函数,由于原型继承,该函数将可用于子作用域。例如,

// 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-包括一样。因此,如果你需要双向数据绑定到父作用域中的原语,请使用$父,或者将模型更改为对象,然后绑定到该对象的属性。这将避免父作用域属性的子作用域隐藏/阴影。

另见AngularJS,绑定开关案例的范围?

ng-重复

NG重复的工作方式略有不同。假设我们在控制器中:

$scope.myArrayOfPrimitives = [ 11, 22 ];$scope.myArrayOfObjects    = [{num: 101}, {num: 202}]

在我们的超文本标记语言中:

<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-重复创建一个新作用域,该作用域原型继承自父作用域但它也将项目的值分配给新子作用域上的新属性。(新属性的名称是循环变量的名称。)这是ng-重复的Angular源代码的实际内容:

childScope = scope.$new();  // child scope prototypically inherits from parent scope...childScope[valueIdent] = value;  // creates a new childScope property

如果Item是基元(如在myArrayOfPrimitics中),本质上是将值的副本分配给新的子作用域属性。更改子作用域属性的值(即使用ng-model,因此子作用域num)确实会没有更改父作用域引用的数组。因此,在上面的第一个ng-重复中,每个子作用域都获得一个独立于myArrayOfPrimi的num属性数组:

带有原语的ng-重复

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

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

ng-重复对象

(我将一行涂成灰色,以便清楚它要去哪里。

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

请参阅ng-模型、ng-重复和输入的困难https://stackoverflow.com/a/13782671/215945

ng控制器

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

(如果您真的想通过控制器范围继承共享数据,则无需做任何事情。子范围将有权访问所有父范围属性。请参阅加载或导航时控制器加载顺序不同

指令

  1. default(scope: false)-指令不创建新作用域,因此此处没有继承。这很容易,但也很危险,因为,例如,指令可能认为它在作用域上创建了一个新属性,而实际上它正在破坏现有属性。这不是编写旨在作为可重用组件的指令的好选择。
  2. scope: true-指令创建一个从父作用域原型继承的新子作用域。如果多个指令(在同一DOM元素上)请求一个新作用域,则只会创建一个新的子作用域。由于我们有“正常”原型继承,这就像ng-包括和ng-Switch,因此要警惕与父作用域原语的双向数据绑定,以及父作用域属性的子作用域隐藏/阴影。
  3. scope: { ... }-指令创建一个新的隔离/隔离作用域。它不原型继承。这通常是创建可重用组件时的最佳选择,因为指令不能意外读取或修改父作用域。然而,此类指令通常需要访问一些父作用域属性。对象哈希用于在父作用域和隔离作用域之间设置双向绑定(使用'=')或单向绑定(使用'@')。还有'&'用于绑定到父作用域表达式。因此,这些都创建了从父作用域派生的局部作用域属性。请注意,属性用于帮助设置绑定——您不能只在对象哈希中引用父范围属性名称,您必须使用一个属性。例如,如果您想绑定到隔离范围中的父属性parentProp<div my-directive>scope: { localProp: '@parentProp' },这将不起作用。必须使用一个属性来指定指令想要绑定到的每个父属性:<div my-directive the-Parent-Prop=parentProp>scope: { localProp: '@theParentProp' }
    隔离作用域的__proto__引用Object。隔离作用域的$父作用域引用父作用域,因此尽管它是隔离的并且不从父作用域继承原型,但它仍然是子作用域。
    对于下面的图片,我们有
    <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-该指令创建了一个新的“转移”子作用域,它通常从父作用域继承。转移和隔离作用域(如果有)是兄弟姐妹-每个作用域的$父属性引用相同的父作用域。当转移作用域和隔离作用域都存在时,隔离作用域属性$nextSible将引用转移作用域。我不知道转移作用域有任何细微差别。
    对于下面的图片,假设与上面相同的指令添加:transclude: true
    独立作用域

小提琴有一个showScope()函数,可用于检查隔离和隔离作用域。请参阅小提琴注释中的说明。


总结

有四种类型的范围:

  1. 标准原型作用域继承——ng-包括、ng-Switch、ng-控制器、带有scope: true的指令
  2. 带有复制/赋值的普通原型作用域继承——ng-重复。ng-重复的每次迭代都会创建一个新的子作用域,并且新的子作用域总是获得一个新属性。
  3. 使用scope: {...}隔离作用域-指令。这不是原型,但 '=', '@', 和'&'提供了一种通过属性访问父作用域属性的机制。
  4. 转包的作用域——带有transclude: true的指令。这个也是正常的原型作用域继承,但它也是任何隔离作用域的兄弟。

对于所有作用域(无论是否为原型),Angular总是通过属性$父级和$子级和$子级跟踪父子关系(即层次结构)。

图表是用“*. dot”文件生成的,这些文件位于github上。Tim Caswell的“使用对象图学习JavaScript”是使用GraphViz制作图表的灵感。

我绝不想与马克的答案竞争,但只是想突出显示最终使所有内容都成为JavaScript继承及其原型链的新人的作品。

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

myObject.prop = '123';

它不会向上看链子,但是当你设置

myObject.myThing.prop = '123';

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

我想在@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.prototype.constructor = Child;

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

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

启动父(父)和子(子)对象。

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

Call Children(son)change eProps方法

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