深入理解Angular.JS中的Scope继承
前言
angularjs中scope之间的继承关系使用javascript的原型继承方式实现。本文结合angularjs scope的实现以及相关资料谈谈原型继承机制。下面来看看详细的介绍:
基本原理
在javascript中,每创建一个构造函数(constructor),就会同时给该函数生成一个指向原型对象的属性prototype。每个原型对象又获得一个constructor属性指向相应的构造函数,原型对象的其他属性和方法从object继承而来。每个通过构造函数创建的实例,都包含一个指向构造函数原型对象的内部属性[[prototype]](在浏览器中通常实现为__proto__)。构造函数、原型对象和实例三者的关系如下 (图片来源:《javascript高级程序设计(第3版)》):
person1和person2为构造函数person创建的两个实例,可以通过[[prototype]]属性访问原型对象person prototype,获得原型中定义的所有方法和属性。person构造函数的prototype属性同样指向person prototype原型对象。以上这些概念是理解原型继承的基础,下面我们来看原型链的概念。如果把一个类型的实例赋值给一个原型对象会发生什么?根据上图中的关系,此时的原型对象包含指向另一个原型的属性,而另一个原型中也包含着指向另一个构造函数的属性。
效果如下图:
supertype为一个父类型,在原型中定义了属性property和方法getsupervalue;subtype是一个子类型,定义了属性subproperty和方法getsubvalue。instance为subtype的一个实例。这里通过下面的关键代码,将subtype的原型对象变为supertype对象的实例:
subtype.prototype = new supertype(); subtype.prototype.getsubvalue = function(){ return this.subproperty; };
我们看到,subtype的原型对象中有来自supertype实例对象的property属性,以及自己在原型上定义的getsubvalue方法,通过[[prototype]]属性,又可以进一步访问supertype原型对象中的成员。假如supertype的原型也被赋值成某个类型的实例,依次类推,那么可以通过[[prototype]]属性一直向上回溯,形成一条直通object原型对象的原型链。上面的例子只展示了链条的前两环。
通过原型链的实现,subtype的实例继承了supertype实例的的所有实例成员和原型成员。例如,若要访问instance.getsupervalue
,首先在instance实例内部搜索,没有该方法;然后通过原型链向上回溯,找到subtype原型对象,也没有该方法;再通过[[prototype]]属性继续回溯,来到supertype的原型对象,找到该方法。
以上描述的这种继承方式就是原型继承。在es5以后,可以使用object提供的create方法规范化上述过程,详细请参考这里。angularjs的scope继承关系的实现类似上述过程。
scope继承实现
在angular中,想要定义一个scope的child scope可以通过scope.$new方法实现,而$new方法本身的实现就体现了上述原型继承的思想。首先,$new方法接受两个参数:isolated和parent。第一个参数表示创建的child scope是否是一个隔离的(isolated)。隔离的scope不继承parent scope的原型,只是在层次结构(hierachy)上属于其child scope,这种结构是digest过程的基础。isolated scope的一个好处是避免parent scope的成员被更改,在directive的实现里很有用。第二个参数指定创建的child scope的parent scope,如果不指定,默认为当前调用$new方法的scope。angular中$new的实现类似:
$new : function(isolate, parent) { var child; parent = parent || this; if (isolate) { child = new scope(); child.$root = this.$root; } else { if (!this.$$childscope) { this.$$childscope = createchildscopeclass(this); } child = new this.$$childscope(); } child.$parent = parent; //... return child; },//...
可以看出,如果是isolate为true,则使用scope类型构造函数创建一个child对象。如果isolate为false或者未指定,则创建一个child scope原型继承于当前scope,这个过程由createchildscopeclass提供的构造函数实现:
function createchildscopeclass(parent) { function childscope() { this.$$watchers = null; this.$$listeners = {};//... } childscope.prototype = parent; return childscope; }
这里定义了childscope类型,包括其需要的属性。然后将该类型的prototye属性设置为传入的scope实例(即前面的this),这就是前面阐述的原型继承。之后通过childscope创建的scope对象都是原型继承于parent的,即可以访问parent scope的所有成员。结合$new的代码,如果child非隔离,则child可以访问当前scope对象中的所有成员(例如$digest,$apply等方法以及自定义成员)。这就解释了在我们自己创建的controller对应的scope里,可以访问$rootscope提供的成员,因为我们的scope最终原型继承自root scope,因而可以通过原型链向上回溯到root scope的实例。
在前面一篇文章中,谈到了angular中digest过程。当调用scope.$apply
方法时,实际上是从root scope开始,按照scope的层次结构,调用每个scope的$digest方法。这就是为什么在scope的构造函数中会设置$root属性:
function scope() { this.$parent = null;//... this.$root = this; this.$$destroyed = false; this.$$listeners = {}; //... }
对于一般child scope,$root会通过原型继承得到,在root scope构造以后,后续的scope都可以访问$root对象,即是root scope对象。对于isolated scope,由于是通过scope构造函数创建(非原型继承),$root被child scope覆盖,需要将$root属性设置为parent的$root属性,如前面$new的实现。这就保证了在任何一个scope中始终能拿到root scope的实例,也就可以完成自上而下的digest过程,在$apply等方法的实现中,使用$rootscope代替$root,二者相同:
$apply: function(expr) { beginphase('$apply'); try { return this.$eval(expr); } finally { clearphase(); } finally { $rootscope.$digest(); } },//...
$rootscope是$rootscopeprovider提供的scope类型实例,是最先初始化的scope对象。在开发中,我们可以这样使用child scope:
.controller('smallcatctrl', [ '$scope', function($scope){ var child = $scope.$new(); child.text = 'cat'; var child1 = $scope.$new(true); child1.value = 0; var child2 = $scope.$new(true, child); child2.value = 1; child2.$watch('value', function(oldvalue, newvalue){ console.log('child2.value changed'); }); child1.$watch('value', function(oldvalue, newvalue){ console.log('child1.value changed'); }) child.$watch('text', function(oldvalue, newvalue){ console.log('child.text changed'); }); console.log(child2.text); }]);
在这段代码中,首先创建$scope的一个子scope----child,没有给$new指定参数,意味着child原型继承于$scope。同时定义了child的属性text。接下来创建$scope的第二个子scope----child1,传入$new的参数要求child1是isolated scope,并且在层次结构上是$scope的后代。同时定义了其value属性。最后创建scope child2,它也是一个isolated scope,不同的是它以child为层次结构上的parent scope。
这段代码的输出如下:
首先,child2只是在层次结构上继承于child,因此没有把child实例作为原型,也就没有text属性,第一行输出undefined。
由于digest过程按scope层次结构自上而下进行,类似于树的深度遍历过程。在该例中scope的顺序为$scope->child->child2->child1,因此三个watch listener函数的输出也按照上面的顺序。
本文参考资料:
1. angularjs 官方文档
2. angularjs 源代码
3. javascript 高级程序设计(第3版)
4. build your own angularjs
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。
上一篇: 使用async-validator编写Form组件的方法
下一篇: Vuex 进阶之模块化组织详解
推荐阅读
-
深入理解JavaScript和TypeScript中的class
-
深入理解php中require/include的顺序
-
深入理解JavaScript中的传值与传引用_javascript技巧
-
深入理解python中的闭包和装饰器
-
CSS3中:nth-child和:nth-of-type的区别深入理解
-
深入理解Javascript中this的作用域_javascript技巧
-
深入理解PHP中的Streams工具,深入理解phpstreams
-
Java 继承与多态的深入理解
-
深入理解JavaScript编程中的原型概念_基础知识
-
深入解析AngularJS框架中$scope的作用与生命周期_AngularJS