花费28800秒,从源码的角度我终于知道了 typeof null 为 object 的原理

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)

typeof 与 Javascript 类型源码分析 - 知乎 (zhihu.com)

相关推荐
夏幻灵3 小时前
JavaScript this 面试:五种绑定规则、优先级与常见面试陷阱
开发语言·javascript·面试
Lvan的前端笔记3 小时前
docker:每个前端项目一个 Nginx 容器还是只有一个Nginx容器
前端·nginx·docker
三天不学习3 小时前
Egg.js 4 突然爆火,原因是否归结于AI 原生落地需求爆发?
前端·javascript·全栈·egg.js
xcs194054 小时前
前端 vue 的前端页面debugger 进不去
前端·javascript·vue.js
明月_清风5 小时前
Deno 终局来了:从挑战 Node 到被 Cloudflare 收编
前端·后端·node.js
Csvn6 小时前
框架性能优化
前端
回眸&啤酒鸭8 小时前
【回眸】OpenSwarm 多智能体协作系统实战指南
大数据·前端·人工智能
用户69371750013848 小时前
2026,程序员的时代拐点到了
android·前端·后端
大龄秃头程序员8 小时前
一次 iBeacon + BLE 无感解锁方案的实现记录
前端
小兔子8 小时前
Python 的 GIL 与 free-threading:3.13 之后「去 GIL」走到哪一步了
前端