面向对象的概念
JavaScript并不是面向对象的程序设计语言,面向对象设计的基本特征:继承、多态等没有得到很好的实现。在纯粹的面向对象语言里,最基本的程序单位是类,类与类之间提供严格的继承关系。比如Java中的类,所有的类都可以通过extends显式继承父类,或者默认继承系统的Object类。而JavaScript并没有提供规范的语法让开发者定义类。JavaScript中的每个函数都可用于创建对象,返回的对象既是该类的实例,也是Object类的实例。
由于JavaScript的函数定义不支持继承语法,因此JavaScript没有完善的继承机制。所以我们习惯上称JavaScript是基于对象的脚本语言。
JavaScript不允许开发者指定类与类之间的继承关系,JavaScript并没有提供完善的继承语法,因此开发者定义的类没有父子关系,但这些类都是Object类的子类。
例子
<script type="text/javascript">
// 定义简单函数
function Person(name)
{
this.name = name;
}
// 使用new关键字,简单创建Person类的实例
var p = new Person('yeeku');
// 如果p是Person实例,则输出静态文本
if (p instanceof Person)
document.writeln("p是Person的实例<br />");
// 如果p是Object 实例,则输出静态文本
if(p instanceof Object)
document.writeln("p是Object的实例");
</script>
对象和关联数组
JavaScript对象与纯粹的面向对象语言的对象存在一定的区别:JavaScript中的对象本质上是一个关联数组,或者说更像Java里的Map数据结构,由一组key-value对组成。与Java中Map对象存在区别的是,JavaScript对象的value,不仅可以是值(包括基本类型的值和复合类型的值),也可以是函数,此时的函数就是该对象的方法。当value是基本类型的值或者复合类型的值时,此时的value就是该对象的属性值。
因此,当需要访问某个JavaScript对象的属性时,不仅可以使用obj.propName的形式,也可以采用obj[propName]的形式,有些时候甚至必须使用这种形式。
例子
<script type="text/javascript">
function Person(name , age)
{
// 将name、age形参的值分别赋给name、age实例属性。
this.name = name;
this.age = age;
this.info = function()
{
alert('info method!');
}
}
var p = new Person('yeeku' , 30);
for (propName in p)
{
// 遍历Person对象的属性
document.writeln('p对象的' + propName
+ "属性值为:" + p[propName] + "<br />");
}
</script>
上面程序中粗体字代码遍历了Person对象的每个属性,因为遍历每个属性时循环计数器是Person对象的属性名,因此程序必须根据属性名来访问Person对象的属性,此时不能采用p.propName的形式,如果采用p.propName的形式,JavaScript不会把propName当成变量处理,它试图直接访问该对象的propName属性——但该属性实际并不存在。
继承和prototype
在面向对象的程序设计语言里,类与类之间有显式的继承关系,一个类可以显式地指定继承自哪个类,子类将具有父类的所有属性和方法。JavaScript虽然也支持类、对象的概念,但没有继承的概念,只能通过一种特殊的手段来扩展原有的JavaScript类。
事实上,每个JavaScript对象都是相同基类(Object类)的实例,因此所有的JavaScript对象之间并没有明显的继承关系。而且JavaScript是一种动态语言,它允许自由地为对象增加属性和方法,当程序为对象的某个不存在的属性赋值时,即可认为是为该对象增加属性。
然而,动态增加方法又存在性能低下和方法内的局部变量产生闭包等问题,通常不建议直接在函数定义(也就是类定义)中直接为该函数定义方法,而是建议使用prototype属性。
JavaScript的所有类(也就是函数)都有一个prototype属性,当我们为JavaScript类的prototype属性增加函数、属性时,则可视为对原有类的扩展。我们可理解为:增加了prototype属性的类继承了原有的类——这就是JavaScript所提供的伪继承机制。
例子
<script type="text/javascript">
// 定义一个Person函数,同时也定义了Person类
function Person(name , age)
{
// 将局部变量name、age赋值给实例属性name、age
this.name = name;
this.age = age;
// 使用内嵌的函数定义了Person类的方法
this.info = function()
{
document.writeln("姓名:" + this.name + "<br />");
document.writeln("年龄:" + this.age + "<br />");
}
}
// 创建Person的实例p1
var p1 = new Person('yeeku' , 29);
// 执行Person的info方法
p1.info();
// 此处不可调用walk方法,变量p还没有walk方法
// 将walk方法增加到Person的prototype属性上
Person.prototype.walk = function()
{
document.writeln(this.name + '正在慢慢溜达...<br />');
}
document.writeln('<hr />');
// 创建Person的实例p2
var p2 = new Person('leegang' , 30);
// 执行p2的info方法
p2.info();
document.writeln('<hr />');
// 执行p2的walk方法
p2.walk();
// 此时p1也具有了walk方法——JavaScript允许为类动态增加方法和属性
// 执行p1的walk方法
p1.walk();
</script>
上面程序采用prototype为Person类增加了一个walk方法,这样会让所有的Person实例共享一个walk方法,而且该walk方法不在Person函数之内,因此不会产生闭包。
注意
JavaScript并没有提供真正的继承,当通过某个类的prototype属性动态地增加属性或方法时,其实质是对原有类的修改,并不是真正产生一个新的子类,所以这种机制依然只是一种伪继承。
虽然可以在任何时候为一个类增加属性和方法,但通常建议在类定义结束后立即增加该类所需的方法,这样可避免造成不必要的混乱。同时,对于需要在类定义中定义方法的情形,尽量避免直接在类定义中定义方法,这样可能造成内存泄漏和产生闭包。比较安全的方式是通过prototype属性为该类增加属性和方法。
尽量避免使用内嵌函数为类定义方法,而应该使用增加prototype属性的方式来增加方法。通过prototype属性来为类动态地增加属性和方法会让程序更加安全,性能更加稳定。
注:本博客内容节选自李刚编著的疯狂HTML 5/CSS 3/JavaScript讲义 ,详细内容请参阅书籍。