JavaScript框架设计
上QQ阅读APP看书,第一时间看更新

1.4 类型的判定

JavaScript 存在两套类型系统,一套是基本数据类型,另一套是对象类型系统。基本数据类型包括6种,分别是undefined、string、null、boolean、function、object。基本数据类型是通过typeof来检测的。对象类型系统是以基础类型系统为基础的,通过 instanceof 来检测。然而,JavaScript自带的这两套识别机制非常不靠谱,于是催生了isXXX系列。就拿typeof来说,它只能粗略识别出 string、number、boolean、function、undefined、object 这 6 种数据类型,无法识别 Null、RegExpAragument 等细分对象类型。

让我们看一下这里面究竟有多少陷阱。

      typeof null// "object"
      typeof document.childNodes //safari "function"
      typeof document.createElement('embed')//ff3-10 "function"
      typeof document.createElement('object')//ff3-10 "function"
      typeof document.createElement('applet')//ff3-10 "function"
      typeof /\d/i //在实现了ecma262v4的浏览器返回 "function"
      typeof window.alert //IE678 "object""
      var iframe = document.createElement('iframe');
      document.body.appendChild(iframe);
      xArray = window.frames[window.frames.length - 1].Array;
      var arr = new xArray(1, 2, 3); // [1,2,3]
      arr instanceof Array; // false
      arr.constructor === Array; // false
      window.onload = function() {
          alert(window.constructor);// IE67 undefined
          alert(document.constructor);// IE67 undefined
          alert(document.body.constructor);// IE67 undefined
          alert((new ActiveXObject('Microsoft.XMLHTTP')).constructor);// IE6789 undefined
      }
      isNaN("aaa") //true

上面分4组,第一组是typeof的坑。第二组是instanceof 的陷阱,只要原型上存在此对象的构造器它就返回true,但如果跨文档比较,iframe里面的数组实例就不是父窗口的Array的实例。第三组有关constructor的陷阱,在旧版本IE下DOM与BOM对象的constructor属性是没有暴露出来的。最后有关NaN,NaN对象与null、undefined一样,在序列化时是原样输出的,但isNaN这方法非常不靠谱,把字符串、对象放进去也返回true,这对我们序列化非常不利。

另外,在IE下typeof还会返回unknow的情况。

      if (typeof window.ActiveXObject != "undefined") {
          var xhr = new ActiveXObject("Msxml2.XMLHTTP");
          alert(typeof xhr.abort);
      }

基于这IE的特性,我们可以用它来判定某个VBscript方法是否存在。

      <script type="text/VBScript">
              function VBMethod(a,b)
              VBMethod = a + b
              end  function
      </script>
      <script>
              if(typeof VBMethod === "unknown"){//看这个
                    alert(VBMethod(10,34))
              }
      </script>

另外,以前人们总是以 document.all事实上是非IE浏览器均实现了叫做“伪装为undefined”特性,当采用逻辑运算或类型判断时,会特意输出 undefined值。http://fremycompany.com/BG/2013/Internet-Explorer-11-9385-new-features-771/是否存在来判定 IE,这其实是很危险的。因为用document.all 来取得页面中的所有元素是不错的主意,这个方法 Firefox、Chrome 觊觎好久了,不过人们都这样判定,于是有了在Chrome下的这出闹剧。

      typeof document.all // undefined
      document.all // HTMLAllCollection[728] (728为元素总数)

在判定undefined、null、string、number、boolean、function这6个还算简单,前面两个可以分别与void(0)、null比较,后面4个直接typeof也可满足90%的情形。这样说是因为string、number、boolean可以包装成“伪对象”,typeof无法按照我们的意愿工作了,虽然它严格执行了 Ecmascript 的标准。

      typeof new Boolean(1);//"object"
      typeof new Number(1);//"object"
      typeof new String("aa");//"object"

