5.2 AngularJS作用域继承

5.2.1 JavaScript对象继承机制

学习AngularJS作用域继承之前,我们需要先了解一下JavaScript对象的继承机制。JavaScript语言遵循ECMAScript规范,在ECMAScript 6规范之前,JavaScript语言并没有类的概念,也不具备面向对象多态的特性,所以严格地讲JavaScript并不是一门面向对象语言,而是一门基于对象的语言。

JavaScript构造对象通常有两种方式。第一种方式是通过字面量创建,形式如下:

        var obj = {
            name:'jane',
            age:32
        };

另一种方式是通过对象的构造方法来创建,需要用到function关键字。JavaScript语言中的方法可以作为构造方法创建对象,比较容易让人产生疑惑。例如,我们使用下面这段代码定义一个构造方法:

        function Person(name, age){
            this.name = name;
            this.age = age;
            this.eat = function() {

            console.log('eat..');
            }
        }

但是我们把它作为普通方法调用就不会有问题。当使用构造方法创建对象时需要用到JavaScript的new关键字,使用方法如下:

        var person = new Person('Jane',32);

在实际项目中,当我们明确地使用function关键字定义一个构造方法时,构造方法名称通常采用帕斯卡命名法,即每个单词首字母大写(例如:LoginController);而使用function定义一个普通方法时,方法名称可采用驼峰命名法,驼峰命名法跟帕斯卡命名法相似,只是首字母为小写(例如:checkUser),看上去像驼峰,因此而得名。

JavaScript语言为我们提供了几个内置的构造方法,例如Object、Array 、String、Boolean等。我们可以直接使用这些构造方法创建对象。

了解了JavaScript对象的创建方法后,我们再来看看JavaScript的对象继承机制。JavaScript语言有以下3种方式实现对象继承。

1.构造方法原型链继承

每个JavaScript构造方法都有一个名称为prototype的属性,可以指向另一个对象。当我们访问对象属性时(例如obj.name), JavaScript引擎会从对象的所有属性中查找该属性,如果找到就返回属性值,如果没有找到就继续从prototype属性指向的对象属性中查找,如果仍然没有找到,则会沿着prototype链一直查找下去,直到prototype链结束或找到对象为止。接下来我们看一个原型链继承的案例,代码如下:

代码清单:ch05\ch05_02.html

        <!doctype html>
        <html>
        <head>
            <meta charset="UTF-8">
            <title>ch05_02</title>
        </head>
        <body>
            <script type="text/javascript">
                function Animal(){
                    this.eat = function(){
                        console.log('eat...');
                    }
                }
                function Cat(age){
                    this.age = age;
                }
                Cat.prototype = new Animal();
                var cat = new Cat(10);
                console.log("cat.age=" + cat.age);
                cat.eat();
            </script>
        </body>
        </html>

如上面的代码所示,首先定义两个构造方法Animal和Cat,把Cat的prototype属性指向一个Animal对象:

        Cat.prototype = new Animal();

接下来通过new关键字创建一个Cat对象:

        var cat = new Cat(10);

在浏览器中运行该案例,控制台输出:

        cat.age=10
        eat...

Cat构造方法中并没有定义eat()方法,而我们通过Cat实例调用eat()方法输出了内容,说明Cat对象通过prototype属性继承了Animal对象的eat()方法。

2.使用apply、call方法实现继承

由于JavaScript构造方法的apply()、call()方法可以改变对象构造中“this”的上下文环境,使特定的对象实例具有对象构造中所定义的属性、方法,因此我们可以使用apply()、call()方法实现JavaScript对象的继承,例如下面的案例:

代码清单:ch05\ch05_03.html

        <!doctype html>
        <html>
        <head>
            <meta charset="UTF-8">
            <title>ch05_03</title>
        </head>
        <body>
            <script type="text/javascript">
                function Person(name, age){
                    this.name = name;
                    this.age = age;
                }
                function Student(name, age, love){
                    //Person.apply(this, [name, age]);
                    Person.call(this, name, age);
                    this.love = love;
                }
                var student = new Student('jane',23, 'pingpong');
                console.log("student.name=" + student.name);
                console.log("student.age=" + student.age);
                console.log("student.love=" + student.love);
            </script>
        </body>
        </html>

如上面的代码所示,在本例中我们首先定义了Person构造方法,接着在Student构造方法中调用Person.call()方法实现了继承,然后通过new关键字创建一个Student对象,在控制台中输出Student对象的属性值。在浏览器中预览ch05_03.html页面,打开开发人员工具,控制台输出内容如下:

        student.name=jane
        student.age=23
        student.love=pingpong

说明Student对象继承了Person对象的name和age属性。apply()方法和call()方法的不同之处在于apply()方法只接收两个参数,第二个参数是一个数组,而call()方法可以接收多个参数。

3.对象实例间继承

在JavaScript语言中,对象可以继承另外一个对象的属性,例如下面的案例:

