原文链接:Understanding Scopes

在AngularJS中,子作用域通常会原型继承于父作用域。这种情况的唯一例外是当一个指令设置了scope:{ ... }-- 这会创建一个孤立的作用域,该作用域不会进行原型继承。这种设置通常用于创建可复用组件。在指令中,默认情况下直接使用父作用域,这意味着,你在指令中作的任何改动都会同时改变父作用域。如果你设置scope:true(而不是scope:{ ... }),那么该指令会进行原型继承。

一般来说,作用域继承是很简单的,通常你甚至不需要知道它正在运作...直到你试图从子作用域中对父级作用域的基本类型数据(比如,数字,字符串,布尔值)进行数据双向绑定(即表单元素,ng-model指令)。这种做法通常不会符合我们的预期。这是因为子作用域会创建自身的属性,从而隐藏/遮蔽了父级作用域的同名属性。这种特性是JavaScript原型链运作原理,而不是AngularJS本身实现造成的。AngularJS初学者通常没有意识到,ng-repeatng-switchng-viewng-include所有这些指令都会创建一个子作用域,所以当执行这些指令时便会出现问题。

如果我们遵循记得在ng-model指令中使用'.'的“最佳实践”-- 值得花3分钟看看,我们能轻易地回避这个问题。Misko用ng-switch阐述了基本类型数据绑定的问题。

在你的ng-model指令中使用“.”能保证原型继承链起作用。所以,我们应该使用:

<input type="text" ng-model="someObj.prop1">

而不是:

"prop1">

如果你真的想或者真的需要用到基本类型数据,这里有两种变通方案:

  1. 在子作用域中使用$parent.parentScopeProperty,防止子作用域创建自身的属性
  2. 在父作用域中定义一个函数,并在子作用域中调用并传递基本类型数据给父作用域(并不是总能够做到)

JavaScript 原型继承

首先,我们要对JavaScript的原型继承有个良好的认知,这很重要,如果你有服务端编程的背景,更是如此。所以让我们先回顾一下原型继承的原理。

假设父级作用域有以下属性aStringaNumberanArrayanObjectaFunction。如果子作用域原型继承于父作用域,我们有:

当我们试图从子作用域中访问父作用域上定义的属性,JavaScript会先在子作用域上查询该属性,如果没有找到该属性,再访问父级作用域并查询该属性。(如果在父作用域中依旧没有找到这个属性,JavaScript会继续顺着原型链往上查找... 直到根作用域)。因此,以下均为true:

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

假设我们接下来进行以下操作:

childScope.aString = 'child string';

原型链并未被查询,而子作用域中新增了一个aString属性。这个新的属性隐藏/遮蔽了父作用域的同名属性。当我们下面讨论到ng-repeat指令和ng-include指令时,这特性会变得非常重要。

接下来假设我们执行:

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

因为在子作用域中没有找到anArrayanObject对象,所以原型链被查询了。在父作用域中被找到这两个对象,所以属性值被更新到了原始的对象上。子作用域上没有添加新的属性,也没有创建新的对象。(注意,在JavaScript中数组和函数都是对象)。

接着,假设我们这么做:

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

原形链并未被访问,并且子作用域获得了两个新的对象属性,这两个属性也会遮蔽父作用域上的同名属性。

顺便提一下:

  • 如果我们读取childScope.propertyX,并且子作用域有 propertyX 属性,那么原型链将不会被访问。
  • 如果我们设置childScope.propertyX,那么原型链也不会被访问。

最后一种情况:

