百度空间 | 百度首页 
 
查看文章
 
Json,prototype,闭包,构造函数
2009年01月07日 星期三 22:01

JSON   
   已经说了许多了许多话题了,但有一个很基本的问题我们忘了讨论,那就是:怎样建立对象?
  
   在前面的示例中,我们已经涉及到了对象的建立了。我们使用了一种被称为JavaScript Object Notation(缩写JSON)的形式,翻译为中文就是“JavaScript对象表示法”。
  
   JSON为创建对象提供了非常简单的方法。例如,
   创建一个没有任何属性的对象:
   var o = {};
   创建一个对象并设置属性及初始值:
   var person = {name: "Angel", age: 18, married: false};
   创建一个对象并设置属性和方法:
   var speaker = {text: "Hello World", say: function(){alert(this.text)}};
   创建一个更复杂的对象,嵌套其他对象和对象数组等:
   var company =
   {
   name: "Microsoft",
   product: "softwares",
   chairman: {name: "Bill Gates", age: 53, Married: true},
   employees: [{name: "Angel", age: 26, Married: false}, {name: "Hanson", age: 32, Marred: true}],
   readme: function() {document.write(this.name + " product " + this.product);}
   };
   JSON的形式就是用大括“{}”号包括起来的项目列表,每一个项目间并用逗号“,”分隔,而项目就是用冒号“:”分隔的属性名和属性值。这是典型的字典 表示形式,也再次表明了 JavaScript里的对象就是字典结构。不管多么复杂的对象,都可以被一句JSON代码来创建并赋值。
  
   其实,JSON就是JavaScript对象最好的序列化形式,它比XML更简洁也更省空间。对象可以作为一个JSON形式的字符串,在网络间自由传递和 交换信息。而当需要将这个JSON字符串变成一个JavaScript对象时,只需要使用eval函数这个强大的数码转换引擎,就立即能得到一个 JavaScript内存对象。正是由于JSON的这种简单朴素的天生丽质,才使得她在AJAX舞台上成为璀璨夺目的明星。
  
   JavaScript就是这样,把面向对象那些看似复杂的东西,用及其简洁的形式表达出来。卸下对象浮华的浓妆,还对象一个眉目清晰!
  
   构造对象
  
   好了,接下我们来讨论一下对象的另一种创建方法。
  
   除JSON外,在JavaScript中我们可以使用new操作符结合一个函数的形式来创建对象。例如:
   function MyFunc() {}; //定义一个空函数
   var anObj = new MyFunc(); //使用new操作符,借助MyFun函数,就创建了一个对象
   JavaScript的这种创建对象的方式可真有意思,如何去理解这种写法呢?
  
   其实,可以把上面的代码改写成这种等价形式:
   function MyFunc(){};
   var anObj = {}; //创建一个对象
   MyFunc.call(anObj); //将anObj对象作为this指针调用MyFunc函数
   我们就可以这样理解,JavaScript先用new操作符创建了一个对象,紧接着就将这个对象作为this参数调用了后面的函数。其实, JavaScript内部就是这么做的,而且任何函数都可以被这样调用!但从 “anObj = new MyFunc()” 这种形式,我们又看到一个熟悉的身影,C++和C#不就是这样创建对象的吗?原来,条条大路通灵山,殊途同归啊!

