使用面向对象技术创建高级Web应用程序

最近,我面试了一位具有5年Web应用开发经验的软件开发人员。她有4年半的JavaScript编程经验,自认为自己具有非常优秀的JavaScript技能,可是,随后我很快发现,实际上她对JavaScript却知之甚少。然而,我并不是要为此而责怪她。JavaScript就是这么不可思议。有很多人(也包括我自己,这种情况直到最近才有所改观)都自以为是,觉得因为他们懂C/C++/C#或者具有编程经验,便以为他们非常擅长JavaScript这门语言。

海晏ssl适用于网站、小程序/APP、API接口等需要进行数据传输应用场景,ssl证书未来市场广阔!成为成都创新互联公司的ssl证书销售渠道,可以享受市场价格4-6折优惠!如果有意向欢迎电话联系或者加微信:13518219792(备注:SSL证书合作)期待与您的合作!

从某个角度讲,这种自以为是也并非毫无道理。用JavaScript做一些简单的事情是非常容易的。其入门的门槛非常低;这个语言待人宽厚,并不苛求你必须懂它很多才能开始用它编写代码。甚至对于非程序员来说,也可以仅花个把小时就能够上手用它为他的网站编写几段或多或少都有些用的脚本。

实际上直到最近,无论懂的JavaScript有多么少,仅仅在MSDN® DHTML参考资料以及我在C++/C#方面编程经验的帮助下,我都能够凑合过下去。直到我在工作中真正开始编写AJAX应用时,我才发现我对 JavaScript的了解有多么欠缺。这种新一代的Web应用复杂的交互特性要求使用一种完全不同的方式来编写JavaScript代码。这些都是非常严肃的JavaScript应用!我们以往那种漫不经心编写脚本的方法不灵了。

面向对象的编程(OOP)这种方法广泛用于多种JavaScript库,采用这种方法可使代码库更加易于管理和维护。JavaScript支持OOP,但它的支持方式同流行的Microsoft® .NET框架下的C++、C#、Visual Basic®等语言完全不同,所以,大量使用这些语言的开发者起初可能会发现,JavaScript中的OOP比较怪异,同直觉不符。我写这篇文章就是要对JavaScript到底是如何支持面向对象编程的以及如何高效利用这种支进行面向对象的JavaScript开发进行深入讨论。接下来让我们开始谈谈对象(除了对象还能有别的吗?)吧。

JavaScript对象是字典

在C++或C#中,当谈及对象时,我们指的是类或者结构的实例。对象根据实例化出它的模版(也即,类)的不同而具有不同的属性和方法。 JavaScript对象不是这样的。在JavaScript中,对象仅仅是name/value对的集合,我们可以把JavaScript对象看作字典,字典中的键为字符串。我们可以用我们熟悉的"." (点)操作符或者一般用于字典的"[]"操作符,来获取或者设置对象的属性。下面的代码片段

 
 
 
 
  1. var userObject = new Object();
  2. userObject.lastLoginTime = new Date();
  3. alert(userObject.lastLoginTime);

同这段代码所做的完全是同样的事情:

 
 
 
 
  1. var userObject = {}; // equivalent to new Object()
  2. userObject["lastLoginTime"] = new Date();
  3. alert(userObject["lastLoginTime"]);

我们还可以用这样的方式,直接在userObject的定义中定义lastLoginTime属性:

 
 
 
 
  1. var userObject = { "lastLoginTime": new Date() };
  2. alert(userObject.lastLoginTime);

请注意这同C# 3.0的对象初始化表达式是多么的相似。另外,熟悉Python的读者会发现,在第二段和第三段代码中,我们实例化userObject的方式就是 Python中指定字典的方式。这里唯一的区别的就是,JavaScript中的对象/字典只接受字符串作为键,而Python中字典则无此限制。

这些例子也表明,同C++或者C#对象相比,JavaScript对象是多么地更加具有可塑性。属性lastLoginTime不必事先声明,如果在使用这个属性的时候userObject还不具有以此为名的属性,就会在userObject中把这个属性添加进来。如果记住了JavaScript对象就是字典的话,你就不会对此大惊小怪了 —— 毕竟我们随时都可以把新键(及其对应的值)添加到字典中去。

JavaScript对象的属性就是这个样子的。那么,JavaScript对象的方法呢?和属性一样,JavaScript仍然和C++/C#不同。为了理解对象的方法,就需要首先仔细看看JavaScript函数。