代码清单:ch05\ch05_04.html

        <!doctype html>
        <html>
        <head>
            <meta charset="UTF-8">
            <title>ch05_04</title>
        </head>
        <body>
            <script type="text/javascript">
                function Person(name, age){
                    this.name = name;
                    this.age = age;
                }
                var person = new Person('jane',28);
                var student = Object.create(person);
                student.love = "pingpong";
                console.log(Object.getPrototypeOf(student));
                console.log("student.name→"+student.name);
                console.log("student.age→"+student.age);
                console.log("student.love→"+student.love);
            </script>
        </body>
        </html>

如上面的代码所示,在本例中我们用到了Object.create()方法,它的作用是以一个对象为原型创建另外一个对象,创建的对象和原对象具有相同的属性,我们可以通过Object. getPrototypeOf()方法获取新对象的原型。

在浏览器中运行ch05_04.html页面,打开开发人员工具,控制台输出内容如下:

        Person
        student.name→jane
        student.age→28
        student.love→pingpong

结合源代码,从输出的日志信息可以看出,student对象继承了Person对象的name和age属性,调用Object.getPrototypeOf()方法获取student对象的原型依然为Person。

本节中笔者对JavaScript对象的继承方式进行了比较全面的学习,下一小节我们一起学习AngularJS如何使用原型方式实现作用域对象的继承。

5.2.2 AngularJS作用域对象原型继承

上一小节介绍了JavaScript语言中对象继承的3种方式,其中AngularJS作用域对象继承采用第一种方式,即构造方法原型链继承。AngularJS作用域构造方法中提供了一个$new()成员方法,用于创建子作用域。AngularJS框架创建子作用域的过程大致如下:

        var parent = $rootScope;
        var child = parent.$new();

我们不妨了解一下$new()方法的定义,内容如下,有兴趣的读者可以参考AngularJS源码。

        Scope.prototype = {
              constructor: Scope,
              $new: function(isolate, parent){
                var child;
                parent = parent || this;
                if(isolate){
                  child = new Scope();
                  child.$root = this.$root;
                } else {
                  // Only create a child scope class if somebody asks for one,
                  // but cache it to allow the VM to optimize lookups.
                  if(! this.$$ChildScope){
                    this.$$ChildScope = createChildScopeClass(this);
                  }
                  child = new this.$$ChildScope();
                }
                child.$parent = parent;
                child.$$prevSibling = parent.$$childTail;
                if(parent.$$childHead){
                  parent.$$childTail.$$nextSibling = child;
                  parent.$$childTail = child;
                } else {
                  parent.$$childHead = parent.$$childTail = child;
                }

                // When the new scope is not isolated or we inherit from `this`, and
                // the parent scope is destroyed, the property `$$destroyed` is
                // inherited prototypically. In all other cases, this property
                // needs to be set when the parent scope is destroyed.
                // The listener needs to be added after the parent is set
                if(isolate || parent ! = this)child.$on('$destroy',
                destroyChildScope);

                return child;
              },

              ......

        }

上面的代码为AngularJS1.5.5版本中$new()方法的定义,如上面黑体代码所示。其中,this.$$ChildScope为子作用域构造方法,由createChildScopeClass()方法调用返回。下面是createChildScopeClass()方法的定义:

        function createChildScopeClass(parent){
            function ChildScope() {
              this.$$watchers = this.$$nextSibling =
                    this.$$childHead = this.$$childTail = null;
              this.$$listeners = {};
              this.$$listenerCount = {};
              this.$$watchersCount = 0;
              this.$id = nextUid();
              this.$$ChildScope = null;
            }
            ChildScope.prototype = parent;
            return ChildScope;
        }

createChildScopeClass()方法中定义了ChildScope构造方法,ChildScope即为子作用域的构造方法,接着指定ChildScope的prototype属性为parent, parent即为父作用域对象,这样子作用域就继承了父作用域的所有属性。

上面我们了解了AngularJS作用域继承机制,所有作用域对象都是$rootScope作用域的子作用域。还有一种情况需要我们考虑,即在AngularJS控制器中可以嵌套另外一个控制器,例如:

        <div ng-app>
            <div ng-controller="OuterController">

            <div ng-controller="InnerController">

            </div>
            </div>
        </div>

在上面的代码片段中,ng-controller指令范围内嵌套了另外一个ng-controller指令,这种情况下AngularJS是如何处理作用域对象继承的呢?过程如下:

AngularJS框架遍历DOM元素,查找到ng-app指令时启动应用,创建$rootScope作用域。

然后AngularJS框架查找到第一个ng-controller指令,指向名称为OuterController的控制器,并调用$rootScope.$new()方法,以原型继承的方式创建$rootScope作用域的子作用域对象(记为$scope1)。当OuterController构造方法接收一个名称为$scope的参数时,AngularJS实例化控制器对象时会把$scope1对象注入控制器对象中。

接下来AngularJS继续遍历DOM元素,遇到第二个嵌套的ng-controller指令时调用$scope1. new()方法,以$scope1为原型创建子作用域(记为$scope2, $scope2作用域对象能够访问$scope1作用域对象的所有属性)。

除了ng-app、ng-controller指令会创建作用域对象外,AngularJS指令也可能会产生子作用域,在后面的章节中我们会接触到。本节内容就介绍这么多,下节我们开始学习AngularJS作用域监视机制。