首页 > 网站 > WEB开发 > 正文

第九章:Javascript类和模块

2024-04-27 14:12:47
字体:
来源:转载
供稿:网友

第九章:javascript类和模块

(过年了,祝大家新年好!)

第6章详细介绍了Javascript对象,每个javascript对象都是一个属性集合,相互之间没有任何联系。在javascript中也可以定义对象的类,让每个对象都共享某些属性,这种“共享”的特性是非常有用的。类的成员或实例都包含一些属性,用以存放它们的状态,其中有些属性定义了它们的行为(通常称为方法)。这些行为通常是由类定义的,而且为所有实例所共享。例如,假如有一个名为complex的类用来表示复数,同时还定义了一些复数运算。一个complex实例应当包含复数的实部和虚部,同样Complex还会定义复数的加法和乘法操作(行为)。

在javascript中,类的实现是基于其原型继承机制的。如果两个实例都从一个原型对象上继承了属性,我们说它们是同一个类的实例。javascript原型和继承在6.1.iii和6.2.ii节中有详细讨论,为了更好的理解本章内容,请务必首先阅读者两个章节,本章会在9.1节中对原型做进一步讨论。

如果两个对象继承自同一个原型,往往意味着(但不是绝对)它们是由同一个构造函数创建并初始化的。我们已经在4.6,6.2和8.2.iii节详细了解构造函数,9.2会有更近一步讨论。如果你对java和c++这种强类型(强、弱类型是指类型检查的严格程度,为所有变量指定数据类型称为强类型)比较熟悉,你会发现javascript中的类和java及c++的类型有很大不同。尽管在写法上类似,而且在javascript中也能“模拟”出很大经典的特性(比如传统类的封装、继承和多态)但是最好要理解javascript类和基于原型的继承机制,以及和传统的java(当然还有类似java的语言)的类和基于类的继承机制的不同之处,9.3展示了如果在javascript中实现经典的类。

javascript中类的一个重要特征是“动态可继承”(dynamically extendable),9.4会详细解释这一特性。我们可以将类看做是类型,9.5讲解检测类的几种方式,改节还介绍了一种编程哲学——“鸭式辨型”,它弱化了对象的类型,强化了对象功能。

在讨论了javascript中所有基本面向对象编程特性之后,我们将关注点从抽象的概念转化为一些实例。9.6节介绍了两种非常重要实现类的方法,包括很多实现面向对象的技术,这些技术可以在很大程度上增强类的功能。9.7节展示(包含很多示例代码)如何实现类的继承,包括如何在javascript中实现类的继承。9.8节讲解如何使用ECMAScript5中的新特性来实现类及面向对象编程。

定义类是模块开发和重用代码的有效方式之一,本章最后一节会集中讨论javascript中的模块

1.类和对象

在javascript中,类的所有实例对象都从一个类型对象上继承属性。因此,原型对象是类的核心。在6.1中定义了inherit()函数,这个函数返回一个新创建的对象,然后继承自某个原型对象。如果定义了一个原型对象,然后通过inherit()函数创建了一个继承自它的对象,这样就定义了一个javascript类。通常,类的实例还需要进一步的初始化,通常是通过定义一个函数来创建并初始化这个新对象。参照下例子,给出一个表示“值的范围”定义了原型对象。还定义了一个“工厂”函数(参照工厂方法)用以创建并初始化类的实例。

            //一个简单的javascript类            //实现一个能表示值的范围的类                        //inherit函数            //inherit()返回了一个继承自原型对象p属性的新对象             //这里是有ECMAScript5中的Object.create()函数(如果存在的话)             //如果不存在Object.create,则使用其他方法            function inherit(p) {                if (p == null) throw TypeError(); //p是一个对象,不能是null                if (Object.create) //如果Object.create存在                    return Object.create(p); //直接使用它                var t = typeof p; //否则进一步检测                if (t !== "object" && t !== "function") throw TypeError;                function f() {}; //定义一个空构造函数                f.PRototype = p; //将其原型属性设置p                return new f(); //将f()创建p的继承对象            }            //inherit函数结束                        //这个工厂方法返回一个新的“范围对象”            function range(from,to){                //使用inherit()函数来创建对象,这个对象继承自下面定义的原型对象                //原型对象作为函数的一个属性存储,并定义所有“范围对象”所共享的方法(行为)                var r = inherit(range.methods);                                //储存新的“范围对象”启始位置和结束位置(状态)                //这两个属性是不可继承的,每个对象都拥有唯一的属性                r.from = from;                r.to = to;                                //返回这个新创建的对象                                return r;                            }                        //原型对象定义方法,这些方法为每个范围对象所继承            range.methods = {                //如果x在范围内,则返回true;否则返回false                //如果这个方法可以比较数字范围。也可以比较字符串和日期范围                includes:function(x){                    return this.from <= x && x <= this.to;},                //对于范围内每个整数都调用一次f                //这个方法只可用作数字范围                foreach:function (f){                    for (var x = Math.ceil(this.from); x <= this.to ; x++) f(x);                },                //返回表示这个范围的字符串                toString:function(){return "("+ this.from + "..." + this.to + ")";}            };                        //这是使用范围对象的一些例子            var r =range(1,3); //创建一个范围对象            r.includes(2); //true:2 在这个范围内//            r.foreach(console.log);            console.log(r)