JavaScript中的函数具有首要地位

在许多编程语言中,函数和对象一般都认为是两种不同的东西。可在JavaScript中,它们之间的区别就没有那么明显了 —— JavaScript中的函数实际上就是对象,只不过这个对象具有同其相关联的一段可执行代码。请看下面这段再普通不过的代码:

 
 
 
 
  1. function func(x) {
  2.     alert(x);
  3. }
  4. func("blah");

这是JavaScript中定义函数最常用的方式了。但是,你还可以先创建一个匿名函数对象再将该对象赋值给变量func,也即,象下面那样,定义出完全相同的函数

 
 
 
 
  1. var func = function(x) {
  2.     alert(x);
  3. };
  4. func("blah2");

或者甚至通过使用Function构造器,向下面这样来定义它:

 
 
 
 
  1. var func = new Function("x", "alert(x);");
  2. func("blah3");

这表明,函数实际上就是一个支持函数调用操作的对象。***这种使用Function构造器来定义函数的方式并不常用,但却为我们带来很多很有趣的可能,其原因可能你也已经发现了,在这种函数定义的方式中,函数体只是Function构造器的一个字符串型的参数。这就意味着,你可以在JavaScript运行的时候构造出任意的函数。

要进一步证明函数是对象,你可以就象为任何其它JavaScript对象一样,为函数设置或添加属性:

 
 
 
 
  1. function sayHi(x) {
  2.     alert("Hi, " + x + "!");
  3. }
  4. sayHi.text = "Hello World!";
  5. sayHi["text2"] = "Hello World... again.";
  6. alert(sayHi["text"]); // displays "Hello World!"
  7. alert(sayHi.text2); // displays "Hello World... again."

作为对象,函数还可以赋值给变量、作为参数传递给其它函数、作为其它函数的返回值、保存为对象的属性或数组中的一员等等。图1所示为其中一例。

图1 函数在JavaScript具有首要地位

 
 
 
 
  1. // assign an anonymous function to a variable
  2. var greet = function(x) {
  3.     alert("Hello, " + x);
  4. };
  5. greet("MSDN readers");
  6. // passing a function as an argument to another
  7. function square(x) {
  8.     return x * x;
  9. }
  10. function operateOn(num, func) {
  11.     return func(num);
  12. }
  13. // displays 256
  14. alert(operateOn(16, square));
  15. // functions as return values
  16. function makeIncrementer() {
  17.     return function(x) { return x + 1; };
  18. }
  19. var inc = makeIncrementer();
  20. // displays 8
  21. alert(inc(7));
  22. // functions stored as array elements
  23. var arr = [];
  24. arr[0] = function(x) { return x * x; };
  25. arr[1] = arr[0](2);
  26. arr[2] = arr[0](arr[1]);
  27. arr[3] = arr[0](arr[2]);
  28. // displays 256
  29. alert(arr[3]);
  30. // functions as object properties
  31. var obj = { "toString" : function() { return "This is an object."; } };
  32. // calls obj.toString()
  33. alert(obj);

记住这一点后,为对象添加方法就简单了,只要选择一个函数名并把一个函数赋值为这个函数名即可。接下来我通过将三个匿名函数分别赋值给各自相应的方法名,为一个对象定义了三个方法:

 
 
 
 
  1. var myDog = {
  2.     "name" : "Spot",
  3.     "bark" : function() { alert("Woof!"); },
  4.     "displayFullName" : function() {
  5.         alert(this.name + " The Alpha Dog");
  6.     },
  7.     "chaseMrPostman" : function() { 
  8.         // implementation beyond the scope of this article 
  9.     }    
  10. };
  11. myDog.displayFullName(); 
  12. myDog.bark(); // Woof!

函数displayFullName中"this"关键字的用法对C++/C#开发者来说并不陌生 —— 该方法是通过哪个对象调用的,它指的就是哪个对象(使用Visual Basic的开发者也应该熟悉这种用法 —— 只不过"this"在Visual Basic称作"Me")。因此在上面的例子中,displayFullName中"this"的值指的就是myDog对象。但是,"this"的值不是静态的。如果通过别的对象对函数进行调用,"this"的值也会随之指向这个别的对象,如图2所示。

#p#

图2 “this”随着对象的改变而改变

 
 
 
 
  1. function displayQuote() {
  2.     // the value of "this" will change; depends on 
  3.     // which object it is called through
  4.     alert(this.memorableQuote);    
  5. }
  6. var williamShakespeare = {
  7.     "memorableQuote": "It is a wise father that knows his own child.", 
  8.     "sayIt" : displayQuote
  9. };
  10. var markTwain = {
  11.     "memorableQuote": "Golf is a good walk spoiled.", 
  12.     "sayIt" : displayQuote
  13. };
  14. var oscarWilde = {
  15.     "memorableQuote": "True friends stab you in the front." 
  16.     // we can call the function displayQuote
  17.     // as a method of oscarWilde without assigning it 
  18.     // as oscarWilde’s method. 
  19.     //"sayIt" : displayQuote
  20. };
  21. williamShakespeare.sayIt(); // true, true
  22. markTwain.sayIt(); // he didn’t know where to play golf
  23. // watch this, each function has a method call()
  24. // that allows the function to be called as a 
  25. // method of the object passed to call() as an
  26. // argument. 
  27. // this line below is equivalent to assigning
  28. // displayQuote to sayIt, and calling oscarWilde.sayIt().
  29. displayQuote.call(oscarWilde); // ouch!

图2***一行的代码是将函数作为一个对象的方法进行调用的另外一种方式。别忘了,JavaScript中的函数是对象。每个函数对象都有一个叫做call的方法,这个方法会将函数作为该方法的***个参数的方法进行调用。也就是说,无论将哪个对象作为***个参数传递给call方法,它都会成为此次函数调用中"this"的值。后面我们就会看到,这个技术在调用基类构造器时会非常有用。

有一点要记住,那就是永远不要调用不属于任意对象却包含有"this"的函数。如果调用了的话,就会搅乱全局命名空间。这是因为在这种调用中,"this"将指向Global对象,此举将严重损害你的应用。例如,下面的脚本将会改变JavaScript的全局函数isNaN的行为。我们不推荐这么干。

 
 
 
 
  1. alert("NaN is NaN: " + isNaN(NaN));
  2. function x() {
  3.     this.isNaN = function() { 
  4.         return "not anymore!";
  5.     };
  6. }
  7. // alert!!! trampling the Global object!!!
  8. x();
  9. alert("NaN is NaN: " + isNaN(NaN));

到此我们已经看过了创建对象并为其添加熟悉和方法的几种方式。但是,如果你仔细看了以上所举的所以代码片段就会发现,所有的熟悉和方法都是在对象的定义之中通过硬性编码定义的。要是你需要对对象的创建进行更加严格的控制,那该怎么办?例如,你可能会需要根据某些参数对对象属性中的值进行计算,或者你可能需要将对象的属性初始化为只有到代码运行时才会得到的值,你还有可能需要创建一个对象的多个实例,这些要求也是非常常见的。

在C#中,我们使用类类实例化出对象实例。但是JavaScript不一样,它并没有类的概念。相反, 在下一小节你将看到,你可以利用这一点:将函数同"new"操作符一起使用就可以把函数当着构造器来用。

有构造函数但没有类

JavaScript中的OOP最奇怪的事,如前所述,就是JavaScript没有C#和C++ 中所具有的类。在C#中,通过如下这样的代码

 
 
 
 
  1. Dog spot = new Dog();

能够得到一个对象,这个对象就是Dog类的一个实例。但在JavaScript中根本就没有类。要想得到同类最近似的效果,可以象下面这样定义一个构造器函数:

 
 
 
 
  1. function DogConstructor(name) {
  2.     this.name = name;
  3.     this.respondTo = function(name) {
  4.         if(this.name == name) {
  5.             alert("Woof");        
  6.         }
  7.     };
  8. }
  9. var spot = new DogConstructor("Spot");
  10. spot.respondTo("Rover"); // nope
  11. spot.respondTo("Spot"); // yeah!

好吧,这里都发生了什么?先请不要管DogConstructor 函数的定义,仔细看看这行代码:

 
 
 
 
  1. var spot = new DogConstructor("Spot");

"new"操作符所做的事情很简单。首先,它会创建出一个新的空对象。然后,紧跟其后的函数调用就会得到执行,并且会将那个新建的空对象设置为该函数中"this"的值。换句话说,这行带有"new"操作符的代码可以看作等价于下面这两行代码:

 
 
 
 
  1. // create an empty object
  2. var spot = {}; 
  3. // call the function as a method of the empty object
  4. DogConstructor.call(spot, "Spot");