这些还是最简单的,难点在于 RegExp 与 Array。判定 RegExp 类型的情形很少,不多讲了, Array则不一样。有关isArray的实现不下二十种,都是因为JavaScript的鸭子类型在程序设计中,鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由当前方法和属性的集合决定。这个概念的名字来源于由James Whitcomb Riley提出的鸭子测试,“鸭子测试”可以这样表述:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”在鸭子类型中,关注的不是对象的类型本身,而是它如何被使用的。被攻破了。直到Prototype.js把Object.prototype.toString发掘出来,此方法是直接输出对象内部的[[Class]],绝对精准。有了它,可以跳过95%的陷阱了。

isArray早些年的探索:

      function isArray(arr) {
          return arr instanceof Array;
      }
      function isArray(arr) {
          return !!arr && arr.constructor == Array;
      }
      function isArray(arr) {//Prototype.js1.6.0.3
          return arr != null && typeof arr === "object" &&
                  'splice' in arr && 'join' in arr;
      }
      function isArray(arr) {//Douglas Crockford
          return typeof arr.sort == 'function'
      }
      function isArray(array) {//kriszyp
          var result = false;
          try {
              new array.constructor(Math.pow(2, 32))
          } catch (e) {
              result = /Array/.test(e.message)
          }
          return result;
      };
      function isArray(o) {// kangax
          try {
              Array.prototype.toString.call(o);
              return true;
          } catch (e) {
              }
          return false;
      };
      function isArray(o) {//kangax
          if (o && typeof o == 'object' && typeof o.length == 'number' && isFinite(o.length))
      {
              var _origLength = o.length;
              o[o.length] = '__test__';
              var _newLength = o.length;
              o.length = _origLength;
              return _newLength == _origLength + 1;
          }
          return false;
      }

至于null、undefined、NaN直接这样:

      function isNaN(obj) {
          return obj !== obj
      }
      function isNull(obj) {
          return obj === null;
      }
      function isUndefined(obj) {
          return obj === void 0;
      }

最后要判定的对象是window,由于ECMA是不规范 Host 对象,window 对象属于 Host ,所以也没有被约定,就算Object.prototype.toString也对它无可奈何。

· [object Object]IE6

· [object Object]IE7

· [object Object]IE8

· [object Window]IE9

· [object Window]firefox3.6

· [object Window]opera10

· [object DOMWindow]safai4.04

· [object global]chrome5.0.3.22

不过根据window.window和window.setInterval去判定更加不够谱,用一个技巧我们可以完美识别IE6、IE7、IE8的window对象,其他还是用toString,这个神奇的hack(技巧)就是,window与document互相比较,如果顺序不一样,其结果是不一样的!

      window == document // IE678 true;
      document == window // IE678 false;

当然,如果细数起来,JavaScript匪夷所思的事比比都是。

存在a !== a的情况;

存在a == b && b != a的情况;

存在a == !a的情况;

存在a === a+100的情况;

1 < 2 < 3为true, 3 > 2 > 1为false;

0/0为NaN;

……

好了,至此,所有重要的 isXXX 问题都解决了,剩下的就把它们表达出来。经典做法就是直接罗列。

在Prototype.js中,拥有isElement、isArray、isHash、isFunction、isString、isNumber、isDate、isUndefined方法。

mootools搞了个typeOf判定基本类型,instanceOf判定自定义“类”。

RightJS有isFunction 、isHash、isString、isNumber、isArray 、isElement 、isNode。

EXT有isEmpty、isArray、isDate、isObject、isSimpleObject、isPrimitive、isPrimitive、isFunction、isNumber、isNumeric、isString、isBoolean、isElement、isTextNode、isDefined、isIterable,应有尽有。最后,还有typeOf判定基本类型。

Underscore.js 有 isElement、isEmpty、isArray、isArguments、isObject、isFunction、isString、isNumber、isFinite、isNaN、isBoolean、isDate、isRegExp、isNull、isUndefined。

isXXX系列就像恶性肿瘤一样不断膨胀,其实你多弄几个isXXX也不能满足用户的全部需求。就像isDate、isRegExp会用到的机率有多高呢?

