angularJS 系列(一)

angularJS 中 $scope 的原型 和 原型继承之间的细微差别是什么?

参考:http://stackoverflow.com/questions/14049480/what-are-the-nuances-of-scope-prototypal-prototypical-inheritance-in-angularjs

The API Reference Scope page says:

A scope can inherit from a parent scope.

The Developer Guide Scope page says:

A scope (prototypically) inherits properties from its parent scope.

So, does a child scope always prototypically inherit from its parent scope? Are there exceptions? When it does inherit, is it always normal JavaScript prototypal inheritance?

Answer:

Quick answer
A child scope normally prototypically inherits from its parent scope, but not always. One exception to this rule is a directive with scope: { ... } -- this creates an "isolate" scope that does not prototypically inherit. This construct is often used when creating a "reusable component" directive.

As for the nuances, scope inheritance is normally straightfoward... until you need 2-way data binding(i.e., form elements, ng-model) in the child scope. Ng-repeat, ng-switch, and ng-include can trip you up if you try to bind to a primitive (e.g., number, string, boolean) in the parent scope from inside the child scope. It doesn‘t work the way most people expect it should work. The child scope gets its own property that hides/shadows the parent property of the same name. Your workarounds are

  1. define objects in the parent for your model, then reference a property of that object in the child: parentObj.someProp
  2. use $parent.parentScopeProperty (not always possible, but easier than 1. where possible)
  3. define a function on the parent scope, and call it from the child (not always possible)


L-o-n-g answer:

JavaScript Prototypal Inheritance

Also placed on the AngularJS wiki: https://github.com/angular/angular.js/wiki/Understanding-Scopes

It is important to first have a solid understanding of prototypal inheritance, especially if you are coming from a server-side background and you are more failiar with class-ical inheritance. So let‘s review that first.

Suppose parentScope has properties aString, aNumber, anArray, anObject, and aFunction. If childScope prototypically inherits from parentScope, we have:

(Note that to save space, I show the anArray object as a single blue object with its three values, rather than an single blue object with three separate gray literals.)

If we try to access a property defined on the parentScope from the child scope, JavaScript will first look in the child scope, not find the property, then look in the inherited scope, and find the property. (If it didn‘t find the property in the parentScope, it would continue up the prototype chain... all the way up to the root scope). So, these are all true:

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

Suppose we then do this:

childScope.aString = ‘child string‘

The prototype chain is not consulted, and a new aString property is added to the childScope. This new property hides/shadows the parentScope property with the same name. This will become very important when we discuss ng-repeat and ng-include below.

Suppose we then do this:

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

The prototype chain is consulted because the objects (anArray and anObject) are not found in the childScope. The objects are found in the parentScope, and the property values are updated on the original objects. No new properties are added to the childScope; no new objects are created. (Note that in JavaScript arrays and functions are also objects.)

Suppose we then do this:

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

The prototype chain is not consulted, and child scope gets two new object properties that hide/shadow the parentScope object properties with the same names.

Takeaways:

  • If we read childScope.propertyX, and childScope has propertyX, then the prototype chain is not consulted.
  • If we set childScope.propertyX, the prototype chain is not consulted.

One last scenario:

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

We deleted the childScope property first, then when we try to access the property again, the prototype chain is consulted.


Angular Scope Inheritance

The contenders:

  • The following create new scopes, and inherit prototypically: ng-repeat, ng-include, ng-switch, ng-controller, directive with scope: true, directive with transclude: true.
  • The following creates a new scope which does not inherit prototypically: directive with scope: { ... }. This creates an "isolate" scope instead.

Note, by default, directives do not create new scope -- i.e., the default is scope: false.

ng-include

Suppose we have in our controller:

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

And in our 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>

Each ng-include generates a new child scope, which prototypically inherits from the parent scope.

Typing (say, "77") into the first input textbox causes the child scope to get a new myPrimitive scope property that hides/shadows the parent scope property of the same name. This is probably not what you want/expect.

Typing (say, "99") into the second input textbox does not result in a new child property. Because tpl2.html binds the model to an object property, prototypal inheritance kicks in when the ngModel looks for object myObject -- it finds it in the parent scope.

We can rewrite the first template to use $parent, if we don‘t want to change our model from a primitive to an object:

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

Typing (say, "22") into this input textbox does not result in a new child property. The model is now bound to a property of the parent scope (because $parent is a child scope property that references the parent scope).

For all scopes (prototypal or not), Angular always tracks a parent-child relationship (i.e., a hierarchy), via scope properties $parent, $$childHead and $$childTail. I normally don‘t show these scope properties in the diagrams.

For scenarios where form elements are not involved, another solution is to define a function on the parent scope to modify the primitive. Then ensure the child always calls this function, which will be available to the child scope due to prototypal inheritance. E.g.,

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

Here is a sample fiddle that uses this "parent function" approach. (The fiddle was written as part of this answer: http://stackoverflow.com/a/14104318/215945.)

See also http://stackoverflow.com/a/13782671/215945 andhttps://github.com/angular/angular.js/issues/1267.

ng-switch

ng-switch scope inheritance works just like ng-include. So if you need 2-way data binding to a primitive in the parent scope, use $parent, or change the model to be an object and then bind to a property of that object. This will avoid child scope hiding/shadowing of parent scope properties.

See also AngularJS, bind scope of a switch-case?

ng-repeat

Ng-repeat works a little differently. Suppose we have in our controller:

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

And in our 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>

For each item/iteration, ng-repeat creates a new scope, which prototypically inherits from the parent scope, but it also assigns the item‘s value to a new property on the new child scope. (The name of the new property is the loop variable‘s name.) Here‘s what the Angular source code for ng-repeat actually is:

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

If item is a primitive (as in myArrayOfPrimitives), essentially a copy of the value is assigned to the new child scope property. Changing the child scope property‘s value (i.e., using ng-model, hence child scope num) does not change the array the parent scope references. So in the first ng-repeat above, each child scope gets a num property that is independent of the myArrayOfPrimitives array:

This ng-repeat will not work (like you want/expect it to). Typing into the textboxes changes the values in the gray boxes, which are only visible in the child scopes. What we want is for the inputs to affect the myArrayOfPrimitives array, not a child scope primitive property. To accomplish this, we need to change the model to be an array of objects.

So, if item is an object, a reference to the original object (not a copy) is assigned to the new child scope property. Changing the child scope property‘s value (i.e., using ng-model, hence obj.num)does change the object the parent scope references. So in the second ng-repeat above, we have:

(I colored one line gray just so that it is clear where it is going.)

This works as expected. Typing into the textboxes changes the values in the gray boxes, which are visible to both the child and parent scopes.

See also Difficulty with ng-model, ng-repeat, and inputs andhttp://stackoverflow.com/a/13782671/215945

ng-controller

Nesting controllers using ng-controller results in normal prototypal inheritance, just like ng-include and ng-switch, so the same techniques apply. However, "it is considered bad form for two controllers to share information via $scope inheritance" -- http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ A service should be used to share data between controllers instead.

(If you really want to share data via controllers scope inheritance, there is nothing you need to do. The child scope will have access to all of the parent scope properties. See also Controller load order differs when loading or navigating)

directives

  1. default (scope: false) - the directive does not create a new scope, so there is no inheritance here. This is easy, but also dangerous because, e.g., a directive might think it is creating a new property on the scope, when in fact it is clobbering an existing property. This is not a good choice for writing directives that are intended as reusable components.
  2. scope: true - the directive creates a new child scope that prototypically inherits from the parent scope. If more than one directive (on the same DOM element) requests a new scope, only one new child scope is created. Since we have "normal" prototypal inheritance, this is like ng-include and ng-switch, so be wary of 2-way data binding to parent scope primitives, and child scope hiding/shadowing of parent scope properties.
  3. scope: { ... } - the directive creates a new isolate/isolated scope. It does not prototypically inherit. This is usually your best choice when creating reusable components, since the directive cannot accidentally read or modify the parent scope. However, such directives often need access to a few parent scope properties. The object hash is used to set up two-way binding (using ‘=‘) or one-way binding (using ‘@‘) between the parent scope and the isolate scope. There is also ‘&‘ to bind to parent scope expressions. So, these all create local scope properties that are derived from the parent scope. Note that attributes are used to help set up the binding -- you can‘t just reference parent scope property names in the object hash, you have to use an attribute. E.g., this won‘t work if you want to bind to parent property parentProp in the isolated scope: <div my-directive> and scope: { localProp: ‘@parentProp‘ }. An attribute must be used to specify each parent property that the directive wants to bind to: <div my-directive the-Parent-Prop=parentProp> and scope: { localProp: ‘@theParentProp‘ }
    Isolate scope‘s __proto__ references Object. Isolate scope‘s $parent references the parent scope, so although it is isolated and doesn‘t inherit prototypically from the parent scope, it is still a child scope. 
    For the picture below we have 
    <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2"> and 
    scope: { interpolatedProp: ‘@interpolated‘, twowayBindingProp: ‘=twowayBinding‘ } 
    Also, assume the directive does this in its linking function: scope.someIsolateProp = "I‘m isolated" 
     
    For more information on isolate scopes see http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true - the directive creates a new "transcluded" child scope, which prototypically inherits from the parent scope. The transcluded and the isolated scope (if any) are siblings -- the $parent property of each scope references the same parent scope. When a transcluded and an isolate scope both exist, isolate scope property $$nextSibling will reference the transcluded scope. I‘m not aware of any nuances with the transcluded scope. 
    For the picture below, assume the same directive as above with this addition: transclude: true 

This fiddle has a showScope() function that can be used to examine an isolate and transcluded scope. See the instructions in the comments in the fiddle.


Summary

There are four types of scopes:

  1. normal prototypal scope inheritance -- ng-include, ng-switch, ng-controller, directive with scope: true
  2. normal prototypal scope inheritance with a copy/assignment -- ng-repeat. Each iteration of ng-repeat creates a new child scope, and that new child scope always gets a new property.
  3. isolate scope -- directive with scope: {...}. This one is not prototypal, but ‘=‘, ‘@‘, and ‘&‘ provide a mechanism to access parent scope properties, via attributes.
  4. transcluded scope -- directive with transclude: true. This one is also normal prototypal scope inheritance, but it is also a sibling of any isolate scope.

For all scopes (prototypal or not), Angular always tracks a parent-child relationship (i.e., a hierarchy), via properties $parent and $$childHead and $$childTail.

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.

时间: 2024-10-08 10:13:45

angularJS 系列(一)的相关文章

深究angularJS系列 - 第三弹

深究angularJS系列 - 初识 深究angularJS系列 - 第二弹 深究angularJS系列 - 第三弹,我们一起深入探究angular的服务和自定义指令O(∩_∩)O~~ Angular服务 $http: $http是angular中的一个核心服务; $http利用浏览器的xmlhttprequest或JSONP与远程HTTP服务器进行交互; $http的支持多种method的请求,get.post.put.delete.jsonp等. 下面通过JSONP方法进行$http服务的使

深究angularJS系列 - 第二弹

深究angularJS系列 - 第二弹,在初步了解了Angular的基础上,进一步的针对Angular的控制器和作用域问题深入探究O(∩_∩)O~~ Angular控制器 控制器(Controller)的理解 控制器是对view的抽象,用来接收view的事件,响应view的请求: 控制器包含view的静态属性和动态的方法: 控制器与view是一对一的关系. 控制器(Controller)的结构 1 .controller("控制器的名字",function($scoppe){ 2 ..

AngularJS系列之总结

AngularJS深入的系列就是这九篇博客了,把我以前在项目中应用到的和自己学习的都总结在了里面.为了更方便的看,把我写的AngularJS系列的博客都列到下面.之后就开始学习ionic:html5移动开发框架,它是基于AngularJS的移动开发框架.希望大家多多关注我的博客,多多推荐. AngularJS之站在jQuery的肩膀上: http://www.cnblogs.com/xuema/p/4335180.html AngularJS应用开发思维之1:声明式界面  http://www.

[转载]去掉的URL里的#号——angularjs系列

AngularJS体验式编程系列文章,将介绍如何用angularjs构建一个强大的web前端系统.angularjs是由Google团队开发的一款非常优秀web前端框架.在当前如此多的web框架下,angularjs能脱颖而出,从架构设计上就高人一等,双向数据绑定,依赖注入,指令,MVC,模板.Angular.js创新地把后台技术融入前端开发,扫去jQuery一度的光芒.用angularjs就像写后台代码,更规范,更结构化,更可控. 关于作者 张丹(Conan), 程序员Java,R,PHP,J

AngularJS系列-翻译官网

公司之前一直用的Web前台框架是Knockout,我们通常直接叫ko,有看过汤姆大叔的KO系列,也有在用,发现有时候用得不太顺手.本人是会WPF的,所以MVVM也是比较熟悉的,学ko也是很快就把汤姆大叔的文章系列看完了,但是ko有时候会有意想不到的问题,同事也有这样的反应,所以就度娘了一下.就找到了AngularJS和KO的对比文章<從Knockout到AngularJS>,看了之后不明觉厉.其实早在AngularJS刚刚问世的那年,因为我订阅了博友--梦想天空(山边小溪)的博客(这里得特别感

深究angularJS系列 - 初识

AngularJS是什么?概念?特征?优点?缺点?那都不是事,话不多说,直接搞起O(∩_∩)O~~ 安装 1.官网http://angularjs.org/下载安装 2.开源库http://www.bootcdn.cn/下载安装 3.bower(一种包管理器)下载安装 bower install angular MVC MVC即"模型 - 视图 - 控制器"的简称,一种设计模式,MVC的从逻辑上将代码清晰地分割为三层,这样可以对每个部分进行独立开发.测试和维护. 模型/Model -

[angularjs] angularjs系列笔记(一)简介

Angularjs通过新的属性和表达式扩展了html Andularjs 可以构建一个单一页面的应用程序(SPAS SinglePageApplications) Angularjs通过指令扩展了html,通过表达式绑定数据到html ng-app指令定义Angularjs的应用程序 ng-model指令绑定元素值到应用程序 ng-bind指令把应用程序数据绑定到html视图 <div ng-app=""> <input type="text" n

[AngularJS] AngularJS系列(6) 中级篇之ngResource

目录 $http ngResource $http几乎是所有ng开发中,都会用到的服务.本节将重点说下$http 与 ngResource $http 使用:$http(config); 参数: method:字符串,请求方法. url:字符串,请求地址. params:字符串或者对象,将使用paramserializer序列化并且作为GET请求的参数. data:字符串或者对象,作为请求信息数据的数据. headers:对象,字符串或者函数返回表示发送到服务器的HTTP请求头.如果函数的返回值

[angularjs] angularjs系列笔记(八)事件

AngularJs有自己的HTML事件 ng-click指令 ng-click指令定义了AngularJs点击事件 当点击按钮的时候,赋值count变量并且给count变量加1,显示出count变量 <body> <div ng-app="Home"> <div ng-controller="ngClickTest"> 测试ng-click指令 <button ng-click="count=count+1&quo