请看下面的代码:
1 function Person(name) //带参数的构造函数
   2 {
   3 this.name = name; //将参数值赋给给this对象的属性
   4 this.SayHello = function() {alert( "Hello, I'm " + this.name);}; //给this对象定义一个SayHello方法。
   5 };
   6
   7 function Employee(name, salary) //子构造函数
   8 {
   9 Person.call(this, name); //将this传给父构造函数
   10 this.salary = salary; //设置一个this的salary属性
   11 this.ShowMeTheMoney = function() {alert(this.name + " $" + this.salary);}; //添加ShowMeTheMoney方法。
   12 };
   13
   14 var BillGates = new Person( "Bill Gates"); //用Person构造函数创建BillGates对象
   15 var SteveJobs = new Employee( "Steve Jobs", 1234); //用Empolyee构造函数创建SteveJobs对象
   16
   17 BillGates.SayHello(); //显示:I'm Bill Gates
   18 SteveJobs.SayHello(); //显示:I'm Steve Jobs
   19 SteveJobs.ShowMeTheMoney(); //显示:Steve Jobs $1234
   20
   21 alert(BillGates.constructor == Person); //显示:true
   22 alert(SteveJobs.constructor == Employee); //显示:true
   23
   24 alert(BillGates.SayHello == SteveJobs.SayHello); //显示:false
这段代码表明,函数不但可以当作构造函数,而且还可以带参数,还可以为对象添加成员和方法。其中的第9行,Employee构造函数又将自己接收的 this作为参数调用Person构造函数,这就是相当于调用基类的构造函数。第21、22行还表明这样一个意思:BillGates是由Person构 造的,而SteveJobs是由Employee构造的。对象内置的constructor属性还指明了构造对象所用的具体函数!
  
   其实,如果你愿意把函数当作“类”的话,她就是“类”,因为她本来就有“类”的那些特征。难道不是吗?她生出的儿子各个都有相同的特征,而且构造函数也与类同名嘛!
  
   但要注意的是,用构造函数操作this对象创建出来的每一个对象,不但具有各自的成员数据,而且还具有各自的方法数据。换句话说,方法的代码体(体现函数 逻辑的数据)在每一个对象中都存在一个副本。尽管每一个代码副本的逻辑是相同的,但对象们确实是各自保存了一份代码体。上例中的最后一句说明了这一实事, 这也解释了JavaScript中的函数就是对象的概念。
  
   同一类的对象各自有一份方法代码显然是一种浪费。在传统的对象语言中,方法函数并不象JavaScript那样是个对象概念。即使也有象函数指针、方法指针或委托那样的变化形式,但其实质也是对同一份代码的引用。一般的对象语言很难遇到这种情况。
  
   不过,JavaScript语言有大的灵活性。我们可以先定义一份唯一的方法函数体,并在构造this对象时使用这唯一的函数对象作为其方法,就能共享方法逻辑。例如:
   function SayHello() //先定义一份SayHello函数代码
   {
   alert("Hello, I'm " + this.name);
   };
  
   function Person(name) //带参数的构造函数
   {
   this.name = name; //将参数值赋给给this对象的属性
   this.SayHello = SayHello; //给this对象SayHello方法赋值为前面那份SayHello代码。
   };
  
   var BillGates = new Person("Bill Gates"); //创建BillGates对象
   var SteveJobs = new Person("Steve Jobs"); //创建SteveJobs对象
  
   alert(BillGates.SayHello == SteveJobs.SayHello); //显示:true
   其中,最后一行的输出结果表明两个对象确实共享了一个函数对象。虽然,这段程序达到了共享了一份方法代码的目的,但却不怎么优雅。因为,定义 SayHello方法时反映不出其与Person类的关系。“优雅”这个词用来形容代码,也不知道是谁先提出来的。不过,这个词反映了程序员已经从追求代 码的正确、高效、可靠和易读等基础上,向着追求代码的美观感觉和艺术境界的层次发展,程序人生又多了些浪漫色彩。
  
   显然,JavaScript早想到了这一问题,她的设计者们为此提供了一个有趣的prototype概念。
  
   初看原型
  
   prototype源自法语,软件界的标准翻译为“原型”,代表事物的初始形态,也含有模型和样板的意义。JavaScript中的prototype概念恰如其分地反映了这个词的内含,我们不能将其理解为C++的prototype那种预先声明的概念。
  
   JavaScript的所有function类型的对象都有一个prototype属性。这个prototype属性本身又是一个object类型的对 象,因此我们也可以给这个prototype对象添加任意的属性和方法。既然prototype是对象的“原型”,那么由该函数构造出来的对象应该都会具 有这个“原型”的特性。事实上,在构造函数的prototype上定义的所有属性和方法,都是可以通过其构造的对象直接访问和调用的。也可以这么说, prototype提供了一群同类对象共享属性和方法的机制。
  
   我们先来看看下面的代码:
   function Person(name)
   {
   this.name = name; //设置对象属性,每个对象各自一份属性数据
   };
  
   Person.prototype.SayHello = function() //给Person函数的prototype添加SayHello方法。
   {
   alert("Hello, I'm " + this.name);
   }
  
   var BillGates = new Person("Bill Gates"); //创建BillGates对象
   var SteveJobs = new Person("Steve Jobs"); //创建SteveJobs对象
  
   BillGates.SayHello(); //通过BillGates对象直接调用到SayHello方法
   SteveJobs.SayHello(); //通过SteveJobs对象直接调用到SayHello方法
  
   alert(BillGates.SayHello == SteveJobs.SayHello); //因为两个对象是共享prototype的SayHello,所以显示:true
   程序运行的结果表明,构造函数的prototype上定义的方法确实可以通过对象直接调用到,而且代码是共享的。显然,把方法设置到prototype的 写法显得优雅多了,尽管调用形式没有变,但逻辑上却体现了方法与类的关系,相对前面的写法,更容易理解和组织代码。
  
   那么,对于多层次类型的构造函数情况又如何呢?

