Javascript学习6 - 类、对象、继承

原文:Javascript学习6 - 类、对象、继承

Javasciprt并不像C++一样支持真正的类,也不是用class关键字来定义类。Javascript定义类也是使用function关键字来完成的。
    在Javascript中,可以把对象(object)定义为“属性的无序集合”,每个属性存放一个原始值、对象或者函数。理解这一点非常重要。
ECMAScript定义:对象由特性(attribute)构成,特性可以是原始值,可以是引用值,如果特性存放的是函数,它将被看作对象的方法(Method),否则该特性被看作是属性(property).

     ★ 关于对象创建和撤销,有以下几个知识点:

       ● 对象的声明和实例化 : 对象是由关键字new后跟实例化的类的名字所创建,如 var obj = new Object()            ● 对象的引用,在ECMAScript中,不能访问对象的物理表示,只能访问对象的引用。每次创建对象,存储在变量中的都是对象的引用,而不是对象本身。                如上面的obj就是一个对象的引用。             ● 对象废除: ECMAScript有无用存储单元收集程序,当再没有对象的引用时,该对象就被废除(dereference),就相当于.net中的垃圾收集。每当函数执行完代码,无用存储单元收集程序都会运行,释放所有的局部变量。另外,也可以把对象的所有引用都设为null,可以强制废除对象。如果一个对象有两个或更多引用,必须将所有引用设置为null,才能正确废除对象。

★ 关于对象的早绑定(early binding)和晚绑定(late binding)

        ● 所谓绑定,就是把对象的接口与对象的实例结合在一起的方法             ● 早绑定: 在实例化对象前,类已经定义了它的特性和方法,这样编辑器或解释器就能提前转换机器代码。但ECMAScript不是强类型语言,所以不支持早绑定              ● 晚绑定: 指的是编辑器或解释器程序在运行前,不知道对象的类型。使用晚绑定,无需检查对象的类型,只需要检查对象是否支持特性和方法。ECMAScript支持晚绑定。

★ Javascript中的本地对象(native object)、内置对象(built-in object) 、宿主对象(host-object)

        ● 本地对象: 独立于宿主环境的ECMAScript实现并提供的一些对象。简单的说,就是ECMA-262标准所定义的类(引用类型),它们包括:                            Object,Function,Array,String,Boolean,Number,Date,RegExp,Error,EvalError,RangError                            ReferenceError,SyntaxError,TypeError,URIError              ● 内置对象: 由ECMAScript实现提供,独立于宿主环境的所有对象,在ECMAScript程序开始执行时出现。开发者不必明确实例化内置对象,它已经在程序开始时被实例化了。ECMA-262有两个内置对象: Global和Math。 每个内置对象也是本地对象                   需要注意的是Global对象是ECMAScript中最特别的一个对象,因为它实际上根本不存在。尝试编写如下代码将得到错误:      var pointer = Global;                  但是,它又隐含的在ECMAScript中体现,需要理解的一个概念是,在ECMAScript中,不存在独立的函数,那么我们平常使用的 isNaN(),parsetInt(),Eval()函数都属于哪个对象呢?答案就是Global对象。记住的一点是,Global不能实际引用,可以把ECMAScript环境看作就是一个全局的已经被实例化的Global对象。一些方法如isNaN等无需生成Global对象进行引用。                  另外,Global不止有方法,还有属性,这些属性包括undefined,NaN,Object,Array,Function….             ● 宿主对象: 所有的非本地对象都是宿主对象,所有自定义的,各浏览器支持的BOM,DOM对象都是宿主对象。下面主要讨论的就是如何构造宿主对象       

6.1 定义类或对象的三种方式

具体来说,定义类或对象包括了多种方式可以定义,分别是: 对象创建后动态定义,工厂方式,构造函数方式,原型方式,混合构造函数/原型方式,动态原型。下面具体来讨论一下 
       6.1.1 对象创建后动态定义
           在Javascipt中,因为对象的属性可以在对象创建后动态定义,所以一般不需要定义类,创建完对象后再定义它拥有的属性方法也是可行的。如下例所示:

