AngularJSAngularjs首页推荐

【转载】理解AngularJS的作用域Scope

2017-08-09  本文已影响25人  竿牍

2013年8月2日, 严清
译文:理解AngularJS的作用域Scope
原文:Understanding Scopes

概叙:

AngularJS中,子作用域一般都会通过JavaScript原型继承机制继承其父作用域的属性和方法。但有一个例外:在directive中使用scope: { ... },这种方式创建的作用域是一个独立的"Isolate"作用域,它也有父作用域,但父作用域不在其原型链上,不会对父作用域进行原型继承。这种方式定义作用域通常用于构造可复用的directive组件。作用域的原型继承是非常简单普遍的,甚至你不必关心它的运作。直到你在子作用域中向父作用域的原始类型属性使用双向数据绑定2-way data binding,比如Form表单的ng-model为父作用域中的属性,且为原始类型,输入数据后,它不会如你期望的那样运行——AngularJS不会把输入数据写到你期望的父作用域属性中去,而是直接在子作用域创建同名属性并写入数据。这个行为符合JavaScript原型继承机制的行为。AngularJS新手通常没有认识到ng-repeat、 ng-switch、ng-view和ng-include 都会创建子作用域, 所以经常出问题。 (见 示例)避免这个问题的最佳实践是在ng-model中总使用.,参见文章 always have a '.' in your ng-models
比如:

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

优于:

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

如果你一定要直接使用原始类型,要注意两点:
在子作用域中使用 $parent.parentScopeProperty,这样可以直接修改父作用域的属性。在父作用域中定义函数,子作用域通过原型继承调用函数把值传递给父作用域(这种方式极少使用)。

正文:

JavaScript Prototypal Inheritance
Angular Scope Inheritanceng-include
ng-switch
ng-view
ng-repeat
ng-controller
directives

JavaScript 原型继承机制
你必须完全理解JavaScript的原型继承机制,尤其是当你有后端开发背景和类继承经验的时候。所以我们先来回顾一下原型继承:
假设父作用域parentScope拥有以下属性和方法:aString、aNumber、anArray、anObject、aFunction。子作用域childScope如果从父作用域parentScope进行原型继承,我们将看到:

normal prototypal inheritance
(注:为节约空间,anArray使用了蓝色方块图)
如果我们在子作用域中访问一个父作用域中定义的属性,JavaScript首先在子作用域中寻找该属性,没找到再从原型链上的父作用域中寻找,如果还没找到会再往上一级原型链的父作用域寻找。在AngularJS中,作用域原型链的顶端是$rootScope,JavaScript寻找到$rootScope为止。所以,以下表达式均为true:
childScope.aString === 'parent string'childScope.anArray[1] === 20childScope.anObject.property1 === 'parent prop1'childScope.aFunction() === 'parent output'

如果我们进行如下操作:

childScope.aString = 'child string'

因为我们赋值目标是子作用域的属性,原型链将不会被查询,一个新的与父作用域中属性同名的属性aString将被添加到当前的子作用域childScope中。


shadowing

如果我们进行如下操作:
childScope.anArray[1] = '22'childScope.anObject.property1 = 'child prop1'

因为我们的赋值目标是子作用域属性anArray和anObject的子属性,也就是说JavaScript必须先要先寻找anArray和anObject
这两个对象——它们必须为对象,否则不能写入属性,而这两个对象不在当前子作用域,原型链将被查询,在父作用域中找到这两个对象, 然后对这两个对象的属性[1]和property1进行赋值操作。子作用域中不会不会创建两个新的同名属性!(注意JavaScript中数组和函数均是对象——引用类型)


follow the chain

如果我们进行如下操作:

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

同样因为我们赋值目标是子作用域的属性,原型链将不会被查询,,JavaScript会直接在子作用域创建两个同名属性,其值分别为数组和对象。


not following the chain

要点:

如果我们读取childScope.propertyX,并且childScope存在propertyX,原型链不会被查询;
如果我们写入childScope.propertyX, 原型链也不会被查询;
如果我们写入childScope.propertyX.subPropertyY, 并且childScope不存在propertyX,原型链将被查询——查找propertyX。

最后一点:

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

如果我们先删除了子作用域childScope的属性,然后再读取该属性,因为找不到该属性,原型链将被查询。


after deleting a property

AngularJS 作用域Scope的继承
**提示:
以下方式会创建新的子作用域,并且进行原型继承: ng-repeat、ng-include、ng-switch、ng-view、ng-controller, 用scope: true
和transclude: true 创建directive。以下方式会创建新的独立作用域,不会进行原型继承:用scope: { ... }创建directive。这样创建的作用域被称为"Isolate"作用域。

注意:默认情况下创建directive使用了scope: false,不会创建子作用域。进行原型继承即意味着父作用域在子作用域的原型链上,这是JavaScript的特性。AngularJS的作用域还存在如下内部定义的关系:
scope.$parent指向scope的父作用域;
scope.$$childHead指向scope的第一个子作用域;
scope.$$childTail指向scope的最后一个子作用域;
scope.$$nextSibling指向scope的下一个相邻作用域;
scope.$$prevSibling指向scope的上一个相邻作用域;

