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

1. 背景

我之前一直知道 typeof null 为 object ,但是一直没深究为什么?在查阅了相关的文档,汇总为以下文档

2. 汇总解释

我们先来看 《JavaScript高级程序设计》 里面是怎么说的吧?

Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回 "object" 的原因

这里我们就可以解释通了,null 其实是一个 空对象指针 ,所以 typeof 会检测 nullobject

如果单纯是这样必然是不对的,是 空对象指针 就一定会被检测为 object 吗?

这其实就是一句总结,里面肯定有更加深层的原因,我看了一眼 ECMAScript 的文档,发现没有说这一点(可能是我没找到),倒是在 MDN 发现了解释

在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"

我来简单解释一遍,对象类型 表示为 0null 在大部分机器上面都是 0x00...000 ,所以 null0 ,所以 nulltypeof 下 会被检测为 object

3. 早期解释

这个解释已经能解答很多疑惑了,如果我们再往下面看一层呢?为什么对象类型为 0 ,null 为 0x00typeof 底层分析类型是通过什么手段?以及这部分源码是什么样子的?

其实 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...](https://link.juejin.cn?target=https%3A%2F%2F2ality.com%2F2013%2F10%2Ftypeof-null.html%23%3A~%3Atext%3DIn "https://2ality.com/2013/10/typeof-null.html#:~:text=In") JavaScript%2C typeof null is,it would break existing code) [typeof 与 Javascript 类型源码分析 - 知乎 (zhihu.com)](https://link.juejin.cn?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F143590829 "https://zhuanlan.zhihu.com/p/143590829")

相关推荐
子兮曰5 小时前
Bun v1.3.14 深度解析:Image API、HTTP/3、全局虚拟存储与五十项变革
前端·后端·bun
kyriewen6 小时前
今天,百年巨头一次砍了9200人,而一个离职科学家的实话让全网睡不着觉
前端·openai·ai编程
问心无愧05136 小时前
ctf show web 入门42
android·前端·android studio
kyriewen6 小时前
老板逼我上AI,我偷偷在浏览器里跑LLaMA,省下20万API费
前端·react.js·llm
Beginner x_u6 小时前
前端八股整理(手写 02)|数组转树、数组扁平化、随机打乱一个数组
前端·数组·数组转树·数组扁平化
KaMeidebaby7 小时前
卡梅德生物技术快报|禽类成纤维细胞 FISH 实验:鸟类性别染色体基因定位技术实现与数据验证
前端·数据库·其他·百度·新浪微博
天若有情6737 小时前
前端高阶性能优化:跳出传统懒加载与预加载,基于用户行为做轻量预判加载
前端·性能优化
小小小小宇7 小时前
前端转后端:SQL 是什么
前端
张元清8 小时前
React Observer Hooks:7 种监听 DOM 而不写样板代码的方式
前端·javascript·面试
广州华水科技8 小时前
单北斗GNSS变形监测是什么?主要有怎样的应用与优势?
前端