delete childScope.anArray  
childScope.anArray[22  // true 

我们先删除子作用域的属性,然后当我们试图再次访问该属性,此时原型链会被访问。

Angular 作用域的继承

两种不同的情况:

  • 以下指令会创建新的作用域,而且原型继承父级作用域:ng-includeng-viewng-controller、带scope: true的指令、设置了transclude:true的指令
  • 以下指令会创建新的作用域,但不会原型继承:设置了scope: { ... }的指令。这指令创建的是孤立的作用域。

注意,通常情况下,即默认情况下scope:false,指令不会创建新的作用域。

ng-include

假设我们的控制器中有:

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

而且在我们的HTML中:

script "text/ng-template" id="/tpl1.html"> <input ng-model="myPrimitive"> </script>  
<div ng-include src="'/tpl1.html'"></div>  
<"/tpl2.html"> <input ng-model="myObject.aNumber"> </"'/tpl2.html'"></div>

每一个ng-include指令都生成一个新的子作用域,这些子作用域都原型继承于其父作用域。

在第一个输入框中输入77,子作用域将会得到一个新的myPrimitive属性,该属性会遮蔽了父作用域的同名属性。这可能不是你想要的。

在第二个输入框中输入99不会新建一个子作用域属性。因为tpl2.html绑定的数据是一个对象属性。当ngModel指令查询该对象,原型继承起到了作用,最终在父作用域中查找到该对象。

如果我们不想将我们的数据从基本类型改为对象,我们可以用$parent变量重写第一个模版:

"$parent.myPrimitive">

在该输入框中输入22不会生成一个新的子作用域属性。现在,这个模型是绑定在父级作用域的一个属性上(因为$parent是子作用域上指向父作用域的属性值)。

对于所有的作用域(无论是否原型继承),Angular总会通过$parent$$childHead`和`$$childTail记录下父-子关系(即一种层级关系)。以上的图表并没有展示这些属性值。

对于一些不涉及表单元素的情况,另一种解决方法是在父级作用域中定义一个函数用来修改基本类型数值。然后保证其子作用域都调用该函数,由于原型继承,其子作用域都能够访问的该函数。比如:

// in the parent scope
$scope.setMyPrimitive = function(value) {
    $scope.myPrimitive = value;
}

更多阅读:What is the angularjs way to databind many inputs?

ng-switch

ng-switch指令的作用域继承的运行原理就类似于ng-include指令。所以如果你需要对父级作用域中的一个基本类型值进行双向版定,你可以使用$parent,或者将数据模型改成对象的形式,然后绑定该对象上的属性。这可以避免子作用域遮蔽到了父作用域上的属性。

更多阅读:AngularJS,bind scope of a switch-case?

ng-repeat

ng-repeat指令的运行原理有点不一样。假设我们控制器中有:

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

而且我们的HMTL中:

ul>  
    <li ng-repeat="num in myArrayOfPrimitives">
       <"num"></input>
    </li>
</ul>  
<"obj in myArrayOfObjects">
       <"obj.num"></ul>

每次迭代,ng-repeat指令都会创建一个新的作用域,该作用会原型继承于其父级作用域,但是同时该指令会给这个新作用域的一个新的属性分配本次迭代对应数值。(这个属性的名称就是循环变量的名字)。以下就Angular源码中ng-repeat具体实现:

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

如果迭代项为基本类型,实质上把该值的拷贝分配给了子作用域新的属性。改变这个属性值(即子作用域的属性num)不会改变父作用域引用的数组。所以在上述第一个ng-repeat指令中,每个子作用域都获得一个独立于myArrayOfPrimitives数组的num属性:

这个ng-repeat指令不会如你期望搬工作。在Angular1.0.2及之前版本中,在输入框中输入,会改变灰色框框内的值,即子作用域的属性值。在Angular 1.0.3+版本,在文本框中输入不会有任何效果(参考Artem在stackOverflow上的解释)。我们想要的是,输入的值能改变myArrayOfPrimitives数组,而不是子作用域的属性值。为了实现这一点,我们需要将模型改成一个包含对象的数组。

所以,如果迭代元素是一个对象,那么分配到子作用域上的就是一个对原始对象的引用(而不是拷贝)。改变子作用域的属性值便会同时改变父级作用域引用的对象。所以在上述第二个ng-repeat指令中,我们有:

(我用灰色标记其中一条线,以便清晰展现它的指向)

这将如期工作。在文本框中的输入将改变灰色框框中的值,这将同时反映到子作用域和父级作用域中。

更多阅读:Difficulty with ng-model,ng-repeat,and inputs和What is the angularjs way to databind many inputs?

ng-view

待定,但我认为该指令和ng-include指令表现一致。

ng-controller

使用ng-controller指令嵌套控制器会造成常规的原型继承,就像ng-include指令和ng-switch指令,所以我们可以用相同的方法解决。然而,“通过作用域继承,在两个控制器中共享数据是一种非常糟糕的实现” --AngularJS Sticky Notes Pt 1 – Architecture,我们应该用服务在控制器之间共享数据。

(如果你真的要通过控制器的作用域继承来分享数据,你不需要做额外的工作。子作用域可以访问所有父级作用域的属性。更多阅读Controller load order differs when loading or navigating)。

指令

1.默认设置scope: false

指令不会新建一个作用域,所以这里不存在继承关系。这很简单,但同时也很危险,比如某指令中可能会创建一个新的属性,然而事实上,这个属性影响到了另一个已经存在的属性。对于书写可复用组件的指令来说,这不是一个好的选择。

2.scope: true

指令会创建一个新的子作用域,原型继承于父级作用域。如果多个指令(在同一个DOM元素上)请求新的作用域,那么只会创建一个作用域。因为涉及到原型继承,就像ng-includeng-switch,所以我们要谨慎对待父级作用域基本类型数据的双向绑定和子作用域遮掩父级作用域属性的问题。

3.scope: { ... }

指令会新建一个封闭的作用域。该作用域不会进行原型继承。这样的配置通常是你创建可复用组件的最好选择,因为这指令不会意外地读取或修改父级作用域。然而,有些指令通常需要访问父作用域的数据。设置对象是用来配置父作用域和封闭作用域之间的双向绑定(使用=)或单向绑定(使用@)。这里也可以使用&绑定父作用域上的表达式。所以,这些配置都会将来自父作用域的数据创建到本地作用域属性中。

要注意的是,这些配置选项只是用来设置绑定方式 -- 你只能运用Dom元素的属性引入父作用域的属性们,而不可以在配置选项中直接引用。比如你想将父作用域的属性parentProp绑定到封闭的作用域:<div my-directive>scope: { localProp: '@parentProp'},这不会起作用。我们必须用DOM元素属性定义指令需要绑定的每一个父作用域属性:<div my-directive the-Parent-Prop=parentProp>scope: { localProp: '@theParentProp' }

封闭作用域的__proto__引用的是一个Scope对象。封闭作用域的$parent指向父作用域,所以,虽然该作用域保持封闭而且不会原型继承于父作用域,但它依旧是一个子作用域。

对于下图我们有<my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">scope: { interpolatedProp: '@interpolated',twowayBindingProp: '=twowayBinding' },而且假设这个指令在link函数中进行如下操作:scope.someIsolateProp = "I'm isolated"

更多关于封闭作用域的信息请查阅:AngularJS Sticky Notes Pt 2 – Isolated Scope

4.transclude: true

指令新建一个用于"transclude(嵌入)"的子作用域,该作用域原型继承于父作用域。所以如果你嵌入的内容(指替换ng-transclude指令的内容)中需要对父作用域中的数据进行双向绑定,你应该使用$parent或把数据模型改成对象,然后把需要的属性绑定在这对象上。这样能够避免子作用域遮蔽父作用域的属性。

如果ng-transclude指令和封闭作用域是同级关系,那么它们各自作用域的$parent属性都指向同一个父作用域。如果ng-transclude指令和封闭作用域同时存在,那么封闭作用域上的$$nextSibling会指向ng-transclude作用域。

更多ng-transclude指令作用域的信息请查阅:Two way binding not working in directive with transcluded scope

假设上面的指令加上transclude:true,我们有下面这张图:

在Angular 1.3+中,此图稍有变化,TranscludedScope依旧继承自ParentScope,但是IsolateScope的$parent是ParentScope,TranscludedScope的$parent是IsolateScope,而且IsolateScope也没有$$nextSibling指向TranscludedScope

关于TranscludeScope和IsolateScope可以查看StackoverFlow上的回答。

总结

一共有3种类型的作用域:

  1. 常规的原型继承的作用域 --ng-include,ng-switch,244)">ng-controller,设置了scope: true的指令。
  2. 封闭作用域 -- 设置scope: {...}的指令。这种作用域没有原型继承,但=,244)">`@,和&提供了一套通过元素属性访问父作用域的机制。
  3. transclude作用域 -- 设置了transclude: true的指令。这种作用域也是常规的原型继承,但它和任何封闭作用域是同级关系。
    对于所有作用域(无论是否原型继承),Angular都会通过作用域的属性$$childHead$$childTail记录下父-子关系。

理解Angular的作用域译的更多相关文章

  1. ios – 不同作用域中相同命名常量的链接器错误

    我有一个名为“ID_KEY”的常量,它在3个单独的.m文件的顶部声明,其中没有包含其他文件.声明如下:而其他两个类也是如此.但是我收到一个链接器错误抱怨同名的多个定义.我的问题是为什么链接器抱怨这个呢?

  2. 15.6 Swift局部引用

    /**局部引用和全局引用1.作用域2.生命周期*/varref:Int=Int.init/**定义一个变量或者常量,如果不是可选类型的话,一定要有初始值。所谓的局部引用就是在代码块里面的引用就是局部引用。作用域生命周期都在该代码块中;离它最近的括号*/iftrue{varref:Student=Student.initref.name="ZHangsan"}//超出作用域啦//ref.name="ZHangsan"

  3. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  4. 一起来了解JavaScript的变量作用域

    这篇文章主要为大家详细介绍了JavaScript变量作用域,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助

  5. Angular的MVC和作用域

    本文主要Angular的MVC和作用域进行详细分析介绍,具有一定的参考价值,下面跟着小编一起来看下吧

  6. Javascript学习笔记之函数篇(六) : 作用域与命名空间

    本文主要讲述了javascript中作用域和命名空间的区别,十分的详细,这里推荐给大家,希望小伙伴能有所收获

  7. JavaScript基础之作用域

    这篇文章主要介绍了如何理解JavaScript中的作用域,帮助大家更好的学习JavaScript,感兴趣的朋友可以了解下

  8. Angularjs使用ng-repeat中$even和$odd属性的注意事项

    无可否认angularjs的崛起成为前端很大的福利,最近接到项目,框架便选中了angularjs。angularjs最吸引人的地方就是数据的双向绑定和指令了,这篇文章主要介绍了Angularjs中使用ng-repeat的$even和$odd属性的注意事项,需要的朋友可以参考下

  9. 解读Spring Bean的作用域

    这篇文章主要介绍了解读Spring Bean的作用域,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  10. Angularjs全局变量被作用域监听的正确姿势

    这篇文章主要介绍了Angularjs全局变量被作用域监听的正确姿势的相关资料,需要的朋友可以参考下

