打印

SD9028:ECMAScript 的数据类型运算符 typeof 在各浏览器中存在兼容性问题

作者:钱宝坤

标准参考

“typeof” 为 JavaScript 中用来实现数据类型判断的一元运算符,一元运算符还包括: "delete" "++" "--" "+" "-" "~" "!" 等。

ECMAScript5.0 规范中对 'typeof val' 表达式的执行步骤如下:

  1. 计算一元表达式的值;
  2. 如果步骤一的计算结果为引用类型:
    1. 如果 IsUnresolvableReference(val)1 的执行结果 true ,则返回最终结果 “undefined”,表达式计算结束;
    2. 执行 GetValue(val) 2
  3. 根据下表返回最终结果:
typeof Operator Results
Type of val Result
Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Object (native and does not implement [[Call]]) "object"
Object (native or host and does implement [[Call]]) "function"
Object (host and does not implement [[Call]]) Implementation-defined except may not be "undefined", "boolean", "number", or "string".

注1:IsUnresolvableReference(val) 函数指的是 JavaScript 引擎内核应实现的处理方法,而不是 JavaScript·语法层面的内置函数或方法。
·参考资料:ECMAScript 5.0中的 8.7 The Reference Specification Type ,如果 val 的值没有被定义则返回 true,否则返回 false;

注2:GetValue (val),指的是 JavaScript 引擎内核应实现的处理方法,而不是 JavaScript·语法层面的内置函数或方法。
参考资料:ECMAScript 5.0中的 8.7.1 GetValue (V)

  • 如果 val 不是引用类型,返回 val;
  • 调用 getBase(val) 方法,返回 val 的基本对象类型;
  • 如果步骤二返回 null,抛出 ReferenceError 异常;
  • 调用步骤二返回值的 [Get] 方法,通过 GetPropertyName(val) 求原型名称;
  • 返回第四步的值。

typeof 运算符返回代表被检测对象类型的字符串值。有关 typeof 的参考资料请查阅:ECMAScript 5.0 中的 11.4.3 The typeof Operator ECMAScript 3.0中的 11.4.3 The typeof Operator

问题描述

typeof 运算符存在兼容性问题,例如:

  • " typeof document.getElementById " 的结果在 IE 中为 “object”,在其他浏览器中为 “function”;
  • " typeof new RegExp() " 或 " typeof /\w/ " 语句对正则表达式实例对象的识别结果在 Chrome Safari 中为 "function" 而不是 "object"。

造成的影响

typeof 运算符在各浏览器之间存在兼容性问题,这将造成依赖此运算符获得的数据在不同浏览器中可能不同从而影响后续代码的执行。

受影响的浏览器

IE6 IE7 IE8  

问题分析

JavaScript 的本地对象里有共六种基本数据类型,其中有五种原始类型:Number String Boolean Null Undefined,其它的都是复合数据类型 Object。对象是一种复合数据类型,是一个无序的属性集合,每个属性都有自己的名字和值。ECMAScript 可以识别的对象包括:本地对象(native Object)和宿主对象(host Object),以及一个本地对象的子分类——内置对象(ECMAScript 5.0 4.3.7 Built-in Object)。本地对象属于 JavaScript 语言范围,而宿主对象由宿主环境提供,例如,document 对象和 DOM 节点。

分析并运行一下代码:

<div id="info"></div>
window.onload = function(){
  var a;
  document.getElementById("info").innerHTML = "typeof undefined == " + (typeof undefined) +
  "<br/> typeof a === " + (typeof a)+
  "<br/> typeof 1 === " + (typeof 1) +
  "<br/> typeof \"hello\" === " + (typeof "hello") +
  "<br/> typeof true === " + (typeof true) +
  "<br/> typeof null === " + (typeof null) +
  "<br/> typeof new Date() === " + (typeof new Date()) +
  "<br/> typeof document === " + (typeof document) +
  "<br/> typeof (new Date()).getDate === " + (typeof (new Date()).getDate) +
  "<br/> typeof parseInt === " + (typeof parseInt) +
  "<br/><span style=\"color:red\"< typeof NodeList === " + (typeof document.getElementsByTagName("*")) +"</span<" +
  "<br/><span style=\"color:red\"> typeof new RegExp === " + (typeof new RegExp())+"</span>" +
  "<br/><span style=\"color:red\"> typeof window.open === " + (typeof window.open)+"</span>" +
  "<br/><span style=\"color:red\"> typeof document.write === " + (typeof document.write)+"<span>"+
  "<br/><span style=\"color:red\"> typeof document.getElementById === " + (typeof document.getElementById)+"</span>";
}

在各浏览器中的运行结果如下:

