深入理解ng里的scope

Feenan's Blog-[译]

摘要

在ng里面,一个子作用域通常原型继承它的父作用域,有一个例外的情况就是,当使用指令的时候,利用scope:{}这个属性会创建一个独立的作用域,而且也不会继承父作用域,这种特例一般用在创建可重用的指令情况下.默认情况下指令中是可以直接使用父级作用域的,而且修改指令中的作用域也会同步更新父级作用域的.当在指令中用scope:true来替换scope:{}的话,则会创建一个新的作用域且原型继承父作用域.

scope里的原型继承比较容易理解,一般情况下都不需要你去了解它的实现,但是当你在子作用域里绑定父作用域里的基本数据类型(比如,整型,字符串,布尔型)的时候,这种情况下就会出现问题,你会发现它并没有像你指望的那样去运行,当修改子作用域里的基本数据类型时,并不会修改父作用域,而是在子作用域里创建一个新的属性,这并不是ng干的,这只是js里原型继承所导致的,关于这个问题,可以看看这个例子

对于这种基本类型的问题很容易去避免,大家可以看看这个视频,值得你去花三分钟看看,通常在你的父级作用域使用.来处理基本数据模型,本质就是对像的原型继承会保持同一个引用

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

上面的用法要比下面的这个好

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

如果你真的需要直接使用基本数据类型,可以使用下面两种方法:

  • 在子作用域中使用$parent.parentScopeProperty,因为每个子作用域都有$parent指向它的父级,不管是否原型继承,本质还是利用对象引用的唯一性.

  • 在父作用域定义一个函数,利用函数来保持父作用域里的基本数据类型同步,然后在子作用域里绑定此函数,参数为基本数据类型.

下面列出下面将要讲的功能

  • Javascript 原型继承
  • Angularjs 作用域 继承
    • ng-include
    • ng-switch
    • ng-repeat
    • ng-view
    • ng-controller
    • directive

Javascript 原型继承

对js的原型继承有一个整体的理解比较重要,尤其是从后端转过来的,他们通常以类继承为主,所以先说说js里的原型继承.

假设父作用域parentScope里有aString,aNumber,aArray,aObject,还有一个aFunction这些属性,如果一个子作用域childScope原型继承它的话,下面是两者之前的关系图

(注意:上图中的灰色区域代表基础数据类型,蓝色和绿色区域代表引用类型)

如果我们在子作用域里访问一个属性,js首先会从本身上面查看,没找到的话则从父作用域里查找,还没有的话,会从原型链一直查找直到根作用域,js里的根作用域为Object,所以下面的代码都是true:

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

假如我们这样做的话:

childScope.aString = 'child string';

这里并不会访问父作用域的原型链,而是直接在子作用域上面创建同名的属性,会覆盖住父作用域里同名属性,了解这点非常重要,因为下面要讲的ng-repeat和ng-include也有这问题

111.png

但是假如我们像下面这样做的话:

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

上面的代码将会访问父作用域的原型链,因为对象(anArray和anObject)在子作用域里没有找到,而是在父作用域里找到了,所以这里会同步更新原始引用里的值,并不会在子作用域上面创建新的对象或者数组(注意这里的aFunction在js里也是对象).

上面的本质是:因为访问的只是同一引用里的单个元素,而不是直接访问这个引用,所以并没有创建新的东西

再看看下面的例子:

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

上面的代码并没有访问父作用域的原型链,只是在子作用域上面新加了两个属性,一个数组,一个对象,看下图

小贴士:

  • 如果我们读取childScope.propertyX,并且childScope上面有propertyX属性的话,则不会访问父级的原型链

  • 如果我们写childScope.propertyX属性,也不会访问父级的原型链

看最后一个脚本:

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

我们先删除子作用域上的数组对象,然后又访问它,这将会访问原型链,如下图:

这里有个例子,描述上面代码里的原型链的情况,通过修改然后看它的输出结果(打开的你开发工具,查看console里的输出,这里将会显示出rootScope).

Angular 作用域继承

摘要:

  • ng-repeat,ng-include,ng-switch,ng-view,ng-controller,带有scope:true或者transclude:true的指令,这些将会创建子作用域,并且原型继承父作用域.

  • 带有scope:{...}的指令将会创建子作用域,但不会继承父作用域,它是独立的作用域.

注意,默认的指令并不会创建子作用域,因为scope默认的属性为false.

ng-include

