JavaScript面向对象的支持(上)

来源:岁月联盟 编辑:zhuzhu 时间:2009-01-15

  很少有人对JavaScript的面向对象特性进行系统的分析。我希望接下来的文字让你了解到这个语言最少为人知的一面。

  1. JavaScript中的类型

  --------

  虽然JavaScript是一个基于对象的语言,但对象(Object)在JavaScript中不是第一型的。JS是以函数(Function)为第一型的语言。这样说,不但是因为JS中的函数具有高级语言中的函数的各种特性,而且也因为在JS中,Object也是由函数来实现的。——关于这一点,可以在后文中“构造与析构”部分看到更进一步的说明。JS中是弱类型的,他的内置类型简单而且清晰:

  undefined : 未定义

  number  : 数字

  boolean  : 布尔值

  string  : 字符串

  function : 函数

  object  : 对象1). undefined类型

  在IE5及以下版本中,除了直接赋值和typeof()之外,其它任何对undefined的操作都将导致异常。如果需要知道一个变量是否是undefined,只能采用typeof()的方法:

<script>
var v;
if (typeof(v) == 'undefined') {
 // ...
}
</script>

  但是在IE5.5及以上版本中,undefined是一个已实现的系统保留字。因此可以用undefined来比较和运算。检测一个值是否是undefined的更简单方法可以是:

<script>
var v;
if (v === undefined) {
 // ...
}
</script>

  因此为了使得核心代码能(部分地)兼容IE5及早期版本,Romo核心单元中有一行代码用来“声明”一个undefined值:

//---------------------------------------------------------
// code from Qomolangma, in JSEnhance.js
//---------------------------------------------------------
var undefined = void null;

  这一行代码还有一点是需要说明的,就是void语句的应用。void表明“执行其后的语句,且忽略返回值”。因此在void之后可以出现能被执行的任何“单个”语句。而执行的结果就undefined。当然,如果你愿意,你也可以用下面的代码之一“定义undefined”。

  // 1. 较复杂的方法,利用一个匿名的空函数执行的返回

  var undefined = function(){}();

  // 2. 代码更简洁,但不易懂的方法

  var undefined = void 0;void也能像函数一样使用,因此void(0)也是合法的。有些时候,一些复杂的语句可能不能使用void的关键字形式,而必须要使用void的函数形式。例如:

  // 必须使用void()形式的复杂表达式

  void(i=1);   

  // 或如下语句:

  void(i=1, i++);

  2). number类型

  JavaScript中总是处理浮点数,因此它没有象Delphi中的MaxInt这样的常量,反而是有这样两个常值定义:

  Number.MAX_VALUE : 返回 JScript 能表达的最大的数。约等于 1.79E+308。

  Number.MIN_VALUE : 返回 JScript 最接近0的数。约等于 2.22E-308。因为没有整型的缘故,因此在一些关于CSS和DOM属性的运算中,如果你期望取值为整数2,你可能会得到字符串“2.0”——或者类似于此的一些情况。这种情况下,你可能需要用到全局对象(Gobal)的parseInt()方法。全局对象(Gobal)中还有两个属性与number类型的运算有关:

  NaN   : 算术表达式的运算结果不是数字,则返回NaN值。

  Infinity : 比MAX_VALUE更大的数。如果一个值是NaN,那么他可以通过全局对象(Gobal)的isNaN()方法来检测。然而两个NaN值之间不是互等的。如下例:

  // NaN的运算与检测
  //---------------------------------------------------------
  var
  v1 = 10 * 'a';
  v2 = 10 * 'a';
  document.writeln(isNaN(v1));
  document.writeln(isNaN(v2));

  document.writeln(v1 == v2);全局对象(Gobal)的Infinity表示比最大的数 (Number.MAX_VALUE) 更大的值。在JS中,它在数学运算时的价值与正无穷是一样的。——在一些实用技巧中,它也可以用来做一个数组序列的边界检测。Infinity在Number对象中被定义为POSITIVE_INFINITY。此外,负无穷也在Number中被定义:

  Number.POSITIVE_INFINITY : 比最大正数(Number.MAX_VALUE)更大的值。正无穷。

  Number.NEGATIVE_INFINITY : 比最小负数(-Number.MAX_VALUE)更小的值。负无穷。与NaN不同的是,两个Infinity(或-Infinity)之间是互等的。如下例:

  // Infinity的运算与检测
  //---------------------------------------------------------
  var
  v1 = Number.MAX_VALUE * 2;
  v2 = Number.MAX_VALUE * 3;
  document.writeln(v1);
  document.writeln(v2);

  document.writeln(v1 == v2);在Global中其它与number类型相关的方法有:

  isFinite()  : 如果值是NaN/正无穷/负无穷,返回false,否则返回true。

  parseFloat() : 从字符串(的前缀部分)取一个浮点数。不成功则返回NaN。

  3). boolean类型

  (略)4). string类型

  JavaScript中的String类型原本没有什么特殊的,但是JavaScript为了适应“浏览器实现的超文本环境”,因此它具有一些奇怪的方法。例如:

  link() : 把一个有HREF属性的超链接标签<A>放在String对象中的文本两端。

  big() : 把一对<big>标签放在String对象中的文本两端。

  以下方法与此类同:

  anchor()

  blink()

  bold()

  fixed()

  fontcolor()

  fontsize()

  italics()

  small()

  strike()

  sub()

  sup()除此之外,string的主要复杂性来自于在JavaScript中无所不在的toString()方法。这也是JavaScript为浏览器环境而提供的一个很重要的方法。例如我们声明一个对象,但是要用document.writeln()来输出它,在IE中会显示什么呢?下例说明这个问题:

// toString()的应用
  //---------------------------------------------------------
  var

  s = new Object();s.v1 = 'hi,';
s.v2 = 'test!';
document.writeln(s);
document.writeln(s.toString());
s.toString = function() {
 return s.v1 + s.v2;
}
document.writeln(s);

  在这个例子中,我们看到,当一个对象没有重新声明(覆盖)自己toString()方法的时候,那么它作为字符串型态使用时(例如被writeln),就会调用Java Script环境缺省的toString()。反过来,你也可以重新定义JavaScript理解这个对象的方法。很多JavaScript框架,在实现“模板”机制的时候,就利用了这个特性。例如

  他们用这样定义一个FontElement对象:

// 利用toString()实现模板机制的简单原理
  //---------------------------------------------------------
  function FontElement(innerHTML) {
  this.face = '宋体';
 this.color = 'red';
 // more...
 var ctx = innerHTML;
 this.toString = function() {
  return '<Font FACE="' + this.face + '" COLOR="' + this.color + '">'
   + ctx
   + '</FONT>';
 }
}
var obj = new FontElement('这是一个测试。');

  // 留意下面这行代码的写法

  document.writeln(obj);

  5). function类型

  javascript函数具有很多特性,除了面向对象的部分之外(这在后面讲述),它自已的一些独特特性应用也很广泛。首先javascript中的每个函数,在调用过程中可以执有一个arguments对象。这个对象是由脚本解释环境创建的,你没有别的方法来自己创建一个arguments对象。arguments可以看成一个数组:它有length属性,并可以通过arguments[n]的方式来访问每一个参数。然而它最重要的,却是可以通过 callee 属性来得到正在执行的函数对象的引用。接下的问题变得很有趣:Function对象有一个 caller 属性,指向正在调用当前函数的父函数对象的引用。

  ——我们已经看到,我们可以在JavaScript里面,通过callee/caller来遍历执行期的调用栈。由于arguments事实上也是Function的一个属性,因此我们事实上也能遍历执行期调用栈上的每一个函数的参数。下面的代码是一个简单的示例:

  // 调用栈的遍历
  //---------------------------------------------------------
  function foo1(v1, v2) {
  foo2(v1 * 100);
  }function foo2(v1) {
 foo3(v1 * 200);
}function foo3(v1) {
 var foo = arguments.callee;
 while (foo && (foo != window)) {
  document.writeln('调用参数:<br>', '---------------<br>');  var args = foo.arguments, argn = args.length;
  for (var i=0; i<argn; i++) {
   document.writeln('args[', i, ']: ', args[i], '<br>');
  }
  document.writeln('<br>');  // 上一级
  foo = foo.caller;
 }
}

  // 运行测试

  foo1(1, 2);

  2. JavaScript面向对象的支持

  --------在前面的例子中其实已经讲到了object类型的“类型声明”与“实例创建”。

  在JavaScript中,我们需要通过一个函数来声明自己的object类型:

//---------------------------------------------------------
// JavaScript中对象的类型声明的形式代码
// (以后的文档中,“对象名”通常用MyObject来替代)
//---------------------------------------------------------
function 对象名(参数表) {
 this.属性 = 初始值; this.方法 = function(方法参数表) {
  // 方法实现代码
 }
}

  然后,我们可以通过这样的代码来创建这个对象类型的一个实例:

  // 创建实例的形式代码

  // (以后的文档中,“实例变量名”通常用obj来替代)

  var 实例变量名 = new 对象名(参数表);

  接下来我们来看“对象”在JavaScript中的一些具体实现和奇怪特性。1). 函数在JavaScript的面向对象机制中的五重身份

  ------“对象名”

  ——如MyObject()

  ——这个函数充当了以下语言角色:

  (1) 普通函数

  (2) 类型声明

  (3) 类型的实现

  (4) 类引用

  (5) 对象的构造函数一些程序员(例如Delphi程序员)习惯于类型声明与实现分开。例如在delphi中,Interface节用于声明类型或者变量,而implementation节用于书写类型的实现代码,或者一些用于执行的函数、代码流程。但在JavaScript中,类型的声明与实现是混在一起的。一个对象的类型(类)通过函数来声明,this.xxxx表明了该对象可具有的属性或者方法。

  这个函数的同时也是“类引用”。在JavaScript,如果你需要识别一个对象的具体型别,你需要执有一个“类引用”。——当然,也就是这个函数的名字。instanceof 运算符就用于识别实例的类型,我们来看一下它的应用:

  //---------------------------------------------------------
  // JavaScript中对象的类型识别
  //  语法: 对象实例 instanceof 类引用
  //---------------------------------------------------------
  function MyObject() {
  this.data = 'test data';
  }// 这里MyObject()作为构造函数使用
  var obj = new MyObject();
  var arr = new Array();// 这里MyObject作为类引用使用
  document.writeln(obj instanceof MyObject);
  document.writeln(arr instanceof MyObject);

  (未完待续)

  接下来的内容:2. JavaScript面向对象的支持

  --------2). 反射机制在JavaScript中的实现

  3). this与with关键字的使用

  4). 使用in关键字的运算

  5). 使用instanceof关键字的运算

  6). 其它与面向对象相关的关键字3. 构造与析构4. 实例和实例引用5. 原型问题6. 函数的上下文环境7. 对象的类型检查问题 类别  :Rich Web Client

  关键词 :JS OOP,JS Framwork, Rich Web Client,RIA,Web Component,

  DOM,DTHML,CSS,JavaScript,JScript项目发起:aimingoo (aim@263.net)

  项目团队:aimingoo, leon(pfzhou@gmail.com)

  有贡献者:JingYu(zjy@cnpack.org)

  2). 反射机制在JavaScript中的实现

  ------JavaScript中通过for..in语法来实现了反射机制。但是JavaScript中并不明确区分“属性”与“方法”,以及“事件”。因此,对属性的类型考查在JS中是个问题。下面的代码简单示例for..in的使用与属性识别:

//---------------------------------------------------------
// JavaScript中for..in的使用和属性识别
  //---------------------------------------------------------
  var _r_event = _r_event = /^[Oo]n.*/;
  var colorSetting = {
  method: 'red',
  event: 'blue',
  property: ''
  }var obj2 = {
 a_method : function() {},
 a_property: 1,
 onclick: undefined
}function propertyKind(obj, p) {
 return (_r_event.test(p) && (obj[p]==undefined || typeof(obj[p])=='function')) ? 'event'
  : (typeof(obj[p])=='function') ? 'method'
  : 'property';
}var objectArr = ['window', 'obj2'];for (var i=0; i<objectArr.length; i++) {
 document.writeln('<p>for ', objectArr[i], '<hr>'); var obj = eval(objectArr[i]);
 for (var p in obj) {
  var kind = propertyKind(obj, p);
  document.writeln('obj.', p, ' is a ', kind.fontcolor(colorSetting[kind]), ': ', obj[p], '<br>');
 } document.writeln('</p>');
}

  一个常常被开发者忽略的事实是:JavaScript本身是没有事件(Event)系统的。通常我们在JavaScript用到的onclick等事件,其实是IE的DOM模型提供的。从更内核的角度上讲:IE通过COM的接口属性公布了一组事件接口给DOM。有两个原因,使得在JS中不能很好的识别“一个属性是不是事件”:

  - COM接口中本身只有方法,属性与事件,都是通过一组get/set方法来公布的。

  - JavaScript中,本身并没有独立的“事件”机制。因此我们看到event的识别方法,是检测属性名是否是以'on'字符串开头(以'On'开头的是Qomo的约定)。接下来,由于DOM对象中的事件是可以不指定处理函数的,这种情况下事件句柄为null值(Qomo采用相同的约定);在另外的一些情况下,用户可能象obj2这样,定义一个值为 undefined的事件。因此“事件”的判定条件被处理成一个复杂的表达式:

  ("属性以on/On开头" && ("值为null/undefined" || "类型为function"))另外,从上面的这段代码的运行结果来看。对DOM对象使用for..in,是不能列举出对象方法来的。最后说明一点。事实上,在很多语言的实现中,“事件”都不是“面向对象”的语言特性,而是由具体的编程模型来提供的。例如Delphi中的事件驱动机制,是由Win32操作系统中的窗口消息机制来提供,或者由用户代码在Component/Class中主动调用事件处理函数来实现。“事件”是一个“如何驱动编程模型”的机制/问题,而不是语言本身的问题。然而以PME(property/method/event)为框架的OOP概念,已经深入人心,所以当编程语言或系统表现出这些特性来的时候,就已经没人关心“event究竟是谁实现”的了。

  3). this与with关键字的使用

  ------在JavaScript的对象系统中,this关键字用在两种地方:

  - 在构造器函数中,指代新创建的对象实例

  - 在对象的方法被调用时,指代调用该方法的对象实例如果一个函数被作为普通函数(而不是对象方法)调用,那么在函数中的this关键字将指向window对象。与此相同的,如果this关键字不在任何函数中,那么他也指向window对象。由于在JavaScript中不明确区分函数与方法。因此有些代码看起来很奇怪:

//---------------------------------------------------------
// 函数的几种可能调用形式
  //---------------------------------------------------------
  function foo() {
  // 下面的this指代调用该方法的对象实例
  if (this===window) {
  document.write('call a function.', '<BR>');
  }
  else {
  document.write('call a method, by object: ', this.name, '<BR>');
  }
  }function MyObject(name) {
 // 下面的this指代new关键字新创建实例
  this.name = name;
  this.foo = foo;
  }var obj1 = new MyObject('obj1');
var obj2 = new MyObject('obj2');
// 测试1: 作为函数调用
  foo();// 测试2: 作为对象方法的调用
  obj1.foo();
  obj2.foo();// 测试3: 将函数作为“指定对象的”方法调用
  foo.call(obj1);
  foo.apply(obj2);

  在上面的代码里,obj1/obj2对foo()的调用是很普通的调用方法。

  ——也就是在构造器上,将一个函数指定为对象的方法。而测试3中的call()与apply()就比较特殊。在这个测试中,foo()仍然作为普通函数来调用,只是JavaScript的语言特性允许在call()/apply()时,传入一个对象实例来指定foo()的上下文环境中所出现的this关键字的引用。

  ——需要注意的是,此时的foo()仍旧是一个普通函数调用,而不是对象方法调用。与this“指示调用该方法的对象实例”有些类同的,with()语法也用于限定“在一段代码片段中默认使用对象实例”。

  ——如果不使用with()语法,那么这段代码将受到更外层with()语句的影响;如果没有更外层的with(),那么这段代码的“默认使用的对象实例”将是window。然而需要注意的是this与with关键字不是互为影响的。如下面的代码:

//---------------------------------------------------------
// 测试: this与with关键字不是互为影响的
  //---------------------------------------------------------
  function test() {
  with (obj2) {
  this.value = 8;
  }
  }
  var obj2 = new Object();
  obj2.value = 10;test();
document.writeln('obj2.value: ', obj2.value, '<br>');
document.writeln('window.value: ', window.value, '<br>');

  你不能指望这样的代码在调用结束后,会使obj2.value属性置值为8。这几行代码的结果是:window对象多了一个value属性,并且值为8。with(obj){...}这个语法,只能限定对obj的既有属性的读取,而不能主动的声明它。一旦with()里的对象没有指定的属性,或者with()限定了一个不是对象的数据,那么结果会产生一个异常。

  4). 使用in关键字的运算

  ------除了用for..in来反射对象的成员信息之外,JavaScript中也允许直接用in关键字去检测对象是否有指定名字的属性。in关键字经常被提及的原因并不是它检测属性是否存在的能力,因此在早期的代码中,很多可喜欢用“if (!obj.propName) {}” 这样的方式来检测propName是否是有效的属性。

  ——很多时候,检测有效性比检测“是否存有该属性”更有实用性。因此这种情况下,in只是一个可选的、官方的方案。in关键字的重要应用是高速字符串检索。尤其是在只需要判定“字符串是否存在”的情况下。例如10万个字符串,如果存储在数组中,那么检索效率将会极差。