IE6 IE7 IE8 FireFox Chrome Opera Safari
typeof undefined === "undefined"
typeof a === "undefined"
typeof 1 === "number"
typeof "hello" === "string"
typeof true === "boolean"
typeof null === "object"
typeof new Date() === "object"
typeof document === "object"
typeof (new Date()).getDate === "function"
typeof parseInt === "function"
typeof NodeList === "object"
typeof new RegExp() === "object"
typeof window.open === "object"
typeof document.write === "object"
typeof document.getElementById === "object"
typeof undefined === "undefined"
typeof a === "undefined"
typeof 1 === "number"
typeof "hello" === "string"
typeof true === "boolean"
typeof null === "object"
typeof new Date() === "object"
typeof document === "object"
typeof (new Date()).getDate === "function"
typeof parseInt === "function"
typeof NodeList === "object"
typeof new RegExp() === "function"
typeof window.open === "function"
typeof document.write === "function"
typeof document.getElementById === "function"
typeof undefined === "undefined"
typeof a === "undefined"
typeof 1 === "number"
typeof "hello" === "string"
typeof true === "boolean"
typeof null === "object"
typeof new Date() === "object"
typeof document === "object"
typeof (new Date()).getDate === "function"
typeof parseInt === "function"
typeof NodeList === "function"
typeof new RegExp() === "function"
typeof window.open === "function"
typeof document.write === "function"
typeof document.getElementById === "function"

从运行结果截图可知黑色文字部分是在各浏览器中相同的 typeof 运行结果,红色文字部分是 IE 与其他浏览器不同的。

可见在所有浏览器中 typeof 对 ECMAScript 中除去 RegExp 对象实例外,其他基本数据类型以及他们的实例识别无兼容性问题。但对 DOM 和 BOM 复合数据类型的识别存在兼容性问题,例如上例中的最后三项,它们是 BOM、DOM 接口中 window 和 document 对象的函数,不属于 ECMAScript 规定的内置类型。

首先分析 NodeList 类型识别问题:

NodeList 类型在标准 DOM 中描述为一个接口(interface),他是规范定义内容,在浏览器中是由浏览器内核语言负责实现这个对象(通常是C语言),这个实现后 NodeList 对象将被绑定到 ECMAScript 语言对象中,以便通过浏览器脚本语言访问以及操作他。

他们的绑定关系可参见:DOM-Level-2-Core Appendix E: ECMAScript Language Binding 与 DOM-Level-2-HTML Appendix D: ECMAScript Language Binding

在规范的绑定关系描述中,NodeList 对象所绑定的 ECMAScript 类型为 “object”。这说明使用 typeof 语句检测 NodeList 类型结果应为 “object” 而不是上例中的 "function"。这是 Safari 的 javaScriptCore 脚本引擎的一个实现 Bug。

【注】:虽然 Safari 中可以使用 NodeList(0) 这样的语法得到 Node ,他看起来像是个函数调用,但是 IE 中同样也支持 "括号" 语法得到指定索引值的元素,而 IE 却正确的返回的 NodeList 类型为 "function"。所以 Safari 的这种 "括号" 语法仅能看做他为了兼容 IE 而单独为 NodeList 扩展出的特殊用法,并不能说明 NodeList 是 Function。

其次分析 RegExp 对象实例的类型识别问题:

ECMAScript 5.0 typeof Operator Results 表中明确说明:原生对象或者宿主对象并且可以被执行的是 Fucntion 类型,也就是内情内部实现了[[Call]] 的对象,而原生的不能被执行的是 Object 类型(见标准参考中表格)。

一个正则表达式对象实例在 IE  (包括 IE10 预览版) 都是不能执行的,如:"(new RegExp('\\d'))(1)" 或 "(/\d/)(1)" 语句均会报错,而同样的语句在 Firefox Safari Chrome Opera 中均可执行

正则表达式对象实例可以被括号运算符符执行并能传入参数,这表明该对象实例依据 ECMAScript 规范标准,实现了内部实现了 [[Call]] 。

Chrome 和 Safari 中依据 ECMAScript typeof Operator Results 中规定,在 typeof 表达式判断时,带有 [[Call]] 实现的正则表达式对象实例中 [[Call]] 被检测到,结果输出为 "function" 字符串。

此外还可以根据源浏览器的源码实现得出旁证。

如,在 Safari 的 javaScriptCore 引擎实现内,Operations.cpp 文件实现了typeof 功能,RegExpObject.cpp 文件实现了正则表达式对象,他们关键部分如下:

// Operations.cpp
JSValue jsTypeStringForValue(CallFrame* callFrame, JSValue v)
{
    if (v.isUndefined())
        return jsNontrivialString(callFrame, "undefined");
    if (v.isBoolean())
        return jsNontrivialString(callFrame, "boolean");
    if (v.isNumber())
        return jsNontrivialString(callFrame, "number");
    if (v.isString())
        return jsNontrivialString(callFrame, "string");
    if (v.isObject()) {
        // Return "undefined" for objects that should be treated
        // as null when doing comparisons.
        if (asObject(v)->structure()->typeInfo().masqueradesAsUndefined())
            return jsNontrivialString(callFrame, "undefined");
        CallData callData;
        if (asObject(v)->getCallData(callData) != CallTypeNone)
            return jsNontrivialString(callFrame, "function");
    }
    return jsNontrivialString(callFrame, "object");
}
// RegExpObject.cpp
CallType RegExpObject::getCallData(CallData& callData)
{
    callData.native.function = callRegExpObject;
    return CallTypeHost;
}

Operations.cpp 文件中,判断如果是对象则进入分支,查找当前对象是否是伪装为 undefined 的,如果是则返回 ”undefined“ 字符串;然后判断执行 getCallData 方法后返回的值是否为非 CallTypeNone,如果判断成立则返回 "function" 字符串。

正则表达式对象的 getCallData 方法的返回值在 RegExpObject.cpp 文件中给出,是 CallTypeHost 而非 CallTypeNone,导致了判断语句的表达式结果为 true,返回 "function" 字符串。

而 Firefox Opera1 浏览器中,在为正则表达式对象实例实现 [[Call]] 后,为了不引起混淆,没有严格遵循 ECMAScript typeof Operator Results 中规定,他们将带有 [[Call]] 实现的正则表达式对象实例中的 [[Call]] 实现判断步骤修正为未实现 [[Call]] 的情况,因此结果与 IE 一致,输出为 "object" 字符串。

【注】:Chrome 的 V8 引擎对此处实现情况与 Safari 的 javaScriptCore 引擎类似,Firefox 的 SpiderMonkey 引擎与之实现相反,均不再给出具体引擎实现代码的分析,有兴趣的读者可以自行查找相关源文件查看。另外,由于 Opera 浏览器并不开源,我们无法得知这个问题在他源代码中的具体实现方法,此处对 Opera 行为的描述基于以上分析的逻辑推理,视同其与 Firefox 的实现思路一致。

然后分析 DOM 和 BOM 复合数据类型的识别问题:

在 IE6 IE7 IE8 1 浏览器中,按照 ECMAScript 规范实现的是名为 JScript 的脚本引擎,他是独立于浏览器系统之外的。JScript 调用 DOM 或 BOM 方法需要通过 windows 系统中的 COM 机制。浏览器中的 DOM 与 BOM 对象和相关必须通过第三方 COM 机制将数据转换到 JScript 脚本引擎内进行相关调用,因此原本 Function 类型的数据在 COM 机制转换后统一变成了 Object 类型。

【注】:微软在 IE9 中已经将 JScript 引擎集成到浏览器核心内,故 IE9 Beta 版本中已不存在这个问题,可参考:Exploring IE9's Enhanced DOM Capabilities

浏览器内核与 JScript 引擎关系模型如图:

IE9

而其他浏览中,脚本引擎是处于浏览器核心实现之内,DOM 的相关实现都是按照现行 DOM 规范中的 DOM 继承和与 ECMAScript 绑定关系实现的;BOM 由各自浏览器自行实现或者按照 HTML5 草案规范基于 ECMAScript 规范实现。这使得这些浏览器内 DOM BOM 与 JavaScript 通信设计上是在一套规范体系内,JavaScript 的 typeof 运算可以正确获得对应类型的计算结果。

他们的绑定关系可参见:DOM-Level-2-Core Appendix E: ECMAScript Language BindingDOM-Level-2-HTML Appendix D: ECMAScript Language Binding

解决方案

由于以上几点的各浏览器实现差异,我们建议用户在充分了解 typeof 运算符含义时再使用。

下面的代码封装了名为 realtypeof 的方法,用来消除已知的各浏览器之间原生 typeof 运算符差异,仅作参考:

function realtypeof(source){
  return (source === undefined)
    ? "undefined"
    : ("object" === typeof source)
      ? (/function/i.test( source + "" ))
        ? "function"
        : "object"
      : (source.constructor == RegExp || !(source.constructor instanceof Function))
        ? "object"
        : typeof source;
}

参见

知识库

相关问题

测试环境

操作系统版本: Windows 7 Ultimate build 7600
浏览器版本: IE6
IE7
IE8
Firefox 3.6.10
Chrome 7.0.517.8 dev
Safari 5.0.2
Opera 10.62
测试页面: typeof.html
本文更新时间: 2010-09-14

关键字

typeof object function