在DogConstructor的函数体中可以看出,调用该函数就会对调用中关键字"this"所指的对象进行初始化。采用这种方式,你就可以为对象创建模版了!无论何时当你需要创建类似的对象时,你就可以用"new"来调用该构造器函数,然后你就能够得到一个完全初始化好的对象。这和类看上去非常相似,不是吗?实际上,JavaScript中构造器函数的名字往往就是你想模拟的类的名字,所以上面例子中的构造函数你就可以直接命名为Dog:

 
 
 
 
  1. // Think of this as class Dog
  2. function Dog(name) {
  3.     // instance variable 
  4.     this.name = name;
  5.     // instance method? Hmmm...
  6.     this.respondTo = function(name) {
  7.         if(this.name == name) {
  8.             alert("Woof");        
  9.         }
  10.     };
  11. }
  12. var spot = new Dog("Spot");

上面在Dog的定义中,我定义了一个叫做name的实例变量。将Dog作为构造器函数使用而创建的每个对象都有自己的一份叫做name的实例变量(如前所述,name就是该对象的字典入口)。这符合我们的期望;毕竟每个对象都需属于自己的一份实例变量,只有这样才能保存它自己的状态。但是如果你再看接下来的那行代码,就会发现Dog的每个实例都有自己的一份respondTo方法,这可是个浪费;respondTo的实例你只需要一个,只有将这一个实例在所有的Dog实例间共享即可!你可以把respondTo的定义从Dog中拿出来,这样就可以克服此问题了,就向下面这样:

 
 
 
 
  1. function respondTo() {
  2.     // respondTo definition
  3. }
  4. function Dog(name) {
  5.     this.name = name;
  6.     // attached this function as a method of the object
  7.     this.respondTo = respondTo;
  8. }

这样一来,Dog的所有实例(也即,用构造器函数Dog创建的所有实例)都可以共享respondTo方法的同一个实例了。但是,随着方法数量的增加,这种方式维护起来会越来越困难。***你的代码库中会堆积大量的全局函数,而且,随着“类”的数量不断增加,特别是这些类的方法具有类似的方法名时,情况会变得更加糟糕。这里还有一个更好的办法,就是使用原型对象,这就是下一个小节要讨论的内容。

原型(Prototype)

原型对象是JavaScript面向对象编程中的一个核心概念。原型这个名称来自于这样一个概念:在JavaScript中,所有对象都是通过对已有的样本(也即,原型)对象进行拷贝而创建的。该原型对象的所有属性和方法都会成为通过使用该原型的构造函数生成的对象的属性和方法。你可以认为,这些对象从它们的原型中继承了相应的属性和方法。当你象这样来创建一个新的Dog对象时

 
 
 
 
  1. var buddy = new Dog("Buddy");

buddy所引用的对象将从它的原型中继承到相应的属性和方法,虽然仅从上面这一行代码可能会很难看出来其原型来自哪里。buddy对象的原型来自来自构造器函数(在此例中指的就是函数Dog)的一个属性。

在JavaScript中,每个函数都有一个叫做“prototype”的属性,该属性指向一个原型对象。发过来,该原型对象据有一个叫做"constructor"的属性,该属性又指回了这个函数本身。这是一种循环引用;图3 更好地揭示出了这种环形关系。

图3 每个函数的原型都具有一个叫做Constructor的属性 

好了,当一个函数(比如上例中的Dog)和"new"操作符一起使用,创建出一个对象时,该对象将从Dog.prototype中继承所有的属性。在图3中,你可以看出,Dog.prototype对象具有一个指会Dog函数的construtor属性,每个Dog对象(它们继承自Dog.prototype)将同样也具有一个指会Dog函数的constructor属性。图4中的代码证明了这一点。构造器函数、原型对象以及用它们创建出来的对象这三者之间的关系如图5所示。

#p#

图4对象同样也具有它们原型的属性

 
 
 
 
  1. var spot = new Dog("Spot");
  2. // Dog.prototype is the prototype of spot
  3. alert(Dog.prototype.isPrototypeOf(spot));
  4. // spot inherits the constructor property
  5. // from Dog.prototype
  6. alert(spot.constructor == Dog.prototype.constructor);
  7. alert(spot.constructor == Dog);
  8. // But constructor property doesn’t belong
  9. // to spot. The line below displays "false"
  10. alert(spot.hasOwnProperty("constructor"));
  11. // The constructor property belongs to Dog.prototype
  12. // The line below displays "true"
  13. alert(Dog.prototype.hasOwnProperty("constructor"));