然而,比静态对象语言更神奇的是,我们可以随时给原型对象动态添加新的属性和方法,从而动态地扩展基类的功能特性。这在静态对象语言中是很难想象的。我们来看下面的代码:
以下是引用片段:
function Person(name)
   {
   this.name = name;
   };
  
   Person.prototype.SayHello = function() //建立对象前定义的方法
   {
   alert( "Hello, I'm " + this.name);
   };
  
   var BillGates = new Person( "Bill Gates"); //建立对象
  
   BillGates.SayHello();
  
   Person.prototype.Retire = function() //建立对象后再动态扩展原型的方法
   {
   alert( "Poor " + this.name + ", bye bye!");
   };
BillGates.Retire(); //动态扩展的方法即可被先前建立的对象立即调用
   阿弥佗佛,原型继承竟然可以玩出有这样的法术!
  
   原型扩展
  
   想必君的悟性极高,可能你会这样想:如果在JavaScript内置的那些如Object和Function等函数的prototype上添加些新的方法和属性,是不是就能扩展JavaScript的功能呢?
  
   那么,恭喜你,你得到了!
  
   在AJAX技术迅猛发展的今天,许多成功的AJAX项目的JavaScript运行库都大量扩展了内置函数的prototype功能。比如微软的 ASP.NET AJAX,就给这些内置函数及其prototype添加了大量的新特性,从而增强了JavaScript的功能。
  
   我们来看一段摘自MicrosoftAjax.debug.js中的代码:
  
   String.prototype.trim = function String$trim() {
   if (arguments.length !== 0) throw Error.parameterCount();
   return this.replace(/^\s+|\s+$/g, '');
   }
   这段代码就是给内置String函数的prototype扩展了一个trim方法,于是所有的String类对象都有了trim方法了。有了这个扩展,今 后要去除字符串两段的空白,就不用再分别处理了,因为任何字符串都有了这个扩展功能,只要调用即可,真的很方便。
  
   当然,几乎很少有人去给Object的prototype添加方法,因为那会影响到所有的对象,除非在你的架构中这种方法的确是所有对象都需要的。
  
   前两年,微软在设计AJAX类库的初期,用了一种被称为“闭包”(closure)的技术来模拟“类”。其大致模型如下:
