曾经,浏览器类型嗅探技术被戏称为javascript程序员的“股票交易”。倘若我们知道一些能够在IE5中生效但却无法在Netscape4运行的技术,我们首先做浏览器类型嗅探,然后根据不同的浏览器的兼容性来写代码。比如:
if(navigator.userAgent.indexOf('MSIE 5') != -1)
{
//we think this browser is IE5
}
然而,各个浏览器厂家竞争日益激烈,他们在user-agent字段里增添额外的值,来确保该浏览器为该厂家拥有所有权。这是Mac版本Safari5的user-agent:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.59.10 (KHTML, like Gecko) Version/5.1.9 Safari/534.59.10
这个字符串可以匹配“safari”、“webkit”与“KHTML”(Webkit构建基础——Konqueror codebase),但是它也能够匹配到“Gecko”(firefox的渲染引擎),还有“Mozilla”(由于历史原因,几乎所有浏览器都声明为Mozilla)
增加这些值的目的是为了规避浏览器嗅探技术。假如一个脚本声明只有firefox能够处理一个特别的功能,它也有可能在其他浏览器如safari能够运行。另外不要忘记用户自己也可以改变user-agent——比如我把我的浏览器设置成“ Googlebot/1.0”,所以我可以访问站长认为仅仅搜索引擎爬虫可以访问到的内容!
因此,一段时间之后,这种浏览器嗅探技术成为一种无法实现的困扰,已经大大地失去了作用,另一种更好的方法闪亮登场——特性检测。
特性检测简单地检查我们需要用到的特性。举个栗子,如果我们需要getBoundingClientRect(获取元素相对浏览器可见视窗的位置),那么要做的重要事情就是浏览器是否支持这个属性;所以。我们不应该检测浏览器类型,而是做特性本身的检测:
if(typeof document.documentElement.getBoundingClientRect != "undefined")
{
//the browser supports this function
}
不支持该特性功能的浏览器会返回“undefined”类型,所以不会进入条件。即使不通过我们在指定的浏览器测试这脚本,我们也知道它要么运行成功,要么静默失败。
或许我们还可以做...
但是,事实上——特性检测也不是完全可依赖的——他们有时候也会失败。让我们来看一些例子,来了解我们如何处理每个事例。
ActiveX object
或许,做特性检测失败最广为认知的事例是,在IE中检测ActiveXObject来做ajax请求。
ActiveX 属于晚绑定对象,具体意思就是在尝试用ActiveX之前,你是无法确定
浏览器是否支持这个特性。所以,如果用户禁止了ActiveX,下面的代码会抛出错误:
if(typeof window.ActiveXObject != "undefined")
{
var request = new ActiveXObject("Microsoft.XMLHTTP");
}
为了解决这个问题,我们需要做异常处理——根据不同情况尝试实例化对象,捕捉失败:
if(typeof window.ActiveXObject != "undefined")
{
try
{
var request = new ActiveXObject("Microsoft.XMLHTTP");
}
catch(ex)
{
request = null;
}
if(request !== null)
{
//... we have a request object
}
}
HTML属性( attributes )与DOM属性的映射(properties)
属性映射经常用来测试浏览器对HTML5 API属性的支持情况。比如,通过寻找draggable属性,检查带有[draggable="true"]的元素支持** Drag and Drop API**:
if("draggable" in element)
{
//the browser supports drag and drop
}
问题是,IE8或者更早的版本,自动建立了所有HTML属性到DOM属性的映射关系。这就是getAttribute在老版本IE是如此糟糕的原因,因为它根本不返回HTML属性,而是返回一个DOM 属性。
这意味着,如果我们用已经存在属性的元素:
<div draggable="true"> ... </div>
这在IE8或更早的版本中,进行draggable检测会返回true,即使这些浏览器并不支持draggable属性。
HTML属性(attribute)可以是任意的:
<div nonsense="true"> ... </div>
在IE8或更早的版本,结果可想而知,返回为true
"nonsense" in element
解决方案,检测没有属性(attribute)的元素,最安全的方法就是创建新的元素:
if("draggable" in document.createElement("div"))
{
//the browser really supports drag and drop
}
对用户行为的判断
你可能遇到过这样的代码用来检测可触控设备:
if("ontouchstart" in window)
{
//this is a touch device
}
大多数触控设备在触发click事件之前故意设置了一个延迟(通常接近300ms),以便于元素能够触发double-tapped而不是进行点击。但是这样会使应用感到反应迟缓或者无响应。所以,开发者有时采用特性检测来管理不同事件:
if("ontouchstart" in window)
{
element.addEventListener("touchstart", doSomething);
}
else
{
element.addEventListener("click", doSomething);
}
然而,这个条件处理来自于一个错误的判断——因为设备只要支持touch,touch事件就会被采用。但是触控屏幕的笔记本该怎么办?用户可能触控屏幕,或者可能用鼠标或触控板。上边的代码无法处理这种情况,所以用鼠标点击根本没有作用。
解决方法就是根本不用检查浏览器对事件类型的支持——取而代之,绑定这些事件包括touch与click,然后采用preventDefault来阻止touch来产生click事件
element.addEventListener("touchstart", function(e)
{
doSomething();
e.preventDefault();
}, false);
element.addEventListener("click", function()
{
doSomething();
}, false);
检测特性失效带来的灾难
这是可以容忍的痛苦事情,但是有时候这不是我们需要的特性的问题——某些浏览器声明支持一些特性,但实际却不能生效。最近的例子就是Opera12中的setDragImage()(可拖拽对象的方法dataTransfer)
这里特性检测失败就是因为Opera12仅是对外声明支持,却无法真正的实现;即使异常处理也不会有作用,因为它不会抛出任何错误。它仅仅是不能运行:
//Opera 12 passes this condition, but the function does nothing
if("setDragImage" in e.dataTransfer)
{
e.dataTransfer.setDragImage("ghost.png", -10, -10);
}
或者,某个浏览器开发了某个特性,但是却有BUG。
面对上述问题,我们不得不思考:什么才是检测浏览器最安全的方法?
我有两点建议:
1. 采用专有对象(proprietary object)测试 优先于 navigator信息
2. 采用排除法,即不包括该特性的浏览器来做特别处理,而不是囊括具有某特性的所有浏览器来做特别处理
比如: opera12或者更早的版本可以检测到window.opera对象,所以我们可以这样检测除了opera之外的其他浏览器对draggable的支持情况:
if(!window.opera && ("draggable" in document.createElement("div")))
{
//the browser supports drag and drop but is not Opera 12
}
更好的方式,我们加上浏览器专有对象的检测:
if(window.opera)
{
//Opera 12 or earlier, but not Opera 15 or later
}
if(document.uniqueID)
{
//any version of Internet Explorer
}
if(window.InstallTrigger)
{
//any version of Firefox
}
专有对象可以采用组合的方式:
if(document.uniqueID && window.JSON)
{
//IE with JSON (which is IE8 or later)
}
if(document.uniqueID && !window.Intl)
{
//IE without the Internationalization API (which is IE10 or earlier)
}
我们已经提过userAgent字符串是不可依赖的,但是vendor字段实际上还是可判断的,可以用来检测是chrome还是safari:
if(navigator.vendor == 'Google Inc.')
{
//any version of Chrome
}
if(navigator.vendor == 'Apple Computer, Inc.')
{
//any version of Safari (including iOS builds)
}
选择测试语法
之前,我喜欢测试检查对象与特性的各种写法。比如,下面的写法是最近比较常用的:
if("foo" in bar)
{
}
过去我们不能采用这种写法是因为IE5时代的浏览器会抛出语法错误;但是现在我们不在考虑这个问题,因为我们不需要在支持这些浏览器。
在本质上,它与下面的写法是等价的:
if(typeof bar.foo != "undefined")
{
}
因为JS可以进行隐式转换,所以我们还可以这样写:
if(foo.bar)
{
}
但这种写法也会有潜在的问题:foo.bar 为空字符串或者布尔值false或者数值0的时候,结果就与我们的预期有差距。举个例子,*style.maxWidth * 属性有时会被除了IE6的浏览器运用,我们应该这样检测:
if(typeof document.documentElement.style.maxWidth != "undefined")
{
}
maxWidth属性只会在浏览器支持并且作者设定了该值转换为true,所以我们这样写检测就会失败:
if(document.documentElement.style.maxWidth)
{
}
总结一下主要规则 : 自动类型转换相对对象与函数是安全可信赖的,但是针对字符串与数值却不值得信赖。
既然已经说到这:如果你能够安全利用它,那就去做吧。因为它在现代浏览器通常比较快。
更多内容,请关注:Automatic Type Conversion In The Real World.
原文地址: http://www.sitepoint.com/javascript-feature-detection-fails/
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。