图5 继承自它们的原型的实例

有些读者可能已经注意到了图4中对hasOwnProperty方法和isPrototypeOf方法的调用。这些方法又来自哪里呢?它们并不是来自Dog.prototype。实际上,JavaScript中还有其它一些类似于toString、 toLocaleString和valueOf等等我们可以直接对Dog.prototype以及Dog的实例进行调用的方法,但它们统统都不是来自于 Dog.prototype的。其实就象.NET框架具有System.Object一样,JavaScript中也有 Object.prototype,它是所有类的最***的基类。(Object.prototype的原型为null。)

在这个例子中,请记住Dog.prototype也是一个对象。它也是通过对Object的构造函数进行调用后生成的,虽然这一点在代码中并不直接出现:

 
 
 
 
  1. Dog.prototype = new Object();

所以,就如同Dog的实例继承自Dog.prototype一样,Dog.prototype继承自Object.prototype。这就使得Dog的所有实例也都会继承Object.prototype的方法和实例。

每个JavaScript对象都会继承一个原型链,该链的最末端都是Object.prototype。请注意,到此为止你在这里所见到的继承都是活生生的对象间的继承。这同你通常所认识的类在定义时形成的继承的概念不同。因此,JavaScript中的继承要来得更加的动态化。继承的算法非常简单,就是这样的:当你要访问一个对象的属性/方法时,JavaScript会首先对该属性/方法是否定义于该对象之中。如果不是,接下来就要对该对象的原型进行检查。如果还没有发现相应的定义,然后就会对该对象的原型的原型进行检查,并以此类推,直到碰到Object.prototype。图6所示即为这个解析过程。

图6 在原型链中对toString()方法进行解析

JavaScript这种动态解析属性访问和方法调用的方式将对JavaScript带来一些影响。对原型对象的修改会马上在继承它的对象中得以体现,即使这种修改是在对象创建后才进行的也无关紧要。如果你在对象中定义了一个叫做X的属性/方法,那么该对象原型中同名的属性/方法就会无法访问到。例如,你可以通过在Dog.prototype中定义一个toString方法来对Object.prototype中的toString方法进行重载。所有修改指挥在一个方向上产生作用,即慈宁宫原型到继承它的对象这个方向,相反则不然。

图7所示即为这种影响。图7还演示了如何解决前文碰到的避免不必要的方法实例问题。不用让每个对象都具有一个单独的方法对象的实例,你可以通过将方法放到其原型之中来让所有对象共享同一个方法。此例中,getBreed方法由 rover和spot共享 —— 至少直到在spot中重载了getBreed(译者注:原文为toString,应为笔误)方法之前。spot在重载之后就具有自己版本的getBreed方法,但是rover对象以及随后使用new和GreatDane创建的对象仍将继承的是定义于GreatDane.prototype对象的getBreed方法。

图7 从原型中进行继承

 
 
 
 
  1. function GreatDane() { }
  2. var rover = new GreatDane();
  3. var spot = new GreatDane();
  4. GreatDane.prototype.getBreed = function() {
  5.     return "Great Dane";
  6. };
  7. // Works, even though at this point
  8. // rover and spot are already created.
  9. alert(rover.getBreed());
  10. // this hides getBreed() in GreatDane.prototype
  11. spot.getBreed = function() {
  12.     return "Little Great Dane";
  13. };
  14. alert(spot.getBreed()); 
  15. // but of course, the change to getBreed 
  16. // doesn’t propagate back to GreatDane.prototype
  17. // and other objects inheriting from it,
  18. // it only happens in the spot object
  19. alert(rover.getBreed());

静态属性和方法

有些时候你会需要同类而不是实例捆绑到一起的属性或方法 —— 也即,静态属性和静态方法。在JavaScript中这很容易就能做到,因为函数就是对象,所以可以随心所欲为其设置属性和方法。既然构造器函数在 JavaScript代表了类这个概念,所以你可以通过在构造器函数中设置属性和昂奋来为一个类添加静态方法和属性,就象这样:

 
 
 
 
  1. function DateTime() { }
  2.     // set static method now()
  3.     DateTime.now = function() {
  4.         return new Date();
  5.     };
  6.     alert(DateTime.now());