这些关系用于AngularJS内部历遍,如$broadcast和$emit事件广播,$digest处理等。
ng-include
In controller:

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

In 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
在第一个input框输入"77"将会导致子作用域中新建一个同名属性,其值为77,这不是你想要的结果。
ng-include primitive
在第二个input框输入"99"会直接修改父作用域的myObject对象,这就是JavaScript原型继承机制的作用。
ng-include object
(注:上图存在错误,红色99因为是50,11应该是99)
如果我们不想把model由原始类型改成引用类型——对象,我们也可以使用$parent直接操作父作用域:
<input ng-model="$parent.myPrimitive">

输入"22"我们得到了想要的结果。


ng-include $parent

另一种方法就是使用函数,在父作用域定义函数,子作用域通过原型继承可运行该函数:
// in the parent scope

$scope.setMyPrimitive = function(value) { $scope.myPrimitive = value;}

请参考:

sample fiddle that uses this "parent function" approach. (This was part of a Stack Overflow post.)
http://stackoverflow.com/a/13782671/215945
https://github.com/angular/angular.js/issues/1267.
ng-switch
ng-switch与ng-include一样。
参考: AngularJS, bind scope of a switch-case?
ng-view
ng-view与ng-include一样。
ng-repeat
ng-repeat
也创建子作用域,但有些不同。
In controller:

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

In 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
对每一个迭代项Item都会创建子作用域, 子作用域也从父作用域进行原型继承。 但它还是会在子作用域中新建同名属性,把Item赋值给对应的子作用域的同名属性。 下面是AngularJS中ng-repeat的部分源代码:

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

如果Item是原始类型(如myArrayOfPrimitives的11、22), 那么子作用域中有一个新属性(如num),它是Item的副本(11、22). 修改子作用域num的值将不会改变父作用域myArrayOfPrimitives,所以在上一个ng-repeat,每一个子作用域都有一个num 属性,该属性与myArrayOfPrimitives无关联:

ng-repeat primitive
显然这不会是你想要的结果。我们需要的是在子作用域中修改了值后反映到myArrayOfPrimitives数组。我们需要使用引用类型的Item,如上面第二个ng-repeat所示。
myArrayOfObjects的每一项Item都是一个对象——引用类型,ng-repeat对每一个Item创建子作用域,并在子作用域新建obj属性,obj属性就是该Item的一个引用,而不是副本。
ng-repeat object
我们修改子作用域的obj.num就是修改了myArrayOfObjects。这才是我们想要的结果。
**参考:
Difficulty with ng-model, ng-repeat, and inputs
ng-repeat and databinding
ng-controller
使用ng-controller与ng-include一样也是创建子作用域,会从父级controller创建的作用域进行原型继承。但是,利用原型继承来使父子controller共享数据是一个糟糕的办法。 "it is considered bad form for two controllers to share information via $scope inheritance",controllers之间应该使用 service进行数据共享。
(如果一定要利用原型继承来进行父子controllers之间数据共享,也可以直接使用。 请参考: Controller load order differs when loading or navigating)
directives
默认 (scope: false) - directive使用原有作用域,所以也不存在原型继承,这种方式很简单,但也很容易出问题——除非该directive与html不存在数据绑定,否则一般情况建议使用第2条方式。
scope: true

“Isolate”作用域的proto是一个标准Scope object (the picture below needs to be updated to show an orange 'Scope' object instead of an 'Object'). “Isolate”作用域的$parent同样指向父作用域。它虽然没有原型继承,但它仍然是一个子作用域。
如下directive:

 <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2"> 

scope:

 scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }

link函数中:

 scope.someIsolateProp = "I'm isolated"
isolate scope
请注意,我们在link函数中使用attrs.$observe('interpolated', function(value) { ... }来监测@属性的变化。
更多请参考: http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/

transclude: true

总结

AngularJS存在四种作用域:
普通的带原型继承的作用域 -- ng-include, ng-switch, ng-controller, directive with scope: true;普通的带原型继承的,并且有赋值行为的作用域 -- ng-repeat,ng-repeat为每一个迭代项创建一个普通的有原型继承的子作用域,但同时在子作用域中创建新属性存储迭代项;“Isolate”作用域 -- directive with scope: {...}, 该作用域没有原型继承,但可以通过'=', '@', 和 '&'与父作用域通信。“transcluded”作用域 -- directive with transclude: true,它也是普通的带原型继承的作用域,但它与“Isolate”作用域是相邻的好基友。

Diagrams were generated with GraphViz "*.dot" files, which are on github. Tim Caswell's "Learning JavaScript with Object Graphs" was the inspiration for using GraphViz for the diagrams.
The above was originally posted on StackOverflow.

上一篇 下一篇

猜你喜欢

热点阅读