//---------------------------------------------------------
// 使用对象来检索
  //---------------------------------------------------------
  function arrayToObject(arr) {
  for (var obj=new Object(), i=0, imax=arr.length; i<imax; i++) {
  obj[arr[i]]=null;
  }
  return obj;
  }var
 arr = ['abc', 'def', 'ghi']; // more and more...
 obj = arrayToObject(arr);function valueInArray(v) {
 for (var i=0, imax=arr.length; i<imax; i++) {
  if (arr[i]==v) return true;
 } return false;
}function valueInObject(v) {
 return v in obj;
}

  这种使用关键字in的方法,也存在一些限制。例如只能查找字符串,而数组元素可以是任意值。另外,arrayToObject()也存在一些开销,这使得它不适合于频繁变动的查找集。最后,(我想你可能已经注意到了)使用对象来查找的时候并不能准确定位到查找数据,而数组中可以指向结果的下标。

  八、JavaScript面向对象的支持

  ~~~~~~~~~~~~~~~~~~

  (续)

  2. JavaScript面向对象的支持

  --------

  (续)5). 使用instanceof关键字的运算

  ------

  在JavaScript中提供了instanceof关键字来检测实例的类型。这在前面讨论它的“五重身份”时已经讲过。但instanceof的问题是,它总是列举整个原型链以检测类型(关于原型继承的原理在“构造与析构”小节讲述),如:

//---------------------------------------------------------
// instanceof使用中的问题
  //---------------------------------------------------------
  function MyObject() {
  // ...
  }function MyObject2() {
 // ...
}
MyObject2.prototype = new MyObject();obj1 = new MyObject();
obj2 = new MyObject2();document.writeln(obj1 instanceof MyObject, '<BR>');
document.writeln(obj2 instanceof MyObject, '<BR>');

  我们看到,obj1与obj2都是MyObject的实例,但他们是不同的构造函数产生的。

  ——注意,这在面向对象理论中正确的:因为obj2是MyObject的子类实例,因此它具有与obj1相同的特性。在应用中这是obj2的多态性的体现之一。但是,即便如此,我们也必须面临这样的问题:如何知道obj2与obj1是否是相同类型的实例呢?

  ——也就是说,连构造器都相同?instanceof关键字不提供这样的机制。一个提供实现这种检测的能力的,是Object.constructor属性。

  ——但请先记住,它的使用远比你想象的要难。好的,问题先到这里。constructor属性已经涉及到“构造与析构”的问题,这个我们后面再讲。“原型继承”、“构造与析构”是JavaScript的OOP中的主要问题、核心问题,以及“致命问题”。

  6). null与undefined

  ------

  在JavaScript中,null与undefined曾一度使我迷惑。下面的文字,有利于

  你更清晰的认知它(或者让你更迷惑):

  - null是关键字;undefined是Global对象的一个属性。

  - null是对象(空对象, 没有任何属性和方法);undefined是undefined类

  型的值。试试下面的代码:

  document.writeln(typeof null);

  document.writeln(typeof undefined);

  - 对象模型中,所有的对象都是Object或其子类的实例,但null对象例外:

  document.writeln(null instanceof Object);

  - null“等值(==)”于undefined,但不“全等值(===)”于undefined:

  document.writeln(null == undefined);

  document.writeln(null == undefined);

  - 运算时null与undefined都可以被类型转换为false,但不等值于false:

  document.writeln(!null, !undefined);

  document.writeln(null==false);

  document.writeln(undefined==false);类别  :Rich Web Client

  关键词 :JS OOP,JS Framwork, Rich Web Client,RIA,Web Component,

  DOM,DTHML,CSS,JavaScript,JScript

  项目发起:aimingoo (aim@263.net)

  项目团队:aimingoo, leon(pfzhou@gmail.com)

  有贡献者:JingYu(zjy@cnpack.org)