随机推荐

  1. Angular2 innerHtml删除样式

    我正在使用innerHtml并在我的cms中设置html,响应似乎没问题,如果我这样打印:{{poi.content}}它给了我正确的内容:``但是当我使用[innerHtml]=“poi.content”时,它会给我这个html:当我使用[innerHtml]时,有谁知道为什么它会剥离我的样式Angular2清理动态添加的HTML,样式,……

  2. 为Angular根组件/模块指定@Input()参数

    我有3个根组件,由根AppModule引导.你如何为其中一个组件指定@input()参数?也不由AppModalComponent获取:它是未定义的.据我所知,你不能将@input()传递给bootstraped组件.但您可以使用其他方法来做到这一点–将值作为属性传递.index.html:app.component.ts:

  3. angular-ui-bootstrap – 如何为angular ui-bootstrap tabs指令指定href参数

    我正在使用角度ui-bootstrap库,但我不知道如何为每个选项卡指定自定义href.在角度ui-bootstrap文档中,指定了一个可选参数select(),但我不知道如何使用它来自定义每个选项卡的链接另一种重新定义问题的方法是如何使用带有角度ui-bootstrap选项卡的路由我希望现在还不算太晚,但我今天遇到了同样的问题.你可以通过以下方式实现:1)在控制器中定义选项卡href:2)声明一个函数来改变控制器中的散列:3)使用以下标记:我不确定这是否是最好的方法,我很乐意听取别人的意见.

  4. 离子框架 – 标签内部的ng-click不起作用

    >为什么标签标签内的按钮不起作用?>但是标签外的按钮(登陆)工作正常,为什么?>请帮我解决这个问题.我需要在点击时做出回复按钮workingdemo解决方案就是不要为物品使用标签.而只是使用divHTML

  5. Angular 2:将值传递给路由数据解析

    我正在尝试编写一个DataResolver服务,允许Angular2路由器在初始化组件之前预加载数据.解析器需要调用不同的API端点来获取适合于正在加载的路由的数据.我正在构建一个通用解析器,而不是为我的许多组件中的每个组件设置一个解析器.因此,我想在路由定义中传递指向正确端点的自定义输入.例如,考虑以下路线:app.routes.ts在第一个实例中,解析器需要调用/path/to/resourc

  6. angularjs – 解释ngModel管道,解析器,格式化程序,viewChangeListeners和$watchers的顺序

    换句话说:如果在模型更新之前触发了“ng-change”,我可以理解,但是我很难理解在更新模型之后以及在完成填充更改之前触发函数绑定属性.如果您读到这里:祝贺并感谢您的耐心等待!

  7. 角度5模板形式检测形式有效性状态的变化

    为了拥有一个可以监听其包含的表单的有效性状态的变化的组件并执行某些组件的方法,是reactiveforms的方法吗?

  8. Angular 2 CSV文件下载

    我在springboot应用程序中有我的后端,从那里我返回一个.csv文件WheniamhittingtheURLinbrowsercsvfileisgettingdownloaded.现在我试图从我的角度2应用程序中点击此URL,代码是这样的:零件:服务:我正在下载文件,但它像ActuallyitshouldbeBook.csv请指导我缺少的东西.有一种解决方法,但您需要创建一个页面上的元

  9. angularjs – Angular UI-Grid:过滤后如何获取总项数

    提前致谢:)你应该避免使用jQuery并与API进行交互.首先需要在网格创建事件中保存对API的引用.您应该已经知道总行数.您可以使用以下命令获取可见/已过滤行数:要么您可以使用以下命令获取所选行的数量:

  10. angularjs – 迁移gulp进程以包含typescript

    或者我应该使用tsc作为我的主要构建工具,让它解决依赖关系,创建映射文件并制作捆绑包?

返回
顶部