标签 ‘类型检测’

从特征上检测对象数据类型

十月 14th, 2009 由 Rock 发表

本文关键字

  • 基本数据类型
  • 对象类型
  • 装箱操作
  • 运算符重载

最近也少写东西了,今天看到网友 topcss 的评论

http://www.bgscript.com/archives/24#comment-159

有感,在分析我为什么要那样写之余,谈谈灵活的JavaScript语言系统中对”对象”类型的检测法.

JavaScript中的运算符重载:valueOf()

有些人可能会觉得很奇怪,JavaScript中没像C++语言中提供运算符重载这强大的功能,我想说的是,灵活的JavaScript也可做到运算符重载,虽然能力有限.

Object对象有个方法可能大家平时很少关注,其实这方法正是贯穿本文的方法,那就是valueOf(),且看例子,JS中怎么实现运算符重载.

obj.valueOf() : 返回指定对象的原始值,即对象求值,JavaScript中利用该值参与对象的运算操作.

  alert(+ new Date());
  alert( (new Date()).valueOf());
  alert(new Date() + new Date());

在上面的语句中,JS引擎正是调用Date对象的valueOf求得值并参与算术运算符+的运算.

得知过程后,众所周知,两字符串间的+运算是字符串的连接操作,

   alert(new String('string1') + new String('string2'));

假如我要重载字符串对象的运算符,使得字符串相加时是字符串长度相加而非连接,怎么做?可以这样,重写valueOf方法:

String.prototype.valueOf = function(){
   return this.length;
}
 
//显示5
alert(new String('ss') + new String('www'));

有些同学可能觉得很奇怪了,为什么

  alert('ss' + 'www');

还是显示 ’sswww’,

其实这也是正常的,因为’ss’,'www’这些是JS的基本类型,未经过装箱操作使得它们变成字符串对象实例,所以在+号运算时JS引擎还是执行默认的字符串连接操作.只要改改,使得基本数据类型在运算时JS引擎对其进行装箱操作后就可以了.

  String.prototype.boxUp = function(){
     //这里不对对像进行其它操作,就返回自身,即证明基本类型已被装箱
     return this;
  };
  // 下面这句就能正常如愿了,显示 5 
  alert('ss'.boxUp() + 'www'.boxUp());
  //显示 6
  alert('ss'.boxUp() * 'www'.boxUp());

好了,经过上面的讨论,有了结论:

JS中对象的所具有的行为与运算方式是通过方法来定义

回到话题,利用上面的方法我们可以构建一个”数字类”,且不讨论这”数字类”是否为一”数字”类.

我们参照Number类的特性,类可以进行算术运算符,逻辑运算符与字符串转换操作.

先定义好这类,名为 NumberB,与Number类一样,接受一个基本数字类型

  function NumberB( num ){
     this.value = Number(num);
     return this.value;
  }

再定义一个类,作为NumberB的”父类”,起桥接作用,暂且不管这桥接的作用,下文会说到.

 function Bridge(){};

NumberB原型指向Bridge实例

  NumberB.prototype = new Bridge();

之后再重载valueOf方法,使得NumberB类能参与运算符运算操作,与Number行为一致.

NumberB.prototype.valueOf = function(){
   return this.value;
};

再重载toString,进行正确的字符串转换.

NumberB.protoype.toString(){
  return this.value + '';
};

现在,该类的行为与Number基本一致了,也可进行运算,可以测试一下:

  var a = new NumberB(10),
       b = new NumberB(5);
 
  alert(a + b);
  alert(a / b);
  alert(a + b + ', hello');

好了,到目前为止,你可能还不大信息这类是一个”数字类”,基于JavaScript的灵活特性,下面将使得它变得更像点,那就是通过原型使得该类”继承”自Number类, 符合instanceof 原型链检测.

这里就用到上面的桥接类了,将上面的的桥椄类代码改进为:
注意下面的第四句,第四句类NumberB的原型被new Bridge()覆盖后,其原型的构造器属性已被覆盖,
在覆盖前为 alert(NumberB.prototype.constructor === NumberB);
最后一句将其修正.

1
2
3
4
5
6
7
  function Bridge(){};
  // 该句使得类"继承"自Number
  Bridge.prototype = Number.prototype;
  NumberB.prototype = new Bridge();
 
  //修正NumberB类的constructor属性,使其指向正确的NumberB
  NumberB.prototype.constructor = NumberB;

上面将类NumberB”继承”自Number类后,符合了instanceof检测,

  //显示 true
  alert( new NumberB('5') instanceof Number);
  //显示 NumberB
  alert( new NumberB('5').constructor);

