深入理解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

总结

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

(0)

相关推荐

  • AngularJS的ng-repeat指令与scope继承关系实例详解

    本文实例分析了AngularJS的ng-repeat指令与scope继承关系.分享给大家供大家参考,具体如下: ng-repeat指令的使用方式可以参考如下代码: <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>ng-repeat</title> <script src="jquery-1

  • AngularJS深入探讨scope,继承结构,事件系统和生命周期

    本文实例讲述了AngularJS的scope,继承结构,事件系统和生命周期.分享给大家供大家参考,具体如下: 深入探讨 Scope 作用域 每一个 $scope 都是类 Scope 的一个实例.类 Scope 拥有可以控制 scope 生命周期的方法,提供事件传播的能力,并支持模板渲染. 作用域的层次结构 让我们再来看看这个简单的 HelloCtrl 的例子: var HelloCtrl = function($scope){ $scope.name = 'World'; } HelloCtrl

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

    前言 AngularJS中scope之间的继承关系使用JavaScript的原型继承方式实现.本文结合AngularJS Scope的实现以及相关资料谈谈原型继承机制.下面来看看详细的介绍: 基本原理 在JavaScript中,每创建一个构造函数(constructor),就会同时给该函数生成一个指向原型对象的属性prototype.每个原型对象又获得一个constructor属性指向相应的构造函数,原型对象的其他属性和方法从Object继承而来.每个通过构造函数创建的实例,都包含一个指向构造函

  • Angular.js之作用域scope'@','=','&'实例详解

    什么是scope AngularJS 中,作用域是一个指向应用模型的对象,它是表达式的执行环境.作用域有层次结构,这个层次和相应的 DOM 几乎是一样的.作用域能监控表达式和传递事件. 在 HTML 代码中,一旦一个 ng-app 指令被定义,那么一个作用域就产生了,由 ng-app 所生成的作用域比较特殊,它是一个根作用域($rootScope),它是其他所有$Scope 的最顶层. 除了用 ng-app 指令可以产生一个作用域之外,其他的指令如 ng-controller,ng-repeat

  • Angular.js中window.onload(),$(document).ready()的写法浅析

    一,问题发现: 最近公司有个微信公众号项目,为了方便直接使用anular.js+ionic进行开发,里面有使用到echarts图表,具体开发中发现echarts在初始化绑定图表的DOM节点时,一直提示该节点不合法;可是明明已经把代码写在了window.onload()中了,又改成$(function(){})结果还是不行. 二,解决方案 1使用angular.element <script type="text/javascript"> angular.element(wi

  • Angular.js中$resource高大上的数据交互详解

    本文主要给大家介绍的是关于Angular.js中$resource数据交互的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: $resource 创建一个resource对象的工厂函数,可以让你安全的和RESFUL服务端进行数据交互. 需要注入 ngResource 模块.angular-resource[.min].js 默认情况下,末尾斜杠(可以引起后端服务器不期望出现的行为)将从计算后的URL中剥离. 这个可以通过$resourceProvider配置: app.config(

  • Angular.JS中select下拉框设置value的方法

    前言 本文主要给大家介绍的是关于Angular.JS中select下拉框设置value的相关内容,非常出来供大家参考学习,下面来一起看看详细的介绍: 最近在系统中增加一个查询的筛选条件,通过下拉框选取,用的是Angular常见的ng-options 指令: <select id="selectDetectUnit" class="form-control" ng-model="detectUnits" ng-options="de

  • 浅谈angular.js中实现双向绑定的方法$watch $digest $apply

    Angular.js 中的特性,双向绑定. 多么神奇的功能,让视图的改变直接反应到数据中,数据的改变又实时的通知到视图,如何做到的? 这要归功于 scope 下面3个重要的方法: $watch $digest $apply 他们的区别是什么,我们来介绍下: $watch 这是一个监听 scope 上数据的监听器 方法说明: $scope.$watch('参数',function(newValue,oldValue){ //逻辑处理 }) 上面我们就是创建了一个监听器. '参数' 就是$scope

  • 解决Angular.js中使用Swiper插件不能滑动的问题

    我们都知道swiper是交互体验十分好的轮播插件 但是通过angular(ng-repeat)循环出来的swiper不能轮播的解决方案 通常我们都是通过以下方法来执行: html <div class="swiper-container" ng-controller="swiperController"> <div class="swiper-wrapper"> <div class="swiper-sli

  • Angular.js中数组操作的方法教程

    前言 前端技术的发展是如此之快,各种优秀技术.优秀框架的出现简直让人目不暇接,紧跟时代潮流,学习掌握新知识自然是不敢怠慢.最近在学习Angular.js,将自己学习的一些经验技巧分享给大家,下面本文将给大家介绍关于Angular.js中数组操作的相关资料,话不多说了,来一起看看详细的介绍. 1:ng-click,ng-model,ng-bind,ng-class,ng-hide,ng-app 2:placeholder, 3:{}中加入代码":true|false",使用逗号隔开,可以

  • 深入理解Node.js中的Worker线程

    概述 多年以来,Node.js都不是实现高 CPU 密集型应用的最佳选择,这主要就是因为JavaScript的单线程.作为对此问题的解决方案,Node.jsv10.5.0 通过worker_threads模块引入了实验性的 "worker 线程" 概念,并从 Node.js v12 LTS 起成为一个稳定功能.本文将解释其如何工作,以及如何使用 Worker 线程获得最佳性能. Node.js 中 CPU 密集型应用的历史 在 worker 线程之前,Node.js 中有多种方式执行

  • Angular.js中控制器之间的传值详解

    前言 每个controller都会有自己的scope,所有的scope都是属于 $rootScope的子或者子的子... 那么问题就好解决了,通过 $rootScope.$broadcast 广播的事件每个controller都能收到事件 另外,我的经验是,尽量不要用event传数据.应该建立一个service来保存数据,并且设置相应getter/setter,具体如下: 每个controller依赖service, call service.setter(...) 统一的service.set

随机推荐