以下是引用片段:
function Person(firstName, lastName, age)
   {
   //私有变量:
   var _firstName = firstName;
   var _lastName = lastName;
  
   //公共变量:
   this.age = age;
  
   //方法:
   this.getName = function()
   {
   return(firstName + " " + lastName);
   };
   this.SayHello = function()
   {
   alert( "Hello, I'm " + firstName + " " + lastName);
   };
   };
  
   var BillGates = new Person( "Bill", "Gates", 53);
   var SteveJobs = new Person( "Steve", "Jobs", 53);
  
   BillGates.SayHello();
   SteveJobs.SayHello();
   alert(BillGates.getName() + " " + BillGates.age);
   alert(BillGates.firstName); //这里不能访问到私有变量
很显然,这种模型的类描述特别象C#语言的描述形式,在一个构造函数里依次定义了私有成员、公共属性和可用的方法,显得非常优雅嘛。特别是“闭包”机制可以模拟对私有成员的保护机制,做得非常漂亮。
  
   所谓的“闭包”,就是在构造函数体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层外层函数体中的临时变量。这使得只要目标 对象在生存期内始终能保持其方法,就能间接保持原构造函数体当时用到的临时变量值。尽管最开始的构造函数调用已经结束,临时变量的名称也都消失了,但在目 标对象的方法内却始终能引用到该变量的值,而且该值只能通这种方法来访问。即使再次调用相同的构造函数,但只会生成新对象和方法,新的临时变量只是对应新 的值,和上次那次调用的是各自独立的。的确很巧妙!

   但是前面我们说过,给每一个对象设置一份方法是一种很大的浪费。还有,“闭包”这种间接保持变量值的机制,往往会给JavaSript的垃圾回收器制造难 题。特别是遇到对象间复杂的循环引用时,垃圾回收的判断逻辑非常复杂。无独有偶,IE浏览器早期版本确实存在JavaSript垃圾回收方面的内存泄漏问 题。再加上“闭包”模型在性能测试方面的表现不佳,微软最终放弃了“闭包”模型,而改用“原型”模型。正所谓“有得必有失”嘛。
  
   原型模型需要一个构造函数来定义对象的成员,而方法却依附在该构造函数的原型上。大致写法如下:
   //定义构造函数
   function Person(name)
   {
   this.name = name; //在构造函数中定义成员
   };
  
   //方法定义到构造函数的prototype上
   Person.prototype.SayHello = function()
   {
   alert("Hello, I'm " + this.name);
   };
  
   //子类构造函数
   function Employee(name, salary)
   {
   Person.call(this, name); //调用上层构造函数
   this.salary = salary; //扩展的成员
   };
  
   //子类构造函数首先需要用上层构造函数来建立prototype对象,实现继承的概念
   Employee.prototype = new Person() //只需要其prototype的方法,此对象的成员没有任何意义!
  
   //子类方法也定义到构造函数之上
   Employee.prototype.ShowMeTheMoney = function()
   {
   alert(this.name + " $" + this.salary);
   };
  
   var BillGates = new Person("Bill Gates");
   BillGates.SayHello();
  
   var SteveJobs = new Employee("Steve Jobs", 1234);
   SteveJobs.SayHello();
   SteveJobs.ShowMeTheMoney();
   原型类模型虽然不能模拟真正的私有变量,而且也要分两部分来定义类,显得不怎么“优雅”。不过,对象间的方法是共享的,不会遇到垃圾回收问题,而且性能优于“闭包”模型。正所谓“有失必有得”嘛。
  
   在原型模型中,为了实现类继承,必须首先将子类构造函数的prototype设置为一个父类的对象实例。创建这个父类对象实例的目的就是为了构成原型链, 以起到共享上层原型方法作用。但创建这个实例对象时,上层构造函数也会给它设置对象成员,这些对象成员对于继承来说是没有意义的。虽然,我们也没有给构造 函数传递参数,但确实创建了若干没有用的成员,尽管其值是undefined,这也是一种浪费啊。
  
   唉!世界上没有完美的事情啊!

