Javascript OO 设计正解(个人意见)
作者: FengWeiGuo / forxm@21cn.com
参考ECMA-262 3rd/ms-jscript/nv-javascript, 适用于所有符合ECMA-262的script语言
1. 对象语言
js是对象语言,所有元素都以对象方式存在.
注:不存在“类”概念。或者说其“类”概念被人为地存贮在某些特定的对象(Function/prototype)中。
2. 创建对象
对象的创建依赖于某个指定的Function对象,每个Function对象都被称为构造器constructor.
注:即便如此,那种认为Function是第一性的观点是不可取的,容易引起混乱.
3. prototype
每个Function/constructor对象都有唯一对应的prototype对象,这个prototype对象对所有新建对象是共享的. 每个Function/constructor对象的prototype对象从来不会被拷贝.而只会被建立参照/指针/reference.
4. 创建对象(2)
创建对象/new时,
a) 先创建一个空对象
b) 如果Function/constructor有prototype对象,为新对象建立一个隐性的prototype参照.
c) 调用Function/constructor,空对象作为this指针.
d) 如调用返回/return一个Object类型的对象,则新建对象为这个Object类型的对象,否则新建对象为a)中建立的对象.
注:ms的文档中没有提到b)和d), 但经过代码简单验证, 确实也是这样实现的.
注:隐性prototype参照是指直接使用obj.prototype是无效,但系统可以内部使用.例如: obj.constructor===obj.[prototype.]constructor.
问题: 如果prototype对象是唯一的,共享的,那么多个对象对同一prototype对象的操作不会发生冲突吗?否,见下5,6.
5. 对象取属性值(GET)
如果本对象有此属性,取此值;如没有,从唯一的prototype对象取;如还没有,从唯一的prototype对象的唯一的prototype对象中取;如还没有,...;这被称为prototype链.
注:取值不修改任何对象.
6. 对象设属性值(PUT)
如果本对象有此属性,设值;如没有,在本对象直接新建此属性.
注:设值不涉及prototype对象.
注:+=,++等操作在ECMA中都被分解为GET/PUT组合.例如,假定一个对象obj自已没有属性a,但它的prototype有,那么obj.a++导致obj建立了一个自己的属性a,其值等于prototype.a+1.
7. 显式的constructor属性
只有prototype对象具有显式的constructor属性,其它对象都没有.
注:显式属性指对象自身确实有这个属性,而不用向prototype链查询.
注:ECMA中,构造器的prototype对象的constructor属性一般都设为自身, 例如:
Object.prototype.constructor=Object
Function.prototype.constructor=Function
因此,一般情况下有: obj.constructor===obj.[prototype.]constructor===obj.constructor.prototype.constructor
注:ECMA中对显式constructor属性的操作只出现过2次,都是设置.第2次是设置error对象的,不讨论; 第1次是设置:
新建Function对象.prototype.constructor=新建Function对象自身
注:ECMA中没有对显式constructor属性的读取操作,也即是说,显式constructor属性是为方便应用层取得对象构造器而设置的一个属性,系统内部不使用,应用层可自行管理这个属性.
8. 在ECMA-262 3rd中,作者认为有用的一些其它信息
a) 在HTML中, global.window===global
b) ECMA-262 3rd实现了try/catch
c) 布尔值转换
Undefined : false
Null : false
Boolean : value
Number : ( +0, -0, NaN ) false, other true
String : "" false, other true
Object : true
(没有对其它类型定义,应该都作为Object为true)
d) 第4.2.1的图对理解ECMAScript的对象模型和prototype模式很有帮助.
9. Javascript OOP 的困难
如上所述,ECMAScript/Javascript实际是按照一种被称为原型模式(prototype pattern)的方法进行设计的,这种方式适用于对象继承,但是对于类(对象的抽象)继承的实现,则不一定适用,或者说在效率/实现等方面不理想.作者认为,这就是javascript OOP的困难所在.
10. Javascript OOP 的几种策略
第1种: 完全抛弃prototype模式,自行定义一种类定义方法.
第2种: 完全采用prototype模式.
第3种: 1,2的混合.
注: 各种策略各有其优缺点,不过,既然script类语言倾向于prototype模式,必然有其合理性.作者在此不深究,以下讨论尽量限于第2种策略.
注: 如果采用抛弃prototype模式策略,上面4.d是一个值得注意的特征,可惜ms没把这点写进文档,不太可靠.
注: 从这里开始,作者提出了一些在使用ECMAScript/Javascript类语言进行OOP设计时的观点和建议,纯属个人意见.
注: 以下建议基于prototype模式,也就是在尽量不对已有属性进行指针拷贝的前提下.
11. Function是对象构造器,而类信息(指直接传给实例的属性)保存在构造器的prototype里.
12. 构造器与prototype在对类的定义上处同一级别,相互可以访问,前提是对prototype.constructor进行合理的设置.
13. 如果将类信息存贮在构造器,那么当实现类继承时,有可能出现类信息分别贮存在构造器和prototype的情况.另外,从实例化的对象访问类信息时必须经过prototype.constructor,从前面7可以知,这是不安全的,使用上也不方便. 因此, 不将类信息存放在构造器是明智的.
14. 如果13没问题,那么把prototype称为"类",大致没问题了.
15. 构造器在new操作时把prototype指针赋予了新对象,因此如果要做到类实例对象一致,必须在类的第一次实例化前将prototype对象预备好(prototype的属性以后再加也可以).否则,如果在第一次构造器/function调用时才做 prototype= new Object/new base_class的工作,那么第一次实例化的对象与后面的对象实例不一致.
16. 仔细再对15进行分析,考虑类继承的情况,可以发现当实现类继承时,这种方法出现了效率问题.根据经验我们知道,当类库经过积累变得庞大时,一个应用可能只使用到其中的几个类,大部份类都用不上.但是,如果这些用不上的类具有基类的话,这些不一定能用上的基类都要进行实例化.
17. 综合上面15/16,这个问题的根源在于prototype模式的工作方式.参考一下c++,如果加载了一个类文件,那么这个类就已准备就绪,可以直接使用.但在ECMAScript中,如果只加载一个类的构造器,不作其它工作的话,系统只为构造器建立一个空的prototype,这个类实际并不完备.解决这个问题的关键在于如何选择时机来使这个类完备.
18. 一个简单的方法是,要求用户在使用任何类之前先声明(declare)一下,借此机会将类完备.
19. 接18,将类完备的方法很多.例如新建一个临时实例,触发构造器第一次调用将类完备,然后扔掉这个实例,这种方法代价是所有基类链的第一个实例都被扔掉一次.另一个方法,在用户声明时触发预先为构造器准备的过程,这种方法要求为每一个有基类的构造器添加一个对应的过程.
个人意见,欢迎指正