假如我们有一个控制器,包含下面代码:

$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的话,并没有在子作用域里创建新的属性,只是修改了父作用域里的对象里的值,这是因为这里绑定的是对象的一个属性,这会造成访问父作用域的原型链,修改之后的关系图如下:

如果不想修改第一个模板里绑定的基本数据类型为对象的话,可以使用$parent属性来访问,修改代码如下:

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

然后我们在第一个输入框中输入22,跟期望的一样,并没有创建新的属性,而且也修改了父作用域里的基本数据类型,因为这里子作用域绑定的是$parent引用,本质还是利用对象引用的唯一性,如图

对于所有的作用域来说(不管是否是原型继承),ng为了跟综各个作用域之间的关系,在每个scope上都增加了一个$parent,$$childHead,$$childTail属性来描述它的父级,第一个子级以及最后一个子级,上面的图里一般都没有画上这些属性.

另外还有一种方法用来处理在子作用域里同步修改父级作用域里的基本数据类型,就是在父级作用域里定义一个函数来修改基本数据类型模型,例如:

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

这里有一个使用父作用域函数的例子,sample jsFiddle(关于这里的部分内容也可以看stack overflow post).

同样也可以参考这两篇文章,http://stackoverflow.com/a/13782671/215945https://github.com/angular/angular.js/issues/1267.

ng-switch

ng-switch的原型继承跟ng-include差不多,所以想要在子作用域里使用基本数据类型的话,可以采用$parent,或者绑定一个对象(这个对象下面有这个基本数据类型的属性),这些都可以避免子作用域里的属性隐藏父级作用域的问题.

同样也可以参考这篇文章,AngularJS, bind scope of a switch-case?

ng-repeat

ng-repeat跟上面的指令有一些不同,先来看看下面的代码,假如我们有一个控制器,然后包含下面这些内容:

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

同时也有下面的html代码:

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


每项内容或者每次迭代,ng-repeat都会创建一个新的子作用域并且原型继承于父作用域,不过这里还有会做另外一件事,就是给每个子作用域添加一个新的属性,属性名称就是ng-repeat里的loop的变量名,下面显示的是ng源码里针对ng-repeat实现的一部分

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

如果每项内容是一个基本数据类型的话(类似于上面代码里的myArrayOfPrimitives),本质上只是复制它的值到这个新增的子作用域属性上,改变这个值(像上面的ng-model里的num),并不会修改父作用域数组引用里的值,所以上面第一个ng-repeat里的num都是独立于myArrayOfPrimitives这个数组的,关系图如下:

这里的第一个ng-repeat并没有像期望中的那样运行,在angular 1.0.2或者更早的版本里,试图在上面两个灰色的输入框里输入值的话,默认没有任务效果(可以点击StackOverflow查看原因),假如想第一个ng-repeat能够按照你的期望运行话,我们需要修改里的数组元素为数组对象.

所以,如果数组每项内容是一个对象的话,则在子作用域里新加的属性是指向原始数组对象的一个引用,修改这个内容的话(像上面的obj.num),将会同步修改父作用域里对应的数组项,上面的第二个ng-repeat修改之后的关系图如下:

(上图中加了一条灰色的线来区分不同的ng-repeat项)

这样就会跟期望中的那样运行,试图修改上图灰色框里的输入框的值,结果为同时修改子作用域与父作用域里的值.

同样可以参考这两篇文章, Difficulty with ng-model, ng-repeat, and inputshttp://stackoverflow.com/a/13782671/215945

ng-view

待定,不过这个应该跟ng-include差不多.

ng-controller

嵌套的控制器通常都是原型继承的,就跟ng-include和ng-switch一样,所以都可以按照同样的技巧来使用,不过,这里有一个不太好的使用例子,就是在两个控制器之前共享数据---点击这里查看,其实可以利用ng里的service来在多个控制器之间共享数据

(如果你真的想利用控制器作用域的继承来实现数据共享的话,其实你不需要做任何事情,因为子作用域可以访问所有父作用域里的属性,想了解更多可以看这里Controller load order differs when loading or navigating).

directives

  • 1.默认(scope:false)的情况下,指令不会创建任务的作用域,不存在原型继承,所以使用起来非常简单,不过要注意的是,当在指令里创建一个属性的话,有可能跟父级的同名从而破坏它,当你想创建一个可重用的指令的时候这不是一个最佳选择.

  • 2.scope:true的时候,指令会创建一个新的子作用域,并且原型继承于父作用域,如果多个相同的指令(同一个dom元素)请求一个新的作用域的话,这里只会创建一个新的子作用域.因为我们这里采用的是普通的原型继承,就跟ng-include和ng-switch一样,当双向绑定父作用域里的基本数据类型的时候,要警惕隐藏同名父级作用域属性的问题.

  • 3.scope:{...}的时候,指令会创建一个独立的子作用域,它不会原型继承父作用域,通常这是构建可重用组件的最佳选择,因为它不会意外的去读写父作用域的属性.不过有些时候,这些独立的作用域也需要访问父作用域的信息,这时候可以在scope的对象属性添加下面三个标识:

    • =,这个会双向绑定子作用域与父作用域的属性
    • @,这个只会读取父作用域里的属性
    • &,这个会绑定父作用域里的表达式这三种标识都会在子作用域里创建本地属性,通过对父作用域属性的导出,不过上面三种绑定标识都需要attributes来安装这种绑定,你不能直接在scope里添加对父作用域属性的设置,比如,这样是不能访问到父作用域属性parentProp:在独立的作用域里,和scope: { localProp: '@parentProp' }.一定要用属性来关联父作用域,然后在scope:{}里关联属性,像这样:和scope: { localProp: '@theParentProp' }.独立作用域里的__proto__属性会引用一个Scope对象(下图中橙色部分的Object应该换成Scope),独立作用域的$parent引用父级作用域,所以虽然独立作用域没有原型继承父级,但是它仍然可以说是子作用域.针对下图,我们有和scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' },然后,假如在指令的link function里执行scope.someIsolateProp = "I'm isolated",效果图如下

最后要注意的是:使用attrs.$observe('attr_name', function(value) { ... })在link function里可以获取到属性里绑定的值,例如,如果我们在link function里有attrs.$observe('interpolated', function(value) { ... }),value的值将会设置成11.(在link function里获取scope.interpolatedProp的值未定义,不过scope.twowayBindingProp的值是定义的,因为它是用=标识)相了解更多的关于独立作用域的信息,可以点击下面的链接http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/

  • 4.transclude: true的时候,指令将会创建一个名为transcluded的作用域,并且原型继承于父作用域,所以如果transclude内容(例如使用ng-transclude的内容来填充)请求双向绑定父级的基本数据类型的话,应该使用$parent或者绑定父级对象属性的方式来避免子作用域覆盖父级作用域属性的问题.transclude与独立的作用域(如果有的话)是兄弟关系,它们的$parent指向的都是同一个父级作用域,当transclude与独立的作用域同时存在的话,则独立作用域的$$nextSibling将会指向这个transclude作用域想了解更多的关于transclude作用域的情况,看AngularJS two way binding not working in directive with transcluded scope.为了匹配下图的效果,我们假定给上面的指令代码添加一个transclude: true项,最后的关系图如下

这个fiddle有一个showScope()函数来查看独立作用域以及关联的transclude作用域,可以在fiddle里查看它的注释来了解函数功能

总结

这里总共有四种类型的作用域:

  • 1.普通的作用域继承--ng-include,ng-switch,ng-controlelr,以及scope:true的指令

  • 2.普通的作用域继承--ng-repeat,类似这种复制任务的指令,每项都会创建一个新的作用域,而且都会有一个包含loop变量名的属性.

  • 3.独立的作用域--以scope:{...}属性的指令,这些不会原型继承,但是可以利用属性attributes绑定父作用域的模型,然后通过在scope:{...}里设置=,@,&的机制来访问父作用域属性.

  • 4.transcluded作用域--以transclude: true属性的指令,这些也是普通的原型继承,但是它是任何独立作用域的兄弟作用域

这些作用域不管是否是原型继承,ng都提供了$parent,$$childHead,$$childTail来跟综父子关系.

上面的这些关系图都是通过 GraphViz创建的,它是以"*.dot"为文件后缀,.dot文件的源代码在github上面.Tim Caswell的"Learning JavaScript with Object Graphs"给了我使用graphviz来画图的启发.

上面文章的原始出处是在StackOverflow上.

本文由 Easy 第一时间收藏到GET,由 luofei614 创建本知识库永久备份,原文来自 → www.ifeenan.com

「GetParty」

关注微信号,推送好文章

微信中长按图片即可关注

更多精选文章

评论
微博一键登入