在JavaScript调用静态方法的语法实际上和C#完全相同。既然构造器函数就是类的名字,所以这也不应该有什么奇怪的。这样你就有了类、共有属性/ 方法以及静态属性/方法。你还需要什么呢?当然,还需要私有成员。但是,JavaScript并不直接支持私有成员(这方面它也不支持protected 成员)。对象的所以属性和方法所有人都可以访问得到。这里有一种在类中定义出私有成员的方法,但要完成这个任务就需要首先对闭包有所了解。

闭包

我学JavaScript完全是迫不得已。因为我意识到,不学习JavaScript,就无法为在工作中参加编写真正的AJAX应用做好准备。起初,我有种在程序员的级别中下降了不少等级的感觉。(我要学JavaScript了!我那些使用C++的朋友该会怎么说我啊?)但是一旦我克服了起初的抗拒心理之后,我很快发现,JavaScript实际上是一门功能强大、表达能力极强而且很小巧的语言。它甚至拥有一些其它更加流行的语言才刚刚开始支持的特性。

JavaScript中更加高级的一个特性便是它对闭包的支持,在C# 2.0中是通过匿名方法对闭包提供支持的。闭包是一种运行时的现象,它产生于内部函数(在C#中成为内部匿名方法)本绑定到了其外部函数的局部变量之上的时候。显然,除非内部函数可以通过某种方式在外部函数之外也可以让其可以访问得到,否则这也没有多大意义。举个例子就可以把这个现象说得更清楚了。

假如你需要基于一个简单评判标准对一个数字序列进行过滤,该标准就是大于100的数字可以留下,但要把其它的所以数字都过滤掉。你可以编写写一个如图8所示的函数。

图8 基于谓词(Predicate)对元素进行过滤

 
 
 
 
  1. function filter(pred, arr) {
  2.     var len = arr.length;
  3.     var filtered = []; // shorter version of new Array();
  4.     // iterate through every element in the array...
  5.     for(var i = 0; i < len; i++) {
  6.         var val = arr[i];
  7.         // if the element satisfies the predicate let it through
  8.         if(pred(val)) {
  9.             filtered.push(val);
  10.         }
  11.     }
  12.     return filtered;
  13. }
  14. var someRandomNumbers = [12, 32, 1, 3, 2, 2, 234, 236, 632,7, 8];
  15. var numbersGreaterThan100 = filter(
  16.     function(x) { return (x > 100) ? true : false; }, 
  17.     someRandomNumbers);
  18. // displays 234, 236, 632
  19. alert(numbersGreaterThan100);

但是现在你想新建一个不同的过滤标准,比方说,这次只有大于300的数字才能留下。你可以这么做:

 
 
 
 
  1. var greaterThan300 = filter(
  2.     function(x) { return (x > 300) ? true : false; }, 
  3.     someRandomNumbers);

可能还需要留下大于50、25、10、600等等的数字,然而,你是如此聪明,很快就会发现它们使用的都是“大于”这同一个谓词,所不同的只是其中的数字。所以,你可以把具体的数字拿掉,编写出这么一个函数:

 
 
 
 
  1. function makeGreaterThanPredicate(lowerBound) {
  2.     return function(numberToCheck) {
  3.         return (numberToCheck > lowerBound) ? true : false;
  4.     };
  5. }

有了这个函数你就可以象下面这样做了:

 
 
 
 
  1. var greaterThan10 = makeGreaterThanPredicate(10);
  2. var greaterThan100 = makeGreaterThanPredicate(100);
  3. alert(filter(greaterThan10, someRandomNumbers));
  4. alert(filter(greaterThan100, someRandomNumbers));

请注意makeGreaterThanPredicate函数所返回的内部匿名函数。该匿名内部函数使用了lowerBound,它是传递给 makeGreaterThanPredicate的一个参数。根据通常的变量范围规则,当makeGreater­ThanPredicate函数退出后,lowerBound就离开了它的作用范围!但是在此种情况下,内部匿名函数仍然还携带着它,即使 make­GreaterThanPredicate早就退出了也还是这样。这就是我们称之为闭包的东西 ——— 因为内部函数关闭着它的定义所在的环境(也即,外部函数的参数和局部变量)。

乍一看,闭包也许没什么大不了的。但是如果使用得当,使用它可以在将你的点子转变为代码时,为你打开很多非常有意思的新思路。在JavaScript中闭包最值得关注的用途之一就是用它来模拟出类的私有变量。

