JavaScript内核系列 第8章 面向对象的JavaScript(上)
原创作者: abruzzi
第八章 面向对象的Javascript
面向对象编程思想在提出之后,很快就流行起来了,它将开发人员从冗长,繁复,难以调试的过程式程序中解放了出来,过程式语言如C,代码的形式往往如此:
C代码
- Component comp;
- init_component(& comp, props);
Component comp;init_component(& comp, props);
而面向对象的语言如Java,则会是这种形式:
Java代码
- Component comp;
- comp.init(props);
Component comp;comp.init(props);
可以看出,方法是对象的方法,对象是方法的对象,这样的代码形式更接近人的思维方式,因此OO大行其道也并非侥幸。
JavaScript本身是基于对象的,而并非基于类。但是,JavaScript的函数式语言的特性使得它本身是可编程的,它可以变成你想要的任何形式。我们在这一章详细讨论如何使用JavaScript进行OO风格的代码开发。
8.1 原型继承
JavaScript中的继承可以通过原型链来实现,调用对象上的一个方法,由于方法在JavaScript对象中是对另一个函数对象的引用,因此解释器会在对象中查找该属性,如果没有找到,则在其内部对象prototype对象上搜索,由于prototype对象与对象本身的结构是一样的,因此这个过程会一直回溯到发现该属性,则调用该属性,否则,报告一个错误。关于原型继承,我们不妨看一个小例子:
Js代码
- function Base(){
- this .baseFunc = function (){
- print ( "base behavior" );
- }
- }
- function Middle(){
- this .middleFunc = function (){
- print ( "middle behavior" );
- }
- }
- Middle. prototype = new Base();
- function Final(){
- this .finalFunc = function (){
- print ( "final behavior" );
- }
- }
- Final. prototype = new Middle();
- function test(){
- var obj = new Final();
- obj.baseFunc();
- obj.middleFunc();
- obj.finalFunc();
- }
function Base(){ this .baseFunc = function (){ print ( "base behavior" ); }} function Middle(){ this .middleFunc = function (){ print ( "middle behavior" ); }} Middle. prototype = new Base(); function Final(){ this .finalFunc = function (){ print ( "final behavior" ); }}Final. prototype = new Middle(); function test(){ var obj = new Final(); obj.baseFunc(); obj.middleFunc(); obj.finalFunc();}
|
图 原型链的示意图
在function test中,我们new了一个Final对象,然后依次调用obj.baseFunc,由于obj对象上并无此方法,则按照上边提到的规则,进行回溯,在其原型链上搜索,由于Final的原型链上包含Middle,而Middle上又包含Base,因此会执行这个方法,这样就实现了类的继承。
base behavior
middle behavior
final behavior
但是这种继承形式与传统的OO语言大相径庭,初学者很难适应,我们后边的章节会涉及到一个比较好的JavaScript的面向对象基础包Base,使用Base包,虽然编码风格上会和传统的OO语言不同,但是读者很快就会发现这种风格的好处。
8.1.1引用
引用是一个比较有意思的主题,跟其他的语言不同的是,JavaScript中的引用始终指向最终的对象,而并非引用本身,我们来看一个例子:
Js代码
- var obj = {}; // 空对象
- var ref = obj; // 引用
- obj. name = "objectA" ;
- print ( ref . name ); //ref 跟着添加了 name 属性
- obj = [ "one" , "two" , "three" ]; //obj 指向了另一个对象 ( 数组对象 )
- print ( ref . name ); //ref 还指向原来的对象
- print (obj. length ); //3
- print ( ref . length ); //undefined
var obj = {}; // 空对象var ref = obj; // 引用 obj. name = "objectA" ;print ( ref . name ); //ref 跟着添加了 name 属性 obj = [ "one" , "two" , "three" ]; //obj 指向了另一个对象 ( 数组对象 )print ( ref . name ); //ref 还指向原来的对象print (obj. length ); //3print ( ref . length ); //undefined
运行结果如下:
objectA
objectA
3
undefined
obj只是对一个匿名对象的引用,所以,ref并非指向它,当obj指向另一个数组对象时
可以看到,引用ref并未改变,而始终指向这那个后来添加了name属性的"空"对象”{}”。理解这一点对后边的内容有很大的帮助。
再看这个例子:
Js代码
- var obj = {}; // 新建一个对象,并被 obj 引用
- var ref1 = obj; //ref1 引用 obj, 事实上是引用 obj 引用的空对象
- var ref2 = obj;
- obj.func = "function" ;
- print (ref1.func);
- print (ref2.func);
var obj = {}; // 新建一个对象,并被 obj 引用 var ref1 = obj; //ref1 引用 obj, 事实上是引用 obj 引用的空对象var ref2 = obj; obj.func = "function" ; print (ref1.func);print (ref2.func);
声明一个对象,然后用两个引用来引用这个对象,然后修改原始的对象,注意这两步的顺序,运行之:
function
function
根据运行结果我们可以看出,在定义了引用之后,修改原始的那个对象会影响到其引用上,这一点也应该注意。
8.1.2 new操作符
有面向对象编程的基础有时会成为一种负担,比如看到new的时候,Java程序员可能会认为这将会调用一个类的构造器构造一个新的对象出来,我们来看一个例子:
Js代码
- function Shape(type){
- this .type = type || "rect" ;
- this .calc = function (){
- return "calc, " + this .type;
- }
- }
- var triangle = new Shape( "triangle" );
- print (triangle.calc());
- var circle = new Shape( "circle" );
- print (circle.calc());
function Shape(type){ this .type = type || "rect" ; this .calc = function (){ return "calc, " + this .type; }} var triangle = new Shape( "triangle" );print (triangle.calc()); var circle = new Shape( "circle" );print (circle.calc());
运行结果如下:
calc, triangle
calc, circle
Java程序员可能会觉得Shape就是一个类,然后triangle,circle即是Shape对应的具体对象,而其实JavaScript并非如此工作的,罪魁祸首即为此new操作符。在JavaScript中,通过new操作符来作用与一个函数,实质上会发生这样的动作:
首先,创建一个空对象,然后用函数的apply方法,将这个空对象传入作为apply的第一个参数,及上下文参数。这样函数内部的this将会被这个空的对象所替代:
Js代码
- var triangle = new Shape( "triangle" );
- // 上一句相当于下面的代码
- var triangle = {};
- Shape.apply(triangle, [ "triangle" ]);
var triangle = new Shape( "triangle" );// 上一句相当于下面的代码var triangle = {};Shape.apply(triangle, [ "triangle" ]);
8.2封装
事实上,我们可以通过JavaScript的函数实现封装,封装的好处在于未经授权的客户代码无法访问到我们不公开的数据,我们来看这个例子:
Js代码
- function Person(name){
- //private variable
- var address = "The Earth" ;
- //public method
- this .getAddress = function (){
- return address;
- }
- //public variable
- this .name = name;
- }
- //public
- Person.prototype.getName = function (){
- return this .name;
- }
- //public
- Person.prototype.setName = function (name){
- this .name = name;
- }
function Person(name){ //private variable var address = "The Earth" ; //public method this .getAddress = function (){ return address; } //public variable this .name = name;} //publicPerson.prototype.getName = function (){ return this .name;} //publicPerson.prototype.setName = function (name){ this .name = name;}
首先声明一个函数,作为模板,用面向对象的术语来讲,就是一个类。用var方式声明的变量仅在类内部可见,所以address为一个私有成员,访问address的唯一方法是通过我们向外暴露的getAddress方法,而get/setName,均为原型链上的方法,因此为公开的。我们可以做个测试:
Js代码
- var jack = new Person( "jack" );
- print(jack.name);//jack
- print(jack.getName());//jack
- print(jack.address);//undefined
- print(jack.getAddress());//The Earth
var jack = new Person( "jack" );print(jack.name);//jackprint(jack.getName());//jackprint(jack.address);//undefinedprint(jack.getAddress());//The Earth
直接通过jack.address来访问address变量会得到undefined。我们只能通过jack.getAddress来访问。这样,address这个成员就被封装起来了。
另外需要注意的一点是,我们可以为类添加静态成员,这个过程也很简单,只需要为函数对象添加一个属性即可。比如:
Js代码
- function Person(name){
- //private variable
- var address = "The Earth" ;
- //public method
- this .getAddress = function (){
- return address;
- }
- //public variable
- this .name = name;
- }
- Person.TAG = "javascript-core" ;// 静态变量
- print(Person.TAG);
function Person(name){ //private variable var address = "The Earth" ; //public method this .getAddress = function (){ return address; } //public variable this .name = name;} Person.TAG = "javascript-core" ;// 静态变量 print(Person.TAG);
也就是说,我们在访问Person.TAG时,不需要实例化Person类。这与传统的面向对象语言如Java中的静态变量是一致的。
8.3 工具包Base
Base是由Dean Edwards开发的一个JavaScript的面向对象的基础包,Base本身很小,只有140行,但是这个很小的包对面向对象编程风格有很好的支持,支持类的定义,封装,继承,子类调用父类的方法等,代码的质量也很高,而且很多项目都在使用Base作为底层的支持。尽管如此,JavaScript的面向对象风格依然非常古怪,并不可以完全和传统的OO语言对等起来。
下面我们来看几个基于Base的例子,假设我们现在在开发一个任务系统,我们需要抽象出一个类来表示任务,对应的,每个任务都可能会有一个监听器,当任务执行之后,需要通知监听器。我们首先定义一个事件监听器的类,然后定义一个任务类:
Js代码
- var EventListener = Base.extend({
- constructor : function(sense){
- this.sense = sense;
- },
- sense : null,
- handle : function(){
- print(this.sense+" occured");
- }
- });
- var Task = Base.extend({
- constructor : function(name){
- this.name = name;
- },
- name : null,
- listener : null,
- execute : function(){
- print(this.name);
- this.listener.handle();
- },
- setListener : function(listener){
- this.listener = listener;
- }
- });
var EventListener = Base.extend({ constructor : function(sense){ this.sense = sense; }, sense : null, handle : function(){ print(this.sense+" occured"); }}); var Task = Base.extend({ constructor : function(name){ this.name = name; }, name : null, listener : null, execute : function(){ print(this.name); this.listener.handle(); }, setListener : function(listener){ this.listener = listener; }});
创建类的方式很简单,需要给Base.extend方法传入一个JSON对象,其中可以有成员和方法。方法访问自身的成员时需要加this关键字。而每一个类都会有一个constructor的方法,即构造方法。比如事件监听器类(EventListener)的构造器需要传入一个字符串,而任务类(Task)也需要传入任务的名字来进行构造。好了,既然我们已经有了任务类和事件监听器类,我们来实例化它们:
Js代码
- var printing = new Task("printing");
- var printEventListener = new EventListener("printing");
- printing.setListener(printEventListener);
- printing.execute();
var printing = new Task("printing");var printEventListener = new EventListener("printing");printing.setListener(printEventListener);printing.execute();
首先,创建一个新的Task,做打印工作,然后新建一个事件监听器,并将它注册在新建的任务上,这样,当打印发生时,会通知监听器,监听器会做出相应的判断:
printing
printing occurred
既然有了基本的框架,我们就来使用这个框架,假设我们要从HTTP服务器上下载一个页面,于是我们设计了一个新的任务类型,叫做HttpRequester:
Js代码
- var HttpRequester = Task.extend({
- constructor : function(name, host, port){
- this.base(name);
- this.host = host;
- this.port = port;
- },
- host : "127.0.0.1",
- port : 9527,
- execute : function(){
- print("["+this.name+"] request send to "+this.host+" of port "+this.port);
- this.listener.handle();
- }
- });
var HttpRequester = Task.extend({ constructor : function(name, host, port){ this.base(name); this.host = host; this.port = port; }, host : "127.0.0.1", port : 9527, execute : function(){ print("["+this.name+"] request send to "+this.host+" of port "+this.port); this.listener.handle(); }});
HttpRequester类继承了Task,并且重载了Task类的execute方法,setListener方法的内容与父类一致,因此不需要重载。
Js代码
- var requester = new HttpRequester("requester1", "127.0.0.1", 8752);
- var listener = new EventListener("http_request");
- requester.setListener(listener);
- requester.execute();
var requester = new HttpRequester("requester1", "127.0.0.1", 8752);var listener = new EventListener("http_request");requester.setListener(listener);requester.execute();
我们新建一个HttpRequester任务,然后注册上事件监听器,并执行之:
[requester1] request send to 127.0.0.1 of port 8752
http_request occured
应该注意到HttpRequester类的构造器中,有这样一个语句:
Js代码
- this.base(name);
this.base(name);
表示执行父类的构造器,即将name赋值给父类的成员变量name,这样在HttpRequester的实例中,我们就可以通过this.name来访问这个成员了。这套机制简直与在其他传统的OO语言并无二致。同时,HttpRequester类的execute方法覆盖了父类的execute方法,用面向对象的术语来讲,叫做重载。
在很多应用中,有些对象不会每次都创建新的实例,而是使用一个固有的实例,比如提供数据源的服务,报表渲染引擎,事件分发器等,每次都实例化一个会有很大的开销,因此人们设计出了单例模式,整个应用的生命周期中,始终只有顶多一个实例存在。Base同样可以模拟出这样的能力:
Js代码
- var ReportEngine = Base.extend({
- constructor : null,
- run : function(){
- //render the report
- }
- });
var ReportEngine = Base.extend({ constructor : null, run : function(){ //render the report } });
很简单,只需要将构造函数的值赋为null即可。好了,关于Base的基本用法我们已经熟悉了,来看看用Base还能做点什么: