贾红健
摘 要 在很多编程者的心目中,JavaScript作为一种函数式脚本语言长期行走在面向对象语言的边缘,对于它是否面向对象模棱两可,本文通过简单的示例,回归面向对象本意,从语法角度阐述JavaScript是一种彻底的面向对象语言以及如何应用这种特性。
【关键词】JavaScript 面向对象 封装 继承 多态
面向对象程序设计(OOP)是一种程序设计范型,同时也是一种程序开发方法。对象是指类的实例,它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。那么,JavaScript(以下简称JS)是否是面向对象语言?答案是:从语法角度来说,是。但是在实践中相当多的开发者并不严格遵从面向对象。面向对象三个要素:封装,继承,多态。通常JS开发中这三个要素并不会被完全遵守,请看以下例子。
//定义个Person类
function Person(id, name)
{
this.id = id; //身份证号
this.name = name;//姓名
}
//实例化一个Person对象
var user = new Person("321321xxxxxx", "Jack");
实现封装了吗?实现了,但是不很严格。尽管user变量包含了id, name两个成员,但这两个成员都可以被任意更改,比如代码:user.name = “Rose”,没有Java、c++中类似private的关键字来控制访问权限。继承呢?JS中没有显式关键字来表示继承,像Java中有extends、implements,C++中有”:”。至于多态,是基于继承的,没有继承哪来多态。所以看起来JS对面向对象的支持不好啊,那为什么还要说它是面向对象的语言呢?下面就从面向对象三个要素:封装,继承,多态逐条讲解JS对它们的支持。
1 封装
封装是说,不只是让你能用简化的视图来看复杂的概念,同时还不能让你看到复杂概念的任何细节,你能看得到的就是你能全部得到的“代码大全”。将一组变量放到一个对象中并不是完全的封装,所以前文所说示例中的封装不严格,因为不想暴露的成员变量还是暴露了。一般OO语言中成员变量、函数都至少有三个访问级别:public所有对象可见;protected自身、子类可见;private自身可见。JS无法支持到如此详细,仅仅支持public、private。以下示例是在JS类中定义private变量,public方法。
function Person(id, name)
{
varmId; //身份证号
varmName; //姓名
mId = id;
mName = name;
//私有函数,通过身份照Id来取得生日
functiongetBirthday(){}
//读取姓名
this.getName = function(){returnmName;}
//修改姓名,人是可以改名字的
this.setName = function(name){mName = name;}
//id没有set方法,身份证号码是不能改的
this.getId = function(){returnmId;}
this.print = function(){console.log("name:" + mName + " id: " + id);}
}
var user = new Person("P1", "Jack");
private成員变量使用var关键字声明放在Person函数内部,可以防止对象外部的函数直接访问,而成员函数可以访问,从而实现了private成员变量。private成员函数getBirthday也只有成员函数才能访问,类外面是访问不了的。这个实现方法的原理是使用闭包,篇幅原因不对闭包进行展开讲解。实现了private就是完成了封装了吗?没有。
当实例化一个Person对象之后,外部尽管访问不了private变量,但是却可以恶意或不小心扩展、篡改这个对象,进而导致软件缺陷。
比如这样的代码,getName函数将无法返回正确的结果:
var user = new Person("P1", "Jack");
user.getName = function() {return "foo";}
尽管这种情况比较少,但是当软件变得复杂,人员规模变大后,很可能出问题,这是墨菲定律所决定的(墨菲定律:如果有两种或两种以上的方式去做某件事情,而其中一种选择方式将导致灾难,则必定有人会做出这种选择。)。
为了解决这个问题,则要使用函数Object.freeze
var user = new Person("P1", "Jack");
Object.freeze(user); //冻结对象
user.getName = function() {return "foo";}
这个函数调用之后,后面的修改user的代码将不起作用。不过很可惜,这个函数在IE8,或者更低的IE版本下不支持
2 继承
继承是OO设计中支持复用的基石,可以很方便的复用、扩展已有功能。JS中没有显式支持继承的关键字,但可把子类的prototye定义为父类的实例来实现。接上面的Person例子,定义一个子类Programmer。
function Programmer(id, name, skill)
{ //id, name 的意义和Person一样
Person.call(this, id, name); //调用父类构造函数
varmSkill = skill; //数组,表示技能
this.getSkill = function() {returnmSkill;}
this.addSkill = function(s) {mSkill.push(s);}
this.useSkill = function(){console.log(mSkill);}
}
//将子类的prototype指向父类的实例,否则instanceof操作将出错
Programmer.prototype = newPerson();
//设置constructor,否则子类的constructor将是父类的构造函数
Programmer.prototype.constructor = Programmer;
var nerd = newProgrammer("321321aaaa", "Linus",[ "c++", "JS" ]);
这样的操作就实现了继承。但由于无法实现protected权限,导致子类无法访问父类的private变量。对于父类成员的访问,子类和其他的类并没有更多的权限。所以将父类的成员设置为public还是private,要视情况决定了。
3 多态
面向对象中多态即意味着子类的某一功能可以有区别于父类的实现,并且同一父类的不同子类的实现也可以不一样。多態在JS中实现很简单,直接在子类中用同名函数重写父类函数即可。如下所示,在Programmer类重写print函数,将skill也打印出来:
this.print = function(){console.log("name: " + this.getName() + " id: " + this.getId() + " skill: " + JSON.stringify(this.getSkill()));}
4 其他元素
面向对象中还有一些其他元素,如重载、静态变量、多继承/接口继承以及弱类型语言中的鸭式辩型,此处仅做简要介绍。
重载可在JS函数内部判断参数个数、类型来执行不同功能,以此实现重载,代码如下:
functionfoo(v)
{
if(typeof(v) == "number"){console.log(v + " is a number");}
else if (typeof(v) == "string"){console.log(v + " is a string");}
else if (typeof(v) == "boolean"){console.log(v + " is a boolean");}
}
静态变量可通过在类的prototype上面定义变量来实现,代码如下:
Person.prototype.staticVar= "test";
多继承/接口继承是指子类有多个父类,兼有多个父类的功能。JS中没有很好的办法来实现,但由于JS是弱类型语言,只要在一个对象中添加某一个类型的方法就可以冒充该类型,即鸭式辩型。
5 小结
以上讨论了面向对象三要素在JS中的实现。我们讨论JS面向对象的目的并非鼓励大家编写OO的JS代码,而是当你认真考虑发现OO更加适合当下的需求之后,用本文提供的方法可以写出更健壮的JS代码。
作者单位
中国邮政集团公司南京分公司 江苏省南京市 210029