#p#

模拟私有属性

好的,现在让我们来看看在闭包的帮助下怎样才能模拟出私有成员。函数中的私有变量通常在函数之外是访问不到的。在函数执行结束后,实际上局部变量就会永远消失。然而,如果内部函数捕获了局部变量的话,这样的局部变量就会继续存活下去。 这个实情就是在JavaScript中模拟出私有属性的关键所在。请看下面的Person类:

 
 
 
 
  1. function Person(name, age) {
  2.     this.getName = function() { return name; };
  3.     this.setName = function(newName) { name = newName; };
  4.     this.getAge = function() { return age; };
  5.     this.setAge = function(newAge) { age = newAge; };
  6. }

参数name和age对构造器函数Person来说就是局部变量。一旦Person函数返回之后,name 和age就应该被认为永远消失了。然而,这两个参数被4个内部函数捕获,这些内部函数被赋值为Person实例的方法了,因此这样一来就使得name和 age能够继续存活下去,但却被很严格地限制为只有通过这4个方法才能访问到它们。所以,你可以这样做:

 
 
 
 
  1. var ray = new Person("Ray", 31);
  2. alert(ray.getName());
  3. alert(ray.getAge());
  4. ray.setName("Younger Ray");
  5. // Instant rejuvenation!
  6. ray.setAge(22);
  7. alert(ray.getName() + " is now " + ray.getAge() + 
  8.       " years old.");

不必在构造器中进行初始化的私有成员可以声明为构造器函数的局部变量,就象这样:

要注意的是,这样的私有成员同我们所认为的C#中的私有成员稍有不同。在C#中,类的公开方法可以直接访问类的私有成员。但是在JavaScript中,私有成员只有通过在闭包中包含有这些私有成员的方法来访问(这样的方法通常称为特权方法,因为它们不同于普通的公开方法)。因此,在Person的公开方法中,你依然可以通过Person的特权方法方法来访问私有成员:

大家广泛认为,Douglas Crockford是***个发现(或者可能说发表更合适)使用闭包来模拟私有成员的人。他的网站,javascript.crockford.com,包含了JavaScript方面的大量信息 —— 对JavaScript感兴趣的开发人员都应该去他的网站看看。

 
 
 
 
  1. function Person(name, age) {
  2.     var occupation;
  3.     this.getOccupation = function() { return occupation; };
  4.     this.setOccupation = function(newOcc) { occupation = 
  5.                          newOcc; };
  6.     // accessors for name and age    
  7. }

要注意的是,这样的私有成员同我们所认为的C#中的私有成员稍有不同。在C#中,类的公开方法可以直接访问类的私有成员。但是在JavaScript中,私有成员只有通过在闭包中包含有这些私有成员的方法来访问(这样的方法通常称为特权方法,因为它们不同于普通的公开方法)。因此,在Person的公开方法中,你依然可以通过Person的特权方法方法来访问私有成员:

 
 
 
 
  1. Person.prototype.somePublicMethod = function() {
  2.     // doesn’t work!
  3.     // alert(this.name);
  4.     // this one below works
  5.     alert(this.getName());
  6. };

大家广泛认为,Douglas Crockford是***个发现(或者可能说发表更合适)使用闭包来模拟私有成员的人。他的网站,javascript.crockford.com,包含了JavaScript方面的大量信息 —— 对JavaScript感兴趣的开发人员都应该去他的网站看看。

类的继承

好的,现在你已经看到了如何通过构造器函数和原型对象在JavaScript中模拟类。你也已经了解原型链可以确保所有的对象都能具有 Object.prototype中的通用方法。你还看到了如何使用闭包来模拟出私有成员。但是,这里好像还是缺点什么东西。你还没看到在 JavaScript中如何实现类的继承;这在C#中可是司空见惯的事情。很不幸,在JavaScript进行类的继承无法象在C#中那样键入一个冒号而实现;在JavaScript中还需要做更多的事情。但从另一方面讲,因为JavaScript非常灵活,我们有多种途径实现类的继承。

比方说,如图9所示,你有一个基类叫Pet,它有一个派生类叫做Dog。怎样在JavaScript中实现这个继承关系呢?Pet类就很简单了,你已经看到过怎么实现它了:

图9 类 

 
 
 
 
  1. // class Pet
  2. function Pet(name) {
  3.     this.getName = function() { return name; };
  4.     this.setName = function(newName) { name = newName; };
  5. }
  6. Pet.prototype.toString = function() {
  7.     return "This pet’s name is: " + this.getName();
  8. };
  9. // end of class Pet
  10. var parrotty = new Pet("Parrotty the Parrot");
  11. alert(parrotty);

那该如何定义派生自Pet类的Dog类呢?从图9中可看出,Dog类具有一个额外的属性,breed,,并且它还重载了Pet的toString方法(请注意,avaScript中的方法和属性命名惯例采用的是驼峰式大小写方式,即camel case;而C#推荐使用的是Pascal大小写方式)。图10所示即为Pet类的定义实现方法:

图10继承Pet类

 
 
 
 
  1. // class Dog : Pet 
  2. // public Dog(string name, string breed)
  3. function Dog(name, breed) {
  4.     // think Dog : base(name) 
  5.     Pet.call(this, name);
  6.     this.getBreed = function() { return breed; };
  7.     // Breed doesn’t change, obviously! It’s read only.
  8.     // this.setBreed = function(newBreed) { name = newName; };
  9. }
  10. // this makes Dog.prototype inherits
  11. // from Pet.prototype
  12. Dog.prototype = new Pet();
  13. // remember that Pet.prototype.constructor
  14. // points to Pet. We want our Dog instances’
  15. // constructor to point to Dog.
  16. Dog.prototype.constructor = Dog;
  17. // Now we override Pet.prototype.toString
  18. Dog.prototype.toString = function() {
  19.     return "This dog’s name is: " + this.getName() + 
  20.         ", and its breed is: " + this.getBreed();
  21. };
  22. // end of class Dog
  23. var dog = new Dog("Buddy", "Great Dane");
  24. // test the new toString()
  25. alert(dog);
  26. // Testing instanceof (similar to the is operator)
  27. // (dog is Dog)? yes
  28. alert(dog instanceof Dog);
  29. // (dog is Pet)? yes
  30. alert(dog instanceof Pet);
  31. // (dog is Object)? yes
  32. alert(dog instanceof Object);

通过正确设置原型链这个小把戏,就可以同在C#中所期望的那样,使得instanceof测试在JavaScript中也能够正常进行。而且如你所愿,特权方法也能够正常得以运行。

#p#

模拟命名空间

在C++和C#中,命名空间用来将命名冲突的可能性减小到最小的程度。例如,在.NET框架中,命名空间可以帮助我们区分出 Microsoft.Build.Task.Message和Sys­tem.Messaging.Message这两个类。JavaScript并没有明确的语言特性来支持命名空间,但使用对象可以非常容易的模拟出命名空间。比如说你想创建一个JavaScript代码库。不想在全局中定义函数和类,你就可以将你的函数和类封装到如下这样的命名空间之中:

 
 
 
 
  1. var MSDNMagNS = {};
  2. MSDNMagNS.Pet = function(name) { // code here };
  3. MSDNMagNS.Pet.prototype.toString = function() { // code };
  4. var pet = new MSDNMagNS.Pet("Yammer");

只有一层命名空间可能会出现不唯一的请看,所以你可以创建嵌套的命名空间:

 
 
 
 
  1. var MSDNMagNS = {};
  2. // nested namespace "Examples"
  3. MSDNMagNS.Examples = {}; 
  4. MSDNMagNS.Examples.Pet = function(name) { // code };
  5. MSDNMagNS.Examples.Pet.prototype.toString = function() { // code };
  6. var pet = new MSDNMagNS.Examples.Pet("Yammer");

不难想象,每次都键入这些很长的嵌套命名空间很快就会让人厌烦。幸运的是,你的代码库的用户可以很容易地为你的命名空间起一个比较简洁的别名:

你要是看一眼Microsoft AJAX代码库的源代码的话,就会发现该库的编写者也使用了类似的技巧来实现命名空间(请看静态方法Type.registerNamespace的实现代码)。这方面更详细的信息可参见"OOP and ASP.NET AJAX"的侧边栏。

 
 
 
 
  1. // MSDNMagNS.Examples and Pet definition...
  2. // think "using Eg = MSDNMagNS.Examples;" 
  3. var Eg = MSDNMagNS.Examples;
  4. var&

    当前文章:使用面向对象技术创建高级Web应用程序
    文章位置:http://www.hantingmc.com/qtweb/news37/220237.html

    网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联