好了,呵呵,现在你还怀疑NumberB不是一个”数字类”么.

再回到讨论的话题, 判断一个对象是否为”数字”,两个方法比较一下,将从两方面说说我为什么不采用obj.constructor来检测.

下面两个方法经笔者优化过,将==换成===.

 //利用constructor检测
 function isNumber(ob) {
   return ob.constructor === Number;
}

//利用tyoeof 与 instanceof检测
function isNumberB(ob) {
   return typeof ob === "number" || ob instanceof Number;
}

1. 对对象混合类型的判断方面

先弄清楚什么是数字类型?

笔者觉得作为数字类型的对象至少要满足其基本数据类型的行为,如对基本数据类型的运算操作等.
从这意义上去,在类型上面的NumberB与Number类是等价的,Number类是内置的对象,作为对基本数据类型的一个对象化封装,而NumberB是对其的另一个对象封装,不同的是,Number对象是JavaScript引擎默认的基本数据类型装箱对象,所以就往往采用ob.constructor === Number去检测,上面的NumberB例子已说明,

假如按特征来判断一个对象是否为数字类型的话, obj.constructor === Number是obj为数字类型的充分非必要条件.

如上面的NumberB类,有些对象通过原型的继承关系获得父类的大部份行为,
在上面NumberB例子中,按数字类型的特征判断,NumberB类可看作是数字类,isNumber方法不能检测,因为构造器却不是Number,此时,isNumberB方法能测出该对象是数字类.

  var num = new NumberB(5);
  //false
  alert( isNumber(num) );
  // true
  alert( isNumberB(num) );

2. 性能方面

下面具体分析这两个方法的性能.

传入的参数数据类型对性能有很关键影响.

参数有两种类型:

  • 1. 基本数据类型, 如 func( 5 );
  • 2. 对象类型 , 如 func(new Number(5));

对于函数:

function isNumber(ob) {
return ob.constructor === Number;
}

如果传进的是基本数据类型,
所要进行的操作有:

  • 对基本数据类型的装箱操作
  • 装箱成Number对象实例后,寻找原型链上的constructor属性
  • 对象引用比较

如果传进的是对象类型

所要进行的操作有:

  • 寻找原型链上的constructor属性
  • 对象引用比较

所以对对象的操作少了装箱操作.

但日常我们用到的数字多是基本的数据类型,所以大多数情况下isNumber方法少不了装箱操作.

对于函数:

function isNumberB(ob) {
return typeof ob === “number” || ob instanceof Number;
}

如果传进的是基本数据类型,
所要进行的操作有:

  • typeof ob 检测无需装箱
  • 值比较

如果传进的是对象类型

所要进行的操作有:

  • 如果为Number实例,引用比较返回true
  • 如果非Number实例,如上面的NumberB类实例,将进行 instanceof 比较原型链

可见,对于对象类型而非Number的实例对象,isNumberB将进行更多的比较.

但日常我们用到的数字多是基本的数据类型,所以大多数情况下isNumberB方法将执行typeof检测后返回正确的.

可见,如果传进的是基本数据类型的话,无论如何,isNumberB都不会比isNumber慢,如果是对象类型或非数字类型的对象isNumberB有相当可能比isNumber慢.

下面是笔者在firefox,ie,chrome 下的测试数据,判断运行一百万次:

var n = new Number(5);
//   n = new NumberB(5);
var d = (new Date()).valueOf();

for(var i=0;i<1000000;i++){
	 isNumber(n);
         // isNumberB(n);
}
 alert(new Date() - d);

// 结果
 //firefox 3.5.3
   isNumberB(n) 407 ms
   isNumberB(5) 7 ms
 //--
   isNumber(n) 610 ms
   isNumber(5) 1555 ms

 // ie 8
   isNumberB(n) 1110 ms
   isNumberB(5) 813 ms
 //--
   isNumber(n) 970 ms
   isNumber(5) 954 ms

 //chrome
   isNumberB(n) 51 ms
   isNumberB(5) 38 ms
 //--
   isNumber(n) 53 ms
   isNumber(5) 170 ms

额外话:

IE对基本数据类型的typeof检测,真是令我对IE的JS引擎”佩服”得五体投地,
一般JS引擎的基本数据类型数据结构如下:

  struct Value {
     //类型标记
     int dataType;
     Data *data;
     ...
  }

对于typeof 检测,直接判断dataType取值即可,所以理应非常快的.这方面firefox做得最好,居然7ms就能完成任务.
但IE耗时居然和对象类型差不多,可能并没优化,一律进行装箱操作 -_-!!!