jQuery 就不与其他框架一样了,在 jQuery 1.4 中只有 isFunction、isArray、isPlainObject、isEmptyObject。IsFunction、isArray 肯定是用户用得最多,isPlainObject 则是用来判定是否为纯净的JavaScript对象,既不是DOM、BOM对象,也不是自定义“类”的实例对象,制造它的最初目的是用于深拷贝,避开像window那样自己引用自己的对象。isEmptyObject是用于数据缓存系统,当此对象为空时,就可以删除它。

          //jquery2.0
          jQuery.isPlainObject = function(obj) {
              //首先排除基础类型不为Object的类型,然后是DOM节点与window对象
              if (jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow(obj)) {
                  return false;
              }
              //然后回溯它的最近的原型对象是否有isPrototypeOf,
              //旧版本IE的一些原生对象没有暴露constructor、prototype,因此会在这里过滤
              try {
                  if (obj.constructor &&
                          !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
                      return false;
                  }
              } catch (e) {
                  return false;
              }
              return true;
          }

在avalon.mobile中有一个更精简的版本,由于它只支持IE10等非常新的浏览器,就没有干扰因素了,可以大胆使用ecma262v5的新API。

      avalon.isPlainObject = function(obj) {
          return obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype
      }

isArrayLike也是一个常用的方法,但判定一个类数组太难了,唯一的辨识手段是它应该有一个大于或等于零的整型length属性。此外还有一些“共识”,如window与函数和元素节点(如form元素)不算类数组,虽然它们都满足前面的条件。因此至今jQuery没有把它暴露出来。

        //jquery2.0
          function isArraylike(obj) {
              var length = obj.length, type = jQuery.type(obj);
              if (jQuery.isWindow(obj)) {
                  return false;
              }
              if (obj.nodeType === 1 && length) {
                  return true;
              }
              return type === "array" || type !== "function" &&
                      (length === 0 ||
                              typeof length === "number" && length > 0 && (length - 1) in obj);
          }
          //avalon 0.9
          function isArrayLike(obj) {
              if (obj && typeof obj === "object") {
                  var n = obj.length
                  if (+n === n && !(n % 1) && n >= 0) { //检测length属性是否为非负整数
                      try {//像Argument、Array、NodeList等原生对象的length属性是不可遍历的
                          if ({}.propertyIsEnumerable.call(obj, 'length') === false) {
                              return Array.isArray(obj) || /^\s?function/.test(obj.item || obj. callee)
                          }
                          return true;
                      } catch (e) { //IE的NodeList直接抛错
                          return true
                      }
                  }
              }
              return false
          }
          //avalon.mobile更倚重Object.prototoype.toString来判定
          function isArrayLike(obj) {
              if (obj && typeof obj === "object") {
                  var n = obj.length,
                          str = Object.prototype.toString.call(obj)
                  if (/Array|NodeList|Arguments|CSSRuleList/.test(str)) {
                      return true
                  } else if (str === "[object Object]" && (+n === n && !(n % 1) && n >= 0)) {
                      return true //由于 ecma262v5 能修改对象属性的 enumerable,因此不能用 propertyIs
                                    //Enumerable来判定了
                  }
              }
              return false
          }

补充一句,1.3版本中,Prototype.js的研究成果(Object.prototype.toString.call)就应用于jQuery了。在1.2版本中,jQuery判定一个变量是否为函数非常复杂。

      isFunction: function( fn ) {
      return !!fn&&typeoffn != "string" && !fn.nodeName&&
      fn.constructor != Array && /^[\s[]?function/.test( fn + "" );
        }

jQuery1.43引入isWindow来处理makeArray中对window的判定,引入isNaN用于确保样式赋值的安全。同时引入type代替typeof关键字,用于获取数据的基本类型。

      class2type = {}
      jQuery.each("Boolean  Number  String  Function  Array  Date  RegExpObject".split("  "),
      function(i, name) {
          class2type[ "[object " + name + "]" ] = name.toLowerCase();
      });
      jQuery.type = function(obj) {
          return obj == null ?
                  String(obj) :
                  class2type[toString.call(obj) ] || "object";
      })

