1. 背景
我之前一直知道 typeof null 为 object ,但是一直没深究为什么?在查阅了相关的文档,汇总为以下文档
2. 汇总解释
我们先来看 《JavaScript高级程序设计》 里面是怎么说的吧?
Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回 "object" 的原因
这里我们就可以解释通了,null 其实是一个 空对象指针 ,所以 typeof 会检测 null 为 object
如果单纯是这样必然是不对的,是 空对象指针 就一定会被检测为 object 吗?
这其实就是一句总结,里面肯定有更加深层的原因,我看了一眼 ECMAScript 的文档,发现没有说这一点(可能是我没找到),倒是在 MDN 发现了解释
在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于
null
代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null
也因此返回"object"
我来简单解释一遍,对象类型 表示为 0 ,null 在大部分机器上面都是 0x00...000 ,所以 null 为 0 ,所以 null 在 typeof 下 会被检测为 object
3. 早期解释
这个解释已经能解答很多疑惑了,如果我们再往下面看一层呢?为什么对象类型为 0 ,null 为 0x00 ,typeof 底层分析类型是通过什么手段?以及这部分源码是什么样子的?
其实 JavaScript 的数据类型其实是通过 数据标签 + 数据值 的形式存在的,下面就是常见的数据标签
数据标签 | 类型 |
---|---|
000 | object |
1 | int |
010 | double |
100 | string |
110 | boolean |
在32位机器中,int 的 标签为 1 ,占了一位,那么后续的值为 2 ^ 30 ~2 ^ 30 - 1 (根据上图推算,现代JS早就改);其中 object 的标签为 000
这就是 JavaScript 的数据类型如何去做处理和区分的,当然还有2个特殊的值
- undefined 源码标记为 JSVAL_VOID
- null 源码标记为 JSVAL_NULL :是机器代码的 NULL指针 。或者说,它是一个对象类型标记加上一个引用为零的组合,大部分机器都认定为 0x000
我们来看下面的 typeof 源码部分
- 发现没有 显式的判断 null值 的标记,只有 undefined,object,number,string,boolean 的判断
- 再加上 null 在很多的机器中默认是 0x000 ,那么就会走 JSVAl_IS_OBJECT(v) 的逻辑,被认定为 object 类型
c++
// 这是一个宏,它指定了函数的返回类型为 JSType,并且声明该函数是一个公共API
JS_PUBLIC_API(JSType)
JS_TypeOfValue(JSContext *cx, jsval v) {
JSType type = JSTYPE_VOID;
JSObject *obj;
JSObjectOps *ops;
JSClass *clasp;
// 这是一个宏,用于检查传入的 JSContext 是否有效
CHECK_REQUEST(cx);
if (JSVAL_IS_VOID(v)) {
type = JSTYPE_VOID;
} else if (JSVAL_IS_OBJECT(v)) {
// 如果 v 是对象类型,则检查它是否是函数对象,如果是函数对象则将 type 设置为 JSTYPE_FUNCTION
// 否则将其设置为 JSTYPE_OBJECT
obj = JSVAL_TO_OBJECT(v);
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;
}
这也是网上大部分的解释,因为在 JavaScript 设计初期,时间很短,所以导致一些地方没考虑到,那如今的JS引擎怎么做的呢?
4. V8解释
在 V8 Blog 对于 typeof null 为 object 的解释。我来简单解释一下:V8 Blog 认为 null 表示 no object value,所以输出 object 没问题
当然这有一股王婆卖瓜,自卖自夸的感觉了,毕竟是 JavaScript 最初设计的问题
我们先下载 V8 的源码来看看:v8/v8.git - Git at Google (googlesource.com),根据知乎大佬的提示,找到了下面的源码部分
我们来看下下面的整体流程:
-
首先,创建了一个
TNode<String>
类型的函数Typeof
,它接受一个TNode<Object>
类型的参数value
。 -
创建了一些标签,这些标签用于控制程序执行流程。例如,
return_number
表示如果是数字类型就返回数字类型的字符串,return_function
表示如果是函数类型就返回函数类型的字符串,以此类推。 -
使用
GotoIf
语句检查value
的类型,并根据类型跳转到相应的标签处进行处理。比如,如果value
是一个 Smi(表示一个被标记为小整数的对象),那么就跳转到return_number
标签处。 -
如果
value
不是 Smi,那么获取其对应的 HeapObject,并加载其 Map 对象。 -
根据 Map 对象判断
value
的类型,并跳转到相应的标签进行处理。例如,如果是一个奇怪的对象(Oddball),则跳转到if_oddball
标签处。 -
在各个标签处,根据对象的具体类型设置
result_var
的值为相应类型的字符串,并跳转到return_result
标签处,返回result_var
的值。 -
最后,返回
result_var
的值。
可以发现 V8 其实对 null 做了提前的处理,可能是为了兼容早期 V8 做的吧
c++
// 定义一个函数,用于模拟 JavaScript 的 typeof 操作符
TNode<String> CodeStubAssembler::Typeof(TNode<Object> value) {
TVARIABLE(String, result_var);
// 定义标签,用于控制程序流程
Label return_number(this, Label::kDeferred), if_oddball(this),
return_function(this), return_undefined(this), return_object(this),
return_string(this), return_bigint(this), return_symbol(this),
return_result(this);
GotoIf(TaggedIsSmi(value), &return_number);
TNode<HeapObject> value_heap_object = CAST(value);
TNode<Map> map = LoadMap(value_heap_object);
GotoIf(IsHeapNumberMap(map), &return_number);
TNode<Uint16T> instance_type = LoadMapInstanceType(map);
// 如果是oddball,则跳转到 if_oddball 标签处,V8 对 null提前做了判断
GotoIf(InstanceTypeEqual(instance_type, ODDBALL_TYPE), &if_oddball);
TNode<Int32T> callable_or_undetectable_mask =
Word32And(LoadMapBitField(map),
Int32Constant(Map::Bits1::IsCallableBit::kMask |
Map::Bits1::IsUndetectableBit::kMask));
GotoIf(Word32Equal(callable_or_undetectable_mask,
Int32Constant(Map::Bits1::IsCallableBit::kMask)),
&return_function);
GotoIfNot(Word32Equal(callable_or_undetectable_mask, Int32Constant(0)),
&return_undefined);
GotoIf(IsJSReceiverInstanceType(instance_type), &return_object);
GotoIf(IsStringInstanceType(instance_type), &return_string);
GotoIf(IsBigIntInstanceType(instance_type), &return_bigint);
GotoIf(IsSymbolInstanceType(instance_type), &return_symbol);
// 如果类型未知,则终止程序执行
Abort(AbortReason::kUnexpectedInstanceType);
BIND(&return_number);
{
result_var = HeapConstantNoHole(isolate()->factory()->number_string());
Goto(&return_result);
}
BIND(&if_oddball);
{
TNode<String> type =
CAST(LoadObjectField(value_heap_object, offsetof(Oddball, type_of_)));
result_var = type;
Goto(&return_result);
}
BIND(&return_function);
{
result_var = HeapConstantNoHole(isolate()->factory()->function_string());
Goto(&return_result);
}
BIND(&return_undefined);
{
result_var = HeapConstantNoHole(isolate()->factory()->undefined_string());
Goto(&return_result);
}
BIND(&return_object);
{
result_var = HeapConstantNoHole(isolate()->factory()->object_string());
Goto(&return_result);
}
BIND(&return_string);
{
result_var = HeapConstantNoHole(isolate()->factory()->string_string());
Goto(&return_result);
}
BIND(&return_bigint);
{
result_var = HeapConstantNoHole(isolate()->factory()->bigint_string());
Goto(&return_result);
}
BIND(&return_symbol);
{
result_var = HeapConstantNoHole(isolate()->factory()->symbol_string());
Goto(&return_result);
}
BIND(&return_result);
return result_var.value();
}
5. 总结
到这里继续深究下去已经没必要了,最初设计的缺陷导致的,当然这次的查询告诉我们对于高级语言的设计并不一定都是对的,如果想深究,也能看到 JavaScript 很多不合理的地方
参考文章
javascript - Why is typeof null "object"? - Stack Overflow
The story of a V8 performance cliff in React · V8
typeof - JavaScript | MDN (mozilla.org)
[The history of "typeof null" (2ality.com)](2ality.com/2013/10/typ... JavaScript%2C typeof null is,it would break existing code)