那么,我们能否自己定义一个对象来当作原型,并在这个原型上描述类,然后将这个原型设置给新创建的对象,将其当作对象的类呢?我们又能否将这个原型中的一个方法当作构造函数,去初始化新建的对象呢?例如,我们定义这样一个原型对象:
  
   var Person = //定义一个对象来作为原型类
   {
   Create: function(name, age) //这个当构造函数
   {
   this.name = name;
   this.age = age;
   },
   SayHello: function() //定义方法
   {
   alert("Hello, I'm " + this.name);
   },
   HowOld: function() //定义方法
   {
   alert(this.name + " is " + this.age + " years old.");
   }
   };
   这个JSON形式的写法多么象一个C#的类啊!既有构造函数,又有各种方法。如果可以用某种形式来创建对象,并将对象的内置的原型设置为上面这个“类”对象,不就相当于创建该类的对象了吗?
  
   但遗憾的是,我们几乎不能访问到对象内置的原型属性!尽管有些浏览器可以访问到对象的内置原型,但这样做的话就只能限定了用户必须使用那种浏览器。这也几乎不可行。
  
   那么,我们可不可以通过一个函数对象来做媒介,利用该函数对象的prototype属性来中转这个原型,并用new操作符传递给新建的对象呢?
  
   其实,象这样的代码就可以实现这一目标:
  
   function anyfunc(){}; //定义一个函数躯壳
   anyfunc.prototype = Person; //将原型对象放到中转站prototype
   var BillGates = new anyfunc(); //新建对象的内置原型将是我们期望的原型对象
   不过,这个anyfunc函数只是一个躯壳,在使用过这个躯壳之后它就成了多余的东西了,而且这和直接使用构造函数来创建对象也没啥不同,有点不爽。
  
   可是,如果我们将这些代码写成一个通用函数,而那个函数躯壳也就成了函数内的函数,这个内部函数不就可以在外层函数退出作用域后自动消亡吗?而且,我们可以将原型对象作为通用函数的参数,让通用函数返回创建的对象。我们需要的就是下面这个形式:
  
   function New(aClass, aParams) //通用创建函数
   {
   function new_() //定义临时的中转函数壳
   {
   aClass.Create.apply(this, aParams); //调用原型中定义的的构造函数,中转构造逻辑及构造参数
   };
   new_.prototype = aClass; //准备中转原型对象
   return new new_(); //返回建立最终建立的对象
   };
  
   var Person = //定义的类
   {
   Create: function(name, age)
   {
   this.name = name;
   this.age = age;
   },
   SayHello: function()
   {
   alert("Hello, I'm " + this.name);
   },
   HowOld: function()
   {
   alert(this.name + " is " + this.age + " years old.");
   }
   };
  
   var BillGates = New(Person, ["Bill Gates", 53]); //调用通用函数创建对象,并以数组形式传递构造参数
   BillGates.SayHello();
   BillGates.HowOld();
  
   alert(BillGates.constructor == Object); //输出:true
   这里的通用函数New()就是一个“语法甘露”!这个语法甘露不但中转了原型对象,还中转了构造函数逻辑及构造参数。
  
   有趣的是,每次创建完对象退出New函数作用域时,临时的new_函数对象会被自动释放。由于new_的prototype属性被设置为新的原型对象,其 原来的原型对象和new_之间就已解开了引用链,临时函数及其原来的原型对象都会被正确回收了。上面代码的最后一句证明,新创建的对象的 constructor属性返回的是Object函数。其实新建的对象自己及其原型里没有constructor属性,那返回的只是最顶层原型对象的构造 函数,即Object


类别:Javascript | 添加到搜藏 | 浏览() | 评论 (0)
 
最近读者:
 
网友评论:
发表评论:
姓 名:
网址或邮箱: (选填)
内 容:
验证码: 请点击后输入四位验证码,字母不区分大小写
      

     

©2009 Baidu