在这个例子中有一些代码是没有用的。这段代码定义了一个工厂方法range(),用来创建新的范围对象。我们注意到,这里给range()函数定义了一个属性range.methods,用以便捷地存放定义类的原型对象。把原型对象挂载函数上没什么大不了,但也不是惯用做法。再者,注意range()函数给每个范围对象定义了from和to属性,用以定义范围的起始位置和结束位置,这两个属性是非共享的、可继承的方法都用到了form和to属性,而且使用了this关键字,为了指代它们,二者使用的this关键字来指代调运这个方法的对象。任何类的方法都可以通过this的这种基本用法来读取对象的属性。

2.类和构造函数

上边的例子中展示了javascript中定义类的其中一种方法。但这种方法并不常用,毕竟它没有定义构造函数,构造函数是用来初始化和创建对象的。8.2.iii已经讲到,使用new关键字来调用构造函数,使用new调用构造函数会创建一个新对象,因此,构造函数本身只需要初始化这个新对象的状态即可。调用构造函数的一个重要特征是,构造函数的prototype属性被用做新对象的原型。这意味着通过同一个构造函数创建的对象都是继承自一个相同的对象,因此它们都是一个类的成员。下面的例子对上面的例子的“范围类”做了修改,使用构造函数代替工厂函数:

             //表示值的范围的类的另一种实现             //这是一个构造函数,用以初始化新创建的“范围对象”             //注意,这里并没有创建并返回一个对象,仅仅是初始化            function Range(from, to) {                //存储这个“范围对象”的起始位置和结束位置(状态)                //这两个属性是不可继承的,每个对象都拥有唯一的属性                this.from = from;                this.to = to;            }             //所有的“范围对象”都继承自这个对象             //属性的名字必须是"prototype"            Range.prototype = {                //如果x在范围内,则返回true;否则返回false                //这个方法可以比较数字范围,也可以比较字符串和日期范围                includes: function(x) {                    return this.from <= x && x <= this.to;                },                //对于这个范围内的每个整数都调用一次f                //这个方法只可用于数字范围                foreach: function(f) {                    for (var x = Math.ceil(this.from); x <= this.to; x++) f(x);                },                //返回表示这个范围的字符串                toString: function() {                    return "(" + this.from + "..." + this.to + ")";                }            };             //这里是使用“范围对象”的一些例子            var r = new Range(1, 3); //创建一个范围对象            r.includes(2); //=>true 2在这个范围 内            r.foreach(console.log); //输出1 2 3            console.log(r); //输出对象(1...3)

将上面的两个例子对比,就发现两种定义类的技术差别。首先:工厂函数range()转化为构造函数时被重命名Range()。这里遵循了一个常见的编程约定:从某种意义上来讲,定义构造函数既是定义类,并且类首字母要大写,而普通的函数和方法首字母都是小写。

再者,注意Rang()构造函数是通过new关键字调用的,而range()工厂函数则不必使用new。前一个例子调用普通函数来创建新对象,后一个则使用构造函数来创建新对象。由于Rang()函数就是通过new关键字来调用的,所有不必调用inherit()或者其它什么的逻辑来创建新的对象。在调用构造函数之前就已经创建了新对象,通过this关键字可以获取这个新对象。Range()构造函数只不过是初始化this而已。构造函数甚至不必返回这个新创建的对象,构造函数会自动创建对象,然后将构造函数作为这个对象的方法来调用一次,最后返回这个新对象。事实上,构造函数的命名规则和普通函数是如此不同还有另外一个原因,构造函数调用和普通函数的调用是不尽相同的。构造函数就是用来“构造新对象”的,它必须通过关键字new来调用,如果将构造函数做普通函数的话,往往不会正常工作。开发者可以通过命名约定来判断是否应当在函数之前冠以关键字new。

上面两个例子还有一个非常重要的区别:就是原型对象的命名。在第一段示例代码中的原型是range.methods。这种命名方式很方便同时具有很好的语义,但有过于随意。在第二段代码中的原型是Rang.prototype,这是一个强制命名。对Range()构造函数的调用会自动使用Rang.prototype作为新Range对象的原型。

最后,需要注意的是前两个例子中两种类定义方法的相同之处,两者的范围方法定义和调用方式是完全一致的。

i.构造函数和类的标识:

上文提到,原型对象是类的唯一标识:当且仅当两个对象继承自同一个原型对象时,它们在属于同一个类的实例。而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象。那么这两个构造函数创建的实例是属于一个类的。

尽管构造函数不像原型那样基础,但构造函数是类的“外在表现”。 很明显,构造函数的名字通常用做类名。比如,我们说Rang()构造函数创建Range对象,然后根本的讲,当使用instanceof运算符来检测对象是否属于某个类时会用到构造函数。假设这里有一个对象r,我们想知道r是否是Range对象,我们来这样写:

r instanceof Range // 如果r继承自Rang.prototype,则返回true

实际上instanceof运算符不不会检查r是否是由Range()构造函数初始化而来,而会检查r是否继承Range.prototype。不过instanceof的语法强化了“构造函数是类公有标识”的概念,在本章的后面还会碰到对instanceof运算符的介绍。

ii.constructor属性 在上面的例子中,将Range.prototype定义为一个新对象,这个对象包含类所需要的方法。其实没必要新创建一个对象,用单个对象的直接量的属性就可以方便地定义原型上的方法。任何javascript函数都可以用做构造函数,并且调用构造函数是需要用到一个prototype属性 ,因此,每个javascript函数(ECMAScript5中的function.bind()方法返回的函数除外)都自动拥有一个prototype属性。这个属性的值是一个对象,这个对象包含唯一一个不可枚举的属性constructor属性的值是一个函数对象:

            var F = function() {}; //这是一个函数对象:            var p = F.prototype; //这是F相关联的原型对象            var c = p.constructor; //这是与原型相关的函数            c === F; //=>true  对于任意函数F.prototype.constructor == F

可以看到构造函数的原型中存在预先定义好的constructor属性,这意味着对象通常继承的constructor均代指他们的构造函数。由于构造函数是类的“公共标识”,因此这个constructor属性为对象提供了类。

            var o = new F(); //创建类F的一个对象            o.constructor === F //=>true ,constructor属性指代这个类

如下图所示,展示了构造函数和原型之间的关系,包括原型到构造函数的反向引用及构造函数创建的实例。

需要注意的是,使用早前的Range()构造函数作为示例,但实际上,定义的Range类使用它自身的一个新对象重写了预定义的Range.prototype对象。这个新定义的原型对象不含有constructor属性。因此Range类的实例也不包含有constructor属性。我们可以通过补救措施来修正这个问题,显式的给原型添加一个构造函数:

            Range.prototype = {                constructor: Range, //显式的设置构造函数反向引用                includes: function(x) {                    return this.from <= x && x <= this.to;                },                foreach: function(f) {                    for (var x = Math.ceil(this.fro
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表