篇首语
这篇文章算是对《JavaScript 函数的特性与原型链讲解》 的一个补充,如果你还没有看过那篇文章,可以先大致浏览一下。
写这篇文章的原因是分享完《JavaScript 函数的特性与原型链讲解》后,被问到为什么 typeof 一个函数会返回 "function"。这次就给大家讲解一下 typeof 是如何看待函数的,希望能给大家带来帮助,同时也作为上篇文章的补充。
回顾 JS 的数据类型
我们先来回顾一下最新的 ECMAScript 标准的 8 种数据类型:
7 种原始类型(或称作基本类型):Boolean、Null、Undefined、Number、BigInt、String、Symbol 和 Object。
基本类型(基本数值、基本数据类型)是一种既非对象也无方法的数据。在 JavaScript 中,共有7种基本类型:string,number,bigint,boolean,null,undefined,symbol (ECMAScript 2016新增)。
多数情况下,基本类型直接代表了最底层的语言实现。
所有基本类型的值都是不可改变的。但需要注意的是,基本类型本身和一个赋值为基本类型的变量的区别。变量会被赋予一个新值,而原值不能像数组、对象以及函数那样被改变。
何谓不可改变
var bar = "baz";
bar[0] = "a";
console.log(bar[0]); // "b"
console.log(bar);// "baz"
基本类型的包装对象
除了 null 和 undefined 外的基本类型都有其对应的包装对象:
- String 为字符串基本类型。
- Number 为数值基本类型。
- BigInt 为大整数基本类型。
- Boolean 为布尔基本类型。
- Symbol 为字面量基本类型。
这个包裹对象的 valueOf() 方法返回基本类型值。
var s_prim = "foo";
var s_obj = new String(s_prim);
console.log(typeof s_prim); // "string"
console.log(typeof s_obj); // "object"
console.log(typeof s_obj.valueOf()); // "string"
typeof 可能的返回值表
下表总结了 typeof 可能的返回值:
类型 | 结果 |
---|---|
Undefined | "undefined" |
Null | "object" (见下文) |
Boolean | "boolean" |
Number | "number" |
BigInt | "bigint" |
String | "string" |
Symbol (ECMAScript 2015 新增) | "symbol" |
宿主对象(由 JS 环境提供) | 取决于具体实现 |
Function 对象 (按照 ECMA-262 规范实现 [[Call]]) | "function" |
其他任何对象 | "object" |
分析 typeof 的误区
我之前在分析 typeof 函数和对象的时候,是根据这种方式来分析的:
以前面的 String 为例:
var s_obj = new String(s_prim);
console.log(typeof String); // "function"
// 使用 typeof 调用函数的时候,是把函数当做函数对象来调用的,又因为所有的函数对象都是由 Function 构造出来的,所以:String.__Proto__ === Function.prototype,所以结果返回了 "function"
console.log(typeof s_obj); // "object"
// 1. 因为 s_obj 是一个对象,它是由 String 构造出来的,所以:S_obj.__proto__ === String.prototype;
// 2. 又因为所有的构造函数都是由 Object 派生出来的,所以:String.prototype.__proto__ === Object.prototype;
// 3. 所以 s_obj 返回 "object";
如果你看过我的这边文章《JavaScript 函数的特性与原型链讲解》 那么应该能理解我为什么会这么分析,因为原型链是这样的。
但是问题点出在,typeof 是提前把函数的原型链终止了吗?
// 因为
Function.prototype.__proto__ === Object.prototype;
这么看来,如果不提前终止原型链,那么 typeof 的结果应该返回 "object" 才对啊。
一小段 JS 历史
带着这个问题,我们来看一点 JavaScript 的历史。
大家都知道 "typeof null" 的 bug,它是在 JavaScript 的第一版就存在的。在这个版本中,值以 32 位的单位存储,包括一个小型类型标记(1-3 位)和值的实际数据。类型标记存储在单元的较低位上。一共有 5 种类型:
- 000: object,表示这个数据是一个对象的引用。
- 1: int,表示这个数据是一个 31 位的有符号整型。
- 010: double,表示这个数据是一个双精度浮点数的引用。
- 100: string,表示这个数据是一个字符串的引用。
- 110: boolean,表示这个数据是一个布尔值。
两个值比较特殊:
-
undefined (JSVAL_VOID) 的值是 −2^30 整型(一个超出整型范围的数)。
-
null (JSVAL_NULL) 是机器码空指针。或者是:一个对象类型标记加上一个为零的引用。详情参考下面的源码。
现在应该很清楚为什么 typeof 认为 null 是一个对象了:它检查了类型标记和类型标记表示的对象。
mozilla 的 typeof 的源码
回顾完第一版的历史,我们再来看一下 1998 年 mozilla 的 typeof 的源码(对源码不感兴趣的可以跳过不用看,我会在源码后面再附上一遍结论):
// https://dxr.mozilla.org/classic/source/js/src/jspubtd.h#52
/* Result of typeof operator enumeration. */
typedef enum JSType {
JSTYPE_VOID, /* undefined */
JSTYPE_OBJECT, /* object */
JSTYPE_FUNCTION, /* function */
JSTYPE_STRING, /* string */
JSTYPE_NUMBER, /* number */
JSTYPE_BOOLEAN, /* boolean */
JSTYPE_LIMIT
} JSType;
// https://dxr.mozilla.org/classic/source/js/src/jsapi.h
#define JS_BIT(n) ((JSUint32)1 << (n))
#define JS_BITMASK(n) (JS_BIT(n) - 1)
#define JSVAL_OBJECT 0x0 /* untagged reference to object */
#define JSVAL_INT 0x1 /* tagged 31-bit integer value */
#define JSVAL_TAGBITS 3
#define JSVAL_TAGMASK JS_BITMASK(JSVAL_TAGBITS)
#define JSVAL_TAG(v) ((v) & JSVAL_TAGMASK)
#define JSVAL_IS_OBJECT(v) (JSVAL_TAG(v) == JSVAL_OBJECT)
#define JSVAL_IS_NULL(v) ((v) == JSVAL_NULL)
#define JSVAL_IS_VOID(v) ((v) == JSVAL_VOID)
#define OBJECT_TO_JSVAL(obj) ((jsval)(obj))
#define JSVAL_NULL OBJECT_TO_JSVAL(0)
#define JSVAL_VOID INT_TO_JSVAL(0 - JSVAL_INT_POW2(30))
#define INT_TO_JSVAL(i) (((jsval)(i) << 1) | JSVAL_INT)
#define JSVAL_INT_POW2(n) ((jsval)1 << (n))
// https://dxr.mozilla.org/classic/source/js/src/jsfun.c#1172
JSClass js_FunctionClass = {
"Function",
JSCLASS_HAS_PRIVATE | JSCLASS_NEW_RESOLVE,
JS_PropertyStub, fun_delProperty,
fun_getProperty, fun_setProperty,
fun_enumProperty, (JSResolveOp)fun_resolve,
fun_convert, fun_finalize,
NULL, NULL,
NULL, NULL,
fun_xdrObject, fun_hasInstance
};
// 这是 typeof 源码部分
// https://dxr.mozilla.org/classic/source/js/src/jsapi.c#333
JS_PUBLIC_API(JSType)
JS_TypeOfValue(JSContext *cx, jsval v)
{
JSType type = JSTYPE_VOID;
JSObject *obj;
JSObjectOps *ops;
JSClass *clasp;
CHECK_REQUEST(cx);
// 判断是否是 undefined,判断步骤为:
// 1、判断 ((v) == JSVAL_VOID)
// 2、JSVAL_VOID 是 INT_TO_JSVAL(0 - JSVAL_INT_POW2(30))
// 3、JSVAL_INT_POW2(30) 是 ((jsval)1 << (n)),即 1 左移 30 位,相当于 2^30;所以,0 - JSVAL_INT_POW2(30) = -2^30
// 4、INT_TO_JSVAL 是 (((jsval)(i) << 1) | JSVAL_INT) 就转成了:-2147483647 即 -2^31 这个值。
// 5、所以如果传入的一个值等于 JSVAL_VOID,那这个值就代表了 undefined。
// PS:这段分析如果有问题请帮我指出
if (JSVAL_IS_VOID(v)) {
type = JSTYPE_VOID;
}
// 根据类型标记判断是否是对象
// JSVAL_IS_OBJECT(v) 判断的步骤为:
// 1、(JSVAL_TAG(v) == JSVAL_OBJECT) 即:标记类型 == JSVAL_OBJECT,而 JSVAL_OBJECT = 0x0,就转成了(JSVAL_TAG(v) == 0x0)
// 2、JSVAL_TAG(v)是:((v) & JSVAL_TAGMASK);
// 3、JSVAL_TAGMASK 是:JS_BITMASK(JSVAL_TAGBITS);
// 4、JSVAL_TAGBITS = 3;
// 5、JS_BITMASK(n)是:(JS_BIT(n) - 1)
// 6、JS_BIT(n) 是:((JSUint32)1 << (n));
// 7、这样就判断出来是否是一个对象了
// 8、那么如何判断 null 呢:
// 9、因为:JSVAL_NULL 是:OBJECT_TO_JSVAL(0);OBJECT_TO_JSVAL(obj) 是:((jsval)(obj))
// 10、通过 JSVAL_TAG(JSVAL_NULL) 的计算得到的是 0x0,跟 JSVAL_OBJECT 一致,所以 null 就得到的是一个对象类型了
else if (JSVAL_IS_OBJECT(v)) {
// JSVAL_TO_OBJECT(v)是:((JSObject *)JSVAL_TO_GCTHING(v)),即把传入的值转换成了内部的 JSObject 结构的类型。
obj = JSVAL_TO_OBJECT(v);
// 这一串判断很复杂,我只能分析个大概:
// 首先判断是否是 js_ObjectOps,如果是,那么把它转成 JSClass,再看里面是否有 call 属性,
// 如果没有那么再看看是否是 js_FunctionClass;如果不是,那么再看 call 是否不为0;
// 综上判断:如果对象有 call 或者 FunctionClass 或者 call 不为 0,那么就返回 "function",其它情况都返回 "object"
if (obj &&
(ops = obj->map->ops,
ops == &js_ObjectOps
? (clasp = OBJ_GET_CLASS(cx, obj),
clasp->call || clasp == &js_FunctionClass)
: ops->call != 0)) {
type = JSTYPE_FUNCTION;
} else {
type = JSTYPE_OBJECT;
}
} else if (JSVAL_IS_NUMBER(v)) {
type = JSTYPE_NUMBER;
} else if (JSVAL_IS_STRING(v)) {
type = JSTYPE_STRING;
} else if (JSVAL_IS_BOOLEAN(v)) {
type = JSTYPE_BOOLEAN;
}
return type;
}
小结:
1、typeof null 返回 "object",是因为 JS 内部把值的低位为 0x0 的标记定义成对象类型,而 null 被计算成了一个低位具有对象类型标记的值,所以就返回了 "object";
2、typeof 返回 "function",只要一个对象有一个不为 0 的 call 属性/方法,或者是 js_FunctionClass 类型,也就是这个对象里面有 "Function" 标记,那么就返回 "function",其它情况都返回 "object";
ES6 是如何解释的
我们再来看一下现在 ES6 的 typeof 是如何对待函数和对象类型的:
- 如果一个对象(Object)没有实现 [[Call]] 内部方法,那么它就返回 object
- 如果一个对象(Object)实现了 [[Call]] 内部方法,那么它就返回 function
这跟源码中是否存在 call 很类似。
[[Call]] 是什么
执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是一个 this 值和一个包含调用表达式传递给函数的参数的列表。实现此内部方法的对象是可调用的。
这是翻译的原话,简单点说,一个对象如果支持了内部的 [[Call]] 方法,那么它就可以被调用,就变成了函数,所以叫做函数对象。
相应地,如果一个函数对象支持了内部的 [[Construct]] 方法,那么它就可以使用 new 或 super 来调用,这时我们就可以把这个函数对象称为:构造函数。
typeof 原理就大致分析到这里了,我对函数对象和构造函数又有了更清晰的认识,跟各位一起学习进步,如果文章中有不对的地方还请指正。
参考资料:
- developer.mozilla.org/zh-CN/docs/…
- developer.mozilla.org/zh-CN/docs/…
- dxr.mozilla.org/classic/sou…
- 2ality.com/2013/10/typ…
- www.ecma-international.org/ecma-262/6.…
- www.ecma-international.org/ecma-262/6.…
更新
12-16 增加源码和分析,增加源码小结。