新版浏览器的发布速度比起早些时候可能是慢一些了,但是开发者仍然必须面对一系列令人头痛的,品牌和版本都各不相同的浏览器,这些浏览器在 JavaScript 特性的支持方面存在差异。为了对付这个问题,脚本编程人员通常提供两个或者多个代码分支,使浏览器只执行由自身支持的语句组成的程序路径。由于目前存在的浏览器版本变种如此之多,浏览器的识别--即通过考察 navigator 对象的属性来获得版本信息--在很大程度上变得不可管理。本文将展示针对这个问题的解决方案之一--对象检测技术--的细节,这个方案可以把 JavaScript 的开发人员从绝大部分判别浏览器版本的泥潭中解放出来。
浏览器识别方法的软肋
为了特定品牌以及/或者其不同版本(也可能是不同操作系统平台)的浏览器设定一个代码分支是基于如下的基本假设:即版本N的X浏览器实现了一套已知的核心编程脚本语言和对象模型。脚本编程人员经常对这个假设加以扩展:即版本N及其之后版本的浏览器都会支持某个特定的功能。遗憾的是,这些假设有几个问题。
让我们首先看一下浏览器的版本号。navigator.appVersion 属性可以返回一个字符串,该字符串的开头部分是一个和 Mozilla 的版本相关联的数字。在早期的图形浏览器时代,绝大多数的商用浏览器都是在伊利诺伊大学(University of Illinois)的 NCSA Mosaic 浏览器引擎的基础上构建的。这些浏览器都会相当明确地告诉服务器(通过每一个服务器文件请求),它们是属于 Mozilla 家族的(不要和现代的 Mozilla.org 浏览器相混淆)。然而对于脚本编程人员来说,有一个不幸的事,即随着时间的推移,Mozilla 家族的版本号和实际浏览器产品的版本号之间的关联关系被弱化了,IE5 说它是 Mozilla 4.0;而 Netscape 6 则认为自己是 Mozilla 5.0。在这些时候,脚本必须解析包含更多细节信息的 navigator.userAgent 或者 navigator.appVersion 属性的值,才能精确地知道当前运行的产品的版本号。Netscape 6 中包含一个新的 navigator 对象属性,用于报告浏览器的产品版本号,然而您必须在新的代码分支中访问这些属性。总之,脚本编程者需要进行很多字符串的解析,才能产生一个指示浏览器版本的全局变量。
其次,您会碰到一个版本“大于等于”另一个版本,或者一个版本“等于”另一个版本的难题。在脚本编程者满怀热情地区别 Netscape 和 IE 的版本4浏览器的初始阶段,一些脚本程序只是简单的检查浏览器的版本号(先不考虑如何进行这个版本号的抽取)是否等于4。然而在 IE5 出现之后,这些脚本就不能使用 IE4 的特性了,因为5不等于4。反过来,那些自认为聪明的,在书写 Netscape 4 识别代码时使之可以接受版本4或者更新版本的人们,在某些时候也会陷入了困境,即当 NN6 出现的时候,那些依赖于 NN4 层的代码就会出现问题。
一方面,坚持采用“等于4”的人们无法考虑当前浏览器的问题。而另一方面,坚持“大于等于4”的人们又会碰到虽然很少出现的,但又明显不是不可能的情况,因为浏览器厂商在推动人们采用 W3C DOM 标准的过程中,有时需要使对象模型特性(层对象)产生变化。虽然我们很容易预见,浏览器的后续版本肯定会出现,但是预测哪些特性在未来的浏览器中会过时或者被删除,则不是容易的事。现在回过头来可以清楚地看到,在新版的浏览器开始扣响您的网站大门时,上述这两种浏览器的识别方法都需要紧急的代码修补。
最后,大多数浏览器识别代码容易忽略除了IE和NN之外的,同样支持脚本编程的浏览器-比如 Safari! Safari 提供了杰出的 W3C DOM 的脚本编程的支持,但是一个典型的浏览器识别程序会把它归入到“dumb and dumber”的范畴,因为对于 navigator.userAgent 属性值的字符串解析并不能揭示一个浏览器品牌是否有名,或者一个版本号是否足够高。
简单的对象检测
您可以通过一个被称为对象检测的技术来解决很多这样的问题。这个技术泛指基于当前的浏览器是否支持期望的对象,属性,或者方法来创建脚本执行分支的手法。
您可能已经看到对象检测技术在起作用了,只是没有认识到这种方法的强大功能。Netscape Navigator 2(所有OS版本)以及 IE3/Windows 都不支持用脚本编程来产生图像滚动效果,因为在 DOM中,IMG 元素并不被看成对象。但是后来的浏览器实现了 image 对象,并把一个页面上的所有 IMG 元素作为属于 document 对象的 image 对象数组公布出来。因此如果浏览器支持 image 对象,则 document 对象就有一个名为 images 的数组(集合),包含页面上的所有 image 对象。如果把对 image 对象进行操作的程序块包含在测试 document.images 数组是否存在的条件下面,早期版本的浏览器就可以简单绕过那些语句,不发生任何错误:
if (document.images) {
// image object manipulations here
}
这个语法是可以正常工作的,因为在不支持 image 对象的浏览器中,document.images 表达式的值是 undefined(没有定义)。而对于值为 undefined 的表达式,if 结构就将它等同于 false 来处理。
请进一步注意一下,这个无可非议的简单例子程序相当优雅。与鼠标相关的事件处理器在所有支持脚本编程的浏览器中都是可以工作的,但是只有那些把 images 作为对象进行操作的浏览器才会尝试访问这些对象。这是一个优秀的例子,展示了如何使用脚本编程来为那些支持特定功能的浏览器上的页面增加额外的价值。
检测属性是否被支持
现在,随着对象模型变得越来越复杂,根据对象属性和/或者对象方法是否被支持来把代码进行条件分支的做法并不少见。举例来说,在 IE4 以及之后的浏览器上,document.body 对象包含一个名为 scrollTop 的属性,用来确定鼠标事件发生时,鼠标y轴在页面上的位置(不仅指页面的可视部分)。为了确保浏览器对 scrollTop 属性的支持,您可能需要使用下面的结构:
// maybe headed for trouble
if (document.body.scrollTop) {
// statements that work with scrollTop property
}
然而问题(脚本错误)还是出现了,出现在当这个if结构运行在不支持 document.body 对象的浏览器上时(比如 NN4 或者 IE3)。在对条件表达式进行求值时,这个“包含两个点”的表达式会引起脚本错误。为了避免这种错误,表达式必须首先测试 document.body 对象是否存在,然后再测试其属性是否存在:
if (document.body && document.body.scrollTop) {
// statements that work with scrollTop property
}
这行代码是可以正确工作的,因为当一个条件表达式中包含&& (AND)操作符时,最左边的表达式发生错误,将会导致整个表达式错误。因此,如果 document.body 不被支持(也就是说,它的值是 undefined),则整个条件表达式的值就等同于 false(假),而不必再尝试对第二个表达式进行求值。
检测方法是否被支持
页面中的函数也把自己公布为文档对象模型中的对象(其类型是 function)。类似地,对象方法也把自己公布为一个对象(在 IE4+/Windows 和 IE5/Mac; 上,其类型为 object;而在 NN3+ 上,其类型为 function)。
由于对象方法(方法名中不能有圆括号)在求值时会返回一个值,所以您可以很轻松地测试某个对象方法是否被浏览器支持。您可以在条件表达式中引用一个对象方法,就好象引用对象属性一样。举例来说,假定您想看看浏览器是否支持 document.getElementById() 方法,则可以用下面的结构来进行测试:
if (document.getElementById) {
// supports the method -- go for it!
}
这种验证方法只在几个早期的浏览器上会不起作用,特别值得一提的是在 NN2,IE3/Windows,以及版本5之前的 IE/Mac,问题出现在当被测试的对象方法真的被(不支持方法检测的)浏览器支持的时候:在条件表达式中测试一个现有的方法是否存在, IE 上会导致脚本错误,而在 NN2 上则会导致求值错误。支持方法检测的浏览器的最小公分母是那些以某种方式支持 W3C DOM 的浏览器集合。换句话说,为了最大程度的兼容性,您应该对那些属于 W3C DOM 的方法进行测试。因为对于不同的 W3C DOM 对象和方法的支持在不同版本的浏览器中变化比较大,这个测试可以帮助脚本优雅地绕过潜在的雷区。