1 | // 使用 instanceof 判断 |
这3种方法都能判断变量是否是函数,但也存在一些的区别和问题,下面来具体看看。
使用 instanceof 判断
instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。
1 | new Date instanceof Date; // true |
在开头的 isFun 函数中,也就是检测 Object.getPrototypeOf(v) === Function.prototype。
然而在浏览器中,我们的代码可能需要在多个窗口之间进行交互。多个窗口意味着多个全局环境,不同的全局环境拥有不同的全局对象,从而拥有不同的内置类型构造函数。这可能会引发一些问题。比如:
1 | let iFrame = document.createElement('IFRAME'); |
所以使用 instanceof 来判断变量是否是函数在这种场景下并不可靠。
使用 typeof 判断
typeof 操作符返回一个字符串,表示未经计算的操作数的类型。
| Type of val | Result |
|---|---|
| Undefined | ‘undefined’ |
| Null | ‘object’ |
| Boolean | ‘boolean’ |
| Number | ‘number’ |
| String | ‘string’ |
| Object(native and not callable) | ‘object’ |
| Object(native or host and callable) | ‘function’ |
值得注意的是,在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 'object'。
在 ECMAScript 2015 之前,typeof 总能保证对任何所给的操作数返回一个字符串。即便是没有声明的标识符,typeof 也能返回 'undefined'。使用 typeof 永远不会抛出错误。
但在加入了块级作用域的 let 和 const 之后,在其被声明之前对块中的 let 和 const 变量使用 typeof 会抛出一个 ReferenceError。块作用域变量在块的头部处于“暂存死区”,直至其被初始化,在这期间,访问变量将会引发错误。
1 | typeof undeclaredVariable === 'undefined'; |
回到一开始的问题,在 2018 年的现在,使用 typeof 判断变量是否是函数,并没有什么问题。然而我们如果看一下有不短历史的开源仓库的代码,判断变量是否是函数很少会使用 typeof 来实现的。究其原因,实际上是因为在一些很久之前的浏览器版本,判断正则表达式的时候,typeof 会返回 'function',比如 PhantomJS v1 和 Chrome 1-12。所以,如果你不必兼容这些老版本的浏览器,可以放心使用 typeof。
使用 toString 判断
使用 Object.prototype.toString 来判断变量是否是函数是兼容性最好的,唯一的问题是,可读性比较差,不太好理解。
要理解为什么 Object.prototype.toString 能判断变量是否是函数,首先要知道的是这个方法的定义是什么。然而需要注意的是,Object.prototype.toString 这个方法的定义在 ES5 spec 和 ES6 spec 中是有些不一样的。
以前在深入理解函数对象中提到过 Object 是一个属性的集合,每个对象对象有着对应的 internal slot 和 internal method,而在 ES5 spec 中, [[Class]] 就是对象里面的一个 internal slot (ES spec 一般用双方括号 [[]] 来标识内置属性,比如 [[Get]] 、[[Set]] 、 [[Prototype]] 、 [[Extensible]] 等)。而根据 ES5 sepc,[[CLass]] 的定义是:A String value indicating a specification defined classification of objects. 也就是每个内建的对象都会有一个对应的不可修改的 [[Class]] 属性来表明该对象属于哪种类型的对象。
那么再看下 Object.prototype.toString 在 ES5 spec 的定义:
- If the this value is undefined, return “[object Undefined]”.
- If the this value is null, return “[object Null]”.
- Let O be the result of calling ToObject passing the this value as the argument.
- Let class be the value of the [[Class]] internal property of O.
- Return the String value that is the result of concatenating the three Strings “[object “, class, and “]”.
简单来说,对象默认的 toString 方法就是返回 `object ${[[Class]]}` 这样格式的字符串。
而需要注意的是,内建对象的 Object.prototype.toString 方法基本上都会被覆盖:
1 | [1,2,3].toString() // '1, 2, 3' |
所以我们必须用 call 方法来调用 Object.prototype.toString:
1 | Object.prototype.toString.call([1,2,3]) // '[object Array]' |
所以判断变量是否是函数就可以这么实现:
1 | function isFun3(v) { |
而到了 ES6 spec,[[Class]] 内置属性被去掉了,取而代之的是一个名为 @@toStringTag 的 Symbol。它的具体定义是 A String valued property that is used in the creation of the default string description of an object. Accessed by the built-in method Object.prototype.toString.
在 ES6 spec 中,Object.prototype.toString 的定义也变成了:
- If the this value is undefined, return “[object Undefined]”.
- If the this value is null, return “[object Null]”.
- Let O be ToObject(this value).
- Let isArray be IsArray(O).
- ReturnIfAbrupt(isArray).
- If isArray is true, let builtinTag be “Array”.
- Else, if O is an exotic String object, let builtinTag be “String”.
- Else, if O has an [[ParameterMap]] internal slot, let builtinTag be “Arguments”.
- Else, if O has a [[Call]] internal method, let builtinTag be “Function”.
- Else, if O has an [[ErrorData]] internal slot, let builtinTag be “Error”.
- Else, if O has a [[BooleanData]] internal slot, let builtinTag be “Boolean”.
- Else, if O has a [[NumberData]] internal slot, let builtinTag be “Number”.
- Else, if O has a [[DateValue]] internal slot, let builtinTag be “Date”.
- Else, if O has a [[RegExpMatcher]] internal slot, let builtinTag be “RegExp”.
- Else, let builtinTag be “Object”.
- Let tag be Get (O, @@toStringTag).
- ReturnIfAbrupt(tag).
- If Type(tag) is not String, let tag be builtinTag.
- Return the String that is the result of concatenating “[object “, tag, and “]”.
注意 16. Let tag be Get (O, @@toStringTag) ,在 ES5 的时候,自定义的对象调用 Object.prototype.toString 的时候只能返回内置的默认类型:
1 | class ValidatorClass {} |
而在 ES6,自定义对象的 Object.prototype.toString 的返回结果变得可修改了:
1 | class ValidatorClass { |
不过从判断变量是否是函数这个问题上看,ES5 和 ES6 返回的结果都是一样的,所以可以尽情使用 Object.prototype.toString.call(v) === '[object Function]' 来实现。
Ref