jQuery1.7中添加isNumeric代替isNaN。这是个不同于其他框架的isNumber,它可以是字符串,只要外观上像数字就行了。但 jQuery1.7 还做了一件违背之前提到稳定性的事情,贸然去掉jQuery.isNaN ,因此导致基于旧版本 jQuery 的一大批插件失效。

      //jquery1.43~1.64
      jQuery.isNaN = function(obj) {
          return obj == null || !rdigit.test(obj) || isNaN(obj);
      })
      //jquery1.7 就是isNaN的取反版
      jQuery.isNumeric = function(obj) {
          return obj != null && rdigit.test(obj) && !isNaN(obj);
      })
      //jquery1.71~1.72
      jQuery.isNumeric = function(obj) {
          return !isNaN(parseFloat(obj)) && isFinite(obj);
      }
      //jquery2.1
      jQuery.isNumeric = function(obj) {
          return obj - parseFloat(obj) >= 0;
      }

mass Framework的思路与jQuery一致,尽量减少isXXX系列的数量,把isWindow、isNaN、nodeName等方法都整进去了。这是个野心勃勃的方法,代码比较长,它既可以获取类型,也可以传入第二参数进行类型比较。

      var class2type = {
          "[objectHTMLDocument]": "Document",
          "[objectHTMLCollection]": "NodeList",
          "[objectStaticNodeList]": "NodeList",
          "[objectIXMLDOMNodeList]": "NodeList",
          "[objectDOMWindow]": "Window",
          "[object global]": "Window",
          "null": "Null",
          "NaN": "NaN",
          "undefined": "Undefined"
      },
      toString = class2type.toString;
      "Boolean,Number,String,Function,Array,Date,RegExp,Window,Document,Arguments,NodeList"
              .replace($.rword, function(name) {
          class2type[ "[object " + name + "]" ] = name;
      });
      //class2type这个映射几乎把所有常用判定对象“一网打尽”了
      mass.type = function(obj, str) {
          var result = class2type[ (obj == null || obj !== obj) ? obj : toString.call(obj) ]
                  || obj.nodeName || "#";
          if (result.charAt(0) === "#") {  //兼容旧版本浏览器与处理个别情况,如window.opera
          //利用IE6、IE7、IE8 window == document为true,document == window竟然为false的神奇特性
              if (obj == obj.document && obj.document != obj) {
                  result = 'Window';         //返回构造器名字
              } else if (obj.nodeType === 9) {
                  result = 'Document';       //返回构造器名字
              } else if (obj.callee) {
                  result = 'Arguments';      //返回构造器名字
              } else if (isFinite(obj.length) && obj.item) {
                  result = 'NodeList';       //处理节点集合
              } else {
                  result = toString.call(obj).slice(8, -1);
              }
          }
          if (str) {
              return str === result;
          }
          return result;
      }

然后type方法就轻松了,用toString.call(obj)得出的值作键,直接从映射中取。只有在IE6、IE7、IE8中,我们才费一些周折处理window、document、arguments、nodeList等对象。当然,这只是在种子模块的情形,在语言模块,mass Framework还是会添加isArray、isFunction这两个著名API,此外还有isPlainObject、isNative、isEmptyObject、isArrayLike这4个方法,在选择器模块,还追加isXML方法。

基于实用主义,我们有时不得不妥协。百度的tangram就是典型, 与EXT一样,能想到的都写上,而且判定非常严谨。

      baidu.isDate = function(o) {
          return {}.toString.call(o) === "[object Date]" && o.toString() !== 'Invalid Date'
      && !isNaN(o);
      }
      baidu.isNumber = function(o) {
          return '[object Number]' == {}.toString.call(o) && isFinite(o);
      }