var oCar = new Object();     oCar.color = "red";     oCar.doors = ;     oCar.mpg =;     oCar.showColor = function() { alert(this.color); };   //注意最后的分号不要忘记     

执行以上代码后,就拥有一个个对象oCar, 这个对象拥有color,doors,mpg属性,拥有showColor方法。但是最大的问题是,不能使用创建多个实例【当然,如果说把代码复制下来,对象名变一变,我也无话可说。】

         6.1.2 工厂方法
            这个方法可以通过调用函数来创建对象的实例。就像一个工厂方法一样,调用一下函数,一个对象就出来了。

function createCar(sColor,iDoors,iMpg)     {          var oTempCar = new Object;         oTempCar.color = sColor;         oTempCar.doors = iDoors;         oTempCar.mpg = iMpg;         oTempCar.showColor = function() { alert(this.color); };  //注意最后的分号不要忘记         return oTempCar;  // 返回一个创建的对象。     }     var oCar1  = createCar("red",,);     var oCar1  = createCar("blue",,);     oCar1.showColor();      oCar2.showColor();     

以上,对象可以成批的被创建,但是语义上有问题,1.不像使用new关键字构造对象那么正规,2.每次创建对象,showColor是引用的一个函数对象,该函数引用对于每个对象都有一份实例,即每个对象的showColor都指向自己Function引用。而函数是可以被共享的。
           一个重要的知识点为:一个类中,一般来说,属性是每个对象所私有的,而方法可以作为引用被每个对象所共享(因为方法只是一段代码,并不存放实体数据。)

         6.1.3 构造函数方式
               构造函数第一步选择类名,根据惯例,类名采用首字母大写。变量名小写,私有变量名以_开头。

// Define the constructor.      // Note how it initializes the object referred to by "this".      function Rectangle(w, h) {           this._width = w;           this._height = h;           this.area = function( ) { return this._width * this._height; }  //每个对象都维持一份Copy     // Note: no return statement here        }      var rect1 = new Rectangle(, );    // rect1 = { width:2, height:4 };

以上,就创建一个构造函数,与工厂方法的区别是,在构造函数内不创建对象,而是使用this关键字。然后,使用new运算符进行构造对象。
          像工厂函数一样,使用构造函数方式,会重复生成函数,为每个对象都创建独立的函数版本。
 
        6.1.4 原型方式
           原型方式利用了对象的prototype属性,声明类的步骤与构造函数方式相似,不同的是,所有成员加上prototype修饰

function Rectangle() {  }   // 构造函数为空     Rectangle.prototype._width=;     Rectangle.prototype._height=;     Rectangle.prototype.area=function() {return this._widht*this._height;};     //以上,prototype定义在构造函数外

以上,解决了每个对象都会重复生成函数的问题,但是,又带来一个新的问题,就是私有变量_width,_height也被各个对象所共享了,类的一个对象修改了变量,另一个对象也会被反映出来。所以,最好的生成对象方式是使用构造+原型方式

        6.1.5 混合构造函数/原型方式
             使用这种方式,概念比较简单,即用构造函数定义对象的所有非函数属性,用原型方式定义对象的函数属性(方法)。结果是每个对象都具有自己的对象属性实例,且所有的函数也只被创建一次。

function Rectangle(w, h) {            this.width = w;            this.height = h;  }             // The prototype object holds methods and other properties that      // should be shared by each instance.      Rectangle.prototype.area = function( ) { return this.width * this.height; }      

var r = new Rectangle(, );      r.hasOwnProperty("width");   // true: width is a direct property of r      r.hasOwnProperty("area");    // false: area is an inherited property of r      "area" in r;                 // true: "area" is a property of r      

6.1.6 动态原型
           使用混合构造/原型方式的唯一缺点就是在类外面也定义了相应的原型方法,不能把这些方法定义在构造函数中,作为一个定义整体。
           使用动态原型可以做到这一点,但需要额外写点代码:

function Rectangle(){         this._width=;         this._height=;         if( typeof Rectangle._initialized==”undefined”) {   //判断是否已经被构造            this.prototype.area=function() {return this._widht*this._height;};            Rectangle._initialized = true;                           // 设定相应的原型函数已经被构造         }     }  

6.2  究竟采用哪一种方式进行构造

在6.1节中,使用了6种方式进行对象的构造,但是,究竟采用哪一种比较好呢?目前最广泛使用的是混合构造函数/原型方式,此外,动态原型也很流行,功能上也与混合构造函数/原型等价,可以采用两种方式中的任何一种
        但是,不要采用单独的构造函数或原型方式,这样会带来问题


6.3 扩展内建的类型 (修改对象)

用户定义的类可以增加原型对象,另外,像String和Date这样内建的类,JS语言已经定义了一些原型对象,如ChatAt,left等。也可以自定义为他们扩展相应的原型对象,如

 // Returns true if the last character is c       String.prototype.endsWith = function(c) {           return (c == this.charAt(this.length-))       }       var message = "hello world";       message.endsWith('h')  // Returns false       message.endsWith('d')  // Returns true       

注:绝对不能为Object.prototype添加属性。因为所有添加的任何属性和方法都可以用一个for/in循环来枚举,但一个空的对象{}(空的对象也属性Object)应该没有可枚举的属性。

6.4 实例方法和this.

在C++/Java面向对象的语言中,在类中访问自身的属性和方法,无需显式调用this.
        但是,在Javascript语言中,必须为这些属性显式指定this关键字。 
        在对象的方法中,关键字this总是指向调用该方法的对象。

6.5 类方法

类方法有点像C++/Java中的静态方法,无需实例化对象即可以进行调用。如Javascript调用Date.Parse()方法。
        类方法是和一个类而不是类的一个实例相关的方法,通过类自身来调用,而不是通过类的一个具体实例来调用。
        类方法通过一个构造函数来调用的,this关键字并不引用类的任何具体实例,相反,它引用的是构造函数自身。通常,一个类方法根本不使用this.
        和类属性一样,类方法也是全局的。
        请参考以下代码,定义了类方法:Circle.max()

// We begin with the constructor      function Circle(radius) {            // r is an instance property, defined and initialized in the constructor.            this.r = radius;        }             // Circle.PI is a class propertyit is a property of the constructor function.      Circle.PI = 3.14159;             // Here is an instance method that computes a circle's area.      // 函数原型      Circle.prototype.area = function( ) { return Circle.PI * this.r * this.r; }             // This class method takes two Circle objects and returns the      // one that has the larger radius.      // 类方法      Circle.max = function(a,b) {            if (a.r > b.r) return a;            else return b;        }             // Here is some code that uses each of these fields:      var c = new Circle(1.0);      // Create an instance of the Circle class      c.r = 2.2;                    // Set the r instance property      var a = c.area( );             // Invoke the area( ) instance method      var x = Math.exp(Circle.PI);  // Use the PI class property in our own computation      var d = new Circle(1.2);      // Create another Circle instance      var bigger = Circle.max(c,d); // Use the max( ) class method            

6.6 通用对象模型

在自定义类的时候,以下几种方法应该考虑定义。
        ①toString()方法
            每个对象都有自己特定的字符串表示,在定义一个类时,应该为它定义一个toString()方法,以便这个类的实例能够为有意义的字符串。
            另个,应该考虑为类添加一个静态parse()方法,用来把toString()方法的字符串输出解析回对象的形式。

Circle.prototype.toString = function ( ) {             return "[Circle of radius " + this.r + ", centered at ("                 + this.x + ", " + this.y + ").]";        }

②valueOf()方法
            与toString()方法相似,是在JS需要把一个对象转换成基本类型的时候才调用的,一般要转换成一个数字而不是一个字符串时,有可能使用到这个函数的地方。函数返回一个基本类型数值。
        ③compareTo()方法
            默认的,JS对于对象是按照运算符的地址来比较对象,而不是按值。给定两个对象引用,它会去查看两个对象是否都引用同一个对象。
            它不会去检查两个不同的对象具有相同的属性名和值。所以可以实现一个compareTo方法来进行自定义对象的比较:

Complex.prototype.compareTo = function(that) {            // If we aren't given an argument, or are passed a value that            // does not have a magnitude( ) method, throw an exception            // An alternative would be to return -1 or 1 in this case to say            // that all Complex objects are always less than or greater than            // any other values.            if (!that || !that.magnitude || typeof that.magnitude != "function")                throw new Error("bad argument to Complex.compareTo( )");                     // This subtraction trick returns a value less than, equal to, or            // greater than zero.  It is useful in many compareTo( ) methods.            return this.magnitude( ) - that.magnitude( );        }      

6.7 确定对象类型

Javascript是一个松散类型的语言,对象也是松散的类型,有以下几种方法可以用来确定JS中一个任意值的类型。
        ①typeof方法
            使用typeof有几点要注意:
                typeof null 是 "object"
                typeof undefined 是 "undefined"
                任何数组类型都是"object"
                任何函数类型都是"function",尽管函数也是对象!
        ②instanceof方法
            一旦确定一个值是对象而不是基本类型或者函数,就可以使用instanceof运算符来详细了解它。如果x是一个数组,如下的表达式为true
            x instanceof Array // return true;
            对象是自己的类的一个实例,也是任何超类的一个实例 。所以,以下方法也为true
            x instanceof Object // return true;
        ③构造函数方法
            如果要测试一个对象是一个具体的类的一个实例(而不是超类),可以查看对象的constructor属性,如下代码:

var d = new Date( );                     // A Date object; Date extends Object      var isobject = d instanceof Object;     // evaluates to true      var realobject = d.constructor==Object; // evaluates to false

④Object.toString()方法测试
            instancesof和constructor测试的方法一个缺点是,需要根据已经知道的类来进行测试,无法检测未知的对象。
            Object定义了一个默认的toString()方法,任何没有定义自己的toString()方法的类,都会继承这个默认的实现。
            默认的toString()方法总是返回以下形式的一个字符串:
                [object class]
            class是对象的内部类型,通常和该对象的构造函数名字相对应。例如,数组的class是Array,函数的class是Function...
        以上,要查看更具体的介绍,请参见《Javascript权威指南》 9.7节。

6.8 Javascript中类的继承

和定义类的功能一样,ECMAScript中实现继承的方式不止一种,这是因为Javascript中的继承机制并不是明确规定的,而是通过模仿来实现的。
     下面来讨论几种实现继承的方式:
     6.8.1 对象冒充

<script type="text/javascript" language="javascript">    function ClassA(sColor) {        this.color = sColor;        this.sayColor = function() { alert(this.color); };    }    function ClassB(sColor, sName) {        this.newMethods = ClassA;  // ClassA的构造函数成为ClassB的方法        this.newMethods(sColor);    // 商用ClassA的构造函数,ClassB会收到ClassA的构造函数中定义的属性和方法        delete this.newMethods;      // 删除ClassA的引用        this.name = sName;        this.sayName = function() { alert(this.name); };    }    var objB = new ClassB("blue", "Nicholas");    objB.sayColor();    objB.sayName();    alert(objB instanceof ClassA);  // 返回false     alert(objB instanceof ClassB);  // 返回true</script> 

以上,ClassA为构造函数,其实也只是一个函数,所以ClassA的构造子数成为了ClassB的一个方法,然后调用该方法,在ClassB中引入了ClassA()构造函数中定义的属性和方法。最后删除对ClassA的引用。这样一来,相当于ClassA构造函数中定义的属性和方法成为了ClassB中定义的属性及方法。
        对于instanceof方法,在使用对象冒充继承时,如果使用instanceof <基类名>是返回false的。如果使用了原型链继承,则会返回true.原型链继承在6.8.3节中介绍。
        需要指出的是,对象冒充方法可以支持多重继承。

function ClassZ() {    this.newMethod = ClassX;    this.newMethod();    delete this.newMethod;    this.newMethod = ClassY;    this.newMethod();    delete this.newMethod;} 

如果ClassX和ClassY具有同名的属性或方法,ClassY具有高优先级(后面定义的会覆盖前面定义的)
       另外,有继承类中的新属性和新方法必须在删除了新方法的代码行后定义,否则,同名的属性和函数会被基类所覆盖。
       需要强调的是:这样的冒充方法只适合所有的属性及方法在构造函数中进行定义的类。对于原型方法,不适用。对于原型继承,使用原型链方法,后面谈到。

    6.8.2 call()和apply()方法
       ● call方法与经典的对象冒充方法最相似,它第一个参数用作this的对象,表示需要被继承的对象,其它参数都直接传递给基类构造函数的。

function ClassB(sColor, sName) {    //this.newMethod = ClassA;    //this.newMethod(sColor);    //delete this.newMethod;    ClassA.call(this, sColor);  // 很简便,一行相当于上面三行。    this.name = sName;    this.sayName = function () { alert(this.name); };}// 使用直接定义对象方法。从相应的对象中继承。function sayColor(sPrefix, sSuffix) {    alert(sPrefix + this.color + sSuffix);};var obj = new Object();obj.color = “red”;sayColor.call(obj, “The color is “, “, a very nice color indeed. “);  // 表示obj对象继承自sayColor. 

● apply方法有两个参数,分别为this对象(即需要被继承的对象)和要传递给基类构造函数参数的数组。例如:

function ClassB(sColor, sName) {    //this.newMethod = ClassA;    //this.newMethod(sColor);    //delete this.newMethod;    ClassA.apply(this, new Array(sColor));  // 与call不同的是,第二个参数是一个参数数组.    this.name = sName;    this.sayName = function () { alert(this.name); };} 

6.8.3 原型链继承
       前面提到过,prototype对象是个模板,实例化的对象都是以这个模板为基础,进行对象的构造。那么,基类中使用prototype定义的属性和方法怎么被子类继承呢?

function ClassA(sColor) {    this.color = sColor;}ClassA.prototype.sayColor = function() { alert(this.color); };  // 原型方法 

如果需要继承ClassA中的原型方法,需要在子类中使用以下语句: ClassB.prototype = new ClassA();

function ClassB(sColor, sName) {    ClassA.call(this, sColor);   // 继承了ClassA中的构造方法    this.name = sName;    this.sayName = function() { alert(this.name); };}ClassB.prototype = new ClassA();  // 继承了ClassB中的原型方法var objB = new ClassB("blue", "Nicholas");objB.sayColor();objB.sayName();alert(objB instanceof ClassA);  // 返回true alert(objB instanceof ClassB);  // 返回true

在原型链继承中,有一点与对象冒充不同,就是使用<对象名> instanceof <基类>运算,是返回True
       还有一点:原型链不支持多重继承,因为,原型链会用另一个类型的对象重写类的prototype属性。

   6.8.4 关于继承的方式
       一般来说,在Javascript中,继承一个类,对象冒充及原型链方式都会被混合在一起使用,如6.8.3中的例子所示。使用对象冒充继承构造函数的属性,使用原型链继承prototype对象的方法。

对象的构造和继承就讲到这儿,这样的继承方式是在JS语言中很通用的做法,当然,可以使用其它的JS库来实现对象构造和继承。使用JS库可以解决JS中继承机制的一些缺陷,如支持namespace,能简单访问父类方法,支持接口…
       在ASP.NET AJAX中,会讲到ASP.NET中Microsoft Javascript Library怎么样来实现对象,继承,接口等操作的。ASP.NET AJAX详见:

上一篇:(适合入门)JVM堆内存相关的启动参数:年轻一代、岁和永久代内存分配


下一篇:Javascript学习总结三(Array对象的用法)