在JavaScript中,类的实现是基于其原型继承机制的,如果两个实例都从同一个原型对象上继承了属性,我们说它们是同一个类的实例。如果熟悉C++、Java这种强类型面向对象编程语言,比如类的封装、继承、多态特性,你会发现JavaScript中的类与它们有很大不同,一个重要的特性就是动态可继承性,往往弱化对象的类型,强化对象的功能。定义类是模块开发和代码重用的的有效方式之一,下面对JavaScript中的类及面向对象编程作个介绍。
在JavaScript中,类的所有实例对象都从同一个原型对象上继承属性,原型对象是类的核心。如下例子中的inherit()函数,返回一个新创建的对象,后者继承自某个原型对象。
function inherit(p) {
if (p == null) {
throw TypeError();
}
if (Object.create) {
return Object.create(p);
}
var t = typeof p;
if (t !== "object" && t !== "function") {
throw TypeError();
}
function f() {};
f.prototype = p;
return new f();
}
如果定义一个原型对象,如下例子中的range.methods,然后通过inherit()函数创建一个继承自它的对象,如下例子中的r对象,这样就定义了一个JavaScript类,通常类的实例还需要进一步的初始化,一般是通过定义一个函数来创建并初始化这个对象,如下例子中的range()工厂函数,实现一个能表示值的范围的类。
function range(from, to) {
var r = inherit(range.methods);
r.from = from;
r.to = to;
return r;
}
range.methods = {
includes: function(x) {
return this.from <= x && x <= this.to;
},
foreach: function(f) {
for (var x = Math.ceil(this.from); x <= this.to; ++x) {
f(x);
}
},
toString: function() {
return "(" + this.from + "..." + this.to + ")";
}
};
var r = range(1, 3); // 创建一个范围对象
r.inlcudes(2); // true
r.foreach(console.log); // 1 2 3
console.log(r); // (1...3)
上面例子中的自定义类是通过工厂函数实现的,但是一般情况下,类要定义自己的构造函数,然后通过关键字new来创建新的对象,改写上面的例子如下:
function Range(from, to) {
this.from = from;
this.to = to;
}
Range.prototype = {
constructor: Range,
includes: function(x) {
return this.from <= x && x <= this.to;
},
foreach: function(f) {
for (var x = Math.ceil(this.from); x <= this.to; ++x) {
f(x);
}
},
toString: function() {
return "(" + this.from + "..." + this.to + ")";
}
};
var rg = new Range(1, 3);
JavaScript中,定义构造函数即是定义类,类名首字母要大写,而普通的函数和方法都是首字母小写,其原型是prototype,这是一个强制命名规则,对构造函数的调用会自动使用这个prototype作为新对象的原型。prototype属性的值是一个对象,这个对象包含唯一一个不可枚举属性constructor属性,constructor属性的值是一个函数对象即这个类。当自定义prototype时,这个新定义的原型对象不包含有constructor属性,可以通过如上例子中的构造函数反向引用来实现。
instanceof运算符和isPropertyOf()方法,可以用来检测对象是否属于指定的类名,如果对象包含constructor属性的话,也可以用这个属性来判断对象的类型,另外,使用构造函数的名字也可以作为类标识符。上面几个检测对象的类的各种技术多少都会有些问题,至少在客户端JavaScript中是如此。解决办法就是规避掉这些问题,不要关注对象的类是什么,而是关注对象能做什么,这就是“鸭式辩型”思路。
JavaScript中的类设计三种不同的对象,它们是构造函数、原型和实例。定义类,可以分三步走,从上面的例子中也看出来了,第一步先定义一个构造函数,并设置初始化新对象的实例属性,第二步給构造函数的prototype对象定义实例的方法,第三步給构造函数定义类字段和类属性。
JavaScript中基于原型的继承机制是动态的,对象从其原型继承属性,如果创建对象之后原型的属性发生改变,也会影响到继承这个原型的所有实例对象,这意味着我们可以通过给原型对象添加新方法来扩充JavaScript类。
类中,我们常常会定制一些方法,如类型转换方法,比较方法,有时还会涉及到方法借用。
在经典的面向对象编程中,经常需要将对象的某个状态封装或隐藏在对象内,只有通过对象的方法才能访问这些状态,对外值暴露一些重要的状态变量可以直接读写,为了实现这个目的,类似Java/C++的编程语言允许声明类的private实例字段,这些私有实例字段只能被类的实例去访问,且在类的外部是不可见的,我们可以通过将变量或参数闭包在一个构造函数内来模拟实现私有实例字段,调用构造函数会创建一个实例,为了做到这一点,需要在构造函数内部定义一个函数,并将这个函数赋值给新创建对象的属性。但需要注意的是,这种封装技术造成了更多系统开销,使用闭包来封装类的状态的类一定会比不使用封装类的状态变量的等价类运行速度更慢,并占用更多内存。
有时候,我们希望对象的初始化有多种方式,有一个方法可以实现,通过重载这个构造函数让它根据传入参数arguments的不同来执行不同的初始化语法。
JavaScript中继承,子类的prototype要继承自父类的prototype,子类可以作一些有别与父类的特殊处理,如重写父类方法,作一些扩展等,实现这种需求,还可以使用另一种技术:组合。组合是编程中广为人知的一种设计原则,有时候实现起来优于继承。
JavaScript中也可以像C++/Java中那样定义抽象类,提供一些通用的接口,方法是在构造函数中不作任何事情,只是抛出一个异常,说明这个类是抽象类,不能实例化。
将代码组织到类中的一个重要原因是让代码更加模块化,可以在不同的场景中实现代码的重用,但类不是唯一的模块化代码的方式。一般来讲,模块是一个独立的JavaScript文件。模块化的目标是支持大规模的程序开发,不同的模块必须避免修改全局执行上下文,应当尽可能少地定义全局标识。
在模块创建过程中,避免污染全局变量的一种方法是使用一个对象作为命名空间,它将函数和值作为命名空间对象属性存储起来,而不是全局函数和变量。按照约定,模块的文件名应当和命名空间匹配。
var animals = {};
如上,这个animals对象是模块的命名空间,所有的属性都可以添加到animals对象中,也可以通过animals对象访问这些属性,相应地文件名为animals.js。
模块对外导出一些公用API,但模块的实现往往需要一些辅助性API,不需要在模块外部可见,这时就可以使用模块函数,即将函数作用域作为模块的私有命名空间,因为在一个函数中定义的变量和函数都属于函数的局部成员,在函数的外部是不可见的,这样在模块外部也就不可见了。如果想让代码在一个私有命名空间中运行,只要把代码放到如下格式的花括号中就可以了,是一个立即执行的匿名函数:
(function(){...}())