深入理解Angular.JS中的Scope继承


Posted in Javascript onJune 04, 2017

前言

AngularJS中scope之间的继承关系使用JavaScript的原型继承方式实现。本文结合AngularJS Scope的实现以及相关资料谈谈原型继承机制。下面来看看详细的介绍:

基本原理

在JavaScript中,每创建一个构造函数(constructor),就会同时给该函数生成一个指向原型对象的属性prototype。每个原型对象又获得一个constructor属性指向相应的构造函数,原型对象的其他属性和方法从Object继承而来。每个通过构造函数创建的实例,都包含一个指向构造函数原型对象的内部属性[[Prototype]](在浏览器中通常实现为__proto__)。构造函数、原型对象和实例三者的关系如下 (图片来源:《JavaScript高级程序设计(第3版)》):

深入理解Angular.JS中的Scope继承

person1和person2为构造函数Person创建的两个实例,可以通过[[Prototype]]属性访问原型对象Person Prototype,获得原型中定义的所有方法和属性。Person构造函数的prototype属性同样指向Person Prototype原型对象。以上这些概念是理解原型继承的基础,下面我们来看原型链的概念。如果把一个类型的实例赋值给一个原型对象会发生什么?根据上图中的关系,此时的原型对象包含指向另一个原型的属性,而另一个原型中也包含着指向另一个构造函数的属性。

效果如下图:

深入理解Angular.JS中的Scope继承

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。

这段代码的输出如下:

深入理解Angular.JS中的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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
5款Javascript颜色选择器
Oct 25 Javascript
JavaScript和HTML DOM的区别与联系及Javascript和DOM的关系
Nov 15 Javascript
js生成随机数的过程解析
Nov 24 Javascript
JS组件Bootstrap ContextMenu右键菜单使用方法
Apr 17 Javascript
javascirpt实现2个iframe之间传值的方法
Jun 30 Javascript
Bootstrap基本样式学习笔记之表格(2)
Dec 07 Javascript
基于模板引擎Jade的应用(详解)
Dec 12 Javascript
Vue 去除路径中的#号
Apr 19 Javascript
Vue+Jwt+SpringBoot+Ldap完成登录认证的示例代码
May 21 Javascript
vue中created和mounted的区别浅析
Aug 13 Javascript
JS实现可以用键盘方向键控制的动画
Dec 11 Javascript
分享几个JavaScript运算符的使用技巧
Apr 24 Javascript
yarn的使用与升级Node.js的方法详解
Jun 04 #Javascript
npm国内镜像 安装失败的几种解决方案
Jun 04 #Javascript
Angular 4依赖注入学习教程之InjectToken的使用(八)
Jun 04 #Javascript
Angular 4依赖注入学习教程之ValueProvider的使用(七)
Jun 04 #Javascript
Angular 4依赖注入学习教程之Injectable装饰器(六)
Jun 04 #Javascript
Angular 4依赖注入学习教程之FactoryProvider配置依赖对象(五)
Jun 04 #Javascript
JavaScript基础之this详解
Jun 04 #Javascript
You might like
web方式ftp
2006/10/09 PHP
人尽可用的Windows技巧小贴士之下篇
2007/03/22 PHP
PHP随机字符串生成代码(包括大小写字母)
2013/06/24 PHP
nodejs 后缀名判断限制代码
2011/03/31 NodeJs
js/jquery去掉空格,回车,换行示例代码
2013/11/05 Javascript
Javascript 正则表达式实现为数字添加千位分隔符
2015/03/10 Javascript
JavaScript的History API使搜索引擎抓取AJAX内容
2015/12/07 Javascript
js改变style样式和css样式的简单实例
2016/06/28 Javascript
javascript 动态样式添加的简单实现
2016/10/11 Javascript
jquery事件与绑定事件
2017/03/16 Javascript
使用vue框架 Ajax获取数据列表并用BootStrap显示出来
2017/04/24 Javascript
js实现延迟加载的几种方法
2017/04/24 Javascript
vue系列之动态路由详解【原创】
2017/09/10 Javascript
Vue 换肤的示例实践
2018/01/23 Javascript
Vue中android4.4不兼容问题的解决方法
2018/09/04 Javascript
JS拖拽排序插件Sortable.js用法实例分析
2019/02/20 Javascript
详解Vue 如何监听Array的变化
2019/06/06 Javascript
解决 window.onload 被覆盖的问题方法
2020/01/14 Javascript
[03:59]第二届DOTA2亚洲邀请赛选手传记-VGJ.rOtk
2017/04/03 DOTA
PyQt5每天必学之带有标签的复选框
2018/04/19 Python
python语音识别实践之百度语音API
2018/08/30 Python
IntelliJ IDEA安装运行python插件方法
2018/12/10 Python
python 移动图片到另外一个文件夹的实例
2019/01/10 Python
使用Python将Mysql的查询数据导出到文件的方法
2019/02/25 Python
Python 日期区间处理 (本周本月上周上月...)
2019/08/08 Python
Python计算矩阵的和积的实例详解
2020/09/10 Python
如何创建一个Flask项目并进行简单配置
2020/11/18 Python
socket.io 和canvas 实现的共享画板功能
2019/05/22 HTML / CSS
Html5大文件断点续传实现方法
2015/12/05 HTML / CSS
GetYourGuide台湾:预订旅游活动、景点和旅游项目
2019/06/10 全球购物
小学关爱留守儿童活动方案
2014/08/25 职场文书
学生会主席任命书
2015/09/21 职场文书
学校体育节班级口号
2015/12/25 职场文书
Django debug为True时,css加载失败的解决方案
2021/04/24 Python
MongoDB连接数据库并创建数据等使用方法
2021/11/27 MongoDB
海贼王十大逆天果实 魂魂果实上榜,岩浆果实攻击力最强
2022/03/18 日漫