花费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... JavaScript%2C typeof null is,it would break existing code)

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

相关推荐
J总裁的小芒果5 分钟前
Vue3 el-table 默认选中 传入的数组
前端·javascript·elementui·typescript
Lei_zhen968 分钟前
记录一次electron-builder报错ENOENT: no such file or directory, rename xxxx的问题
前端·javascript·electron
辣条小哥哥9 分钟前
electron主进程和渲染进程之间的通信
javascript·electron·ecmascript
咖喱鱼蛋10 分钟前
Electron一些概念理解
前端·javascript·electron
yqcoder11 分钟前
Vue3 + Vite + Electron + TS 项目构建
前端·javascript·vue.js
鑫宝Code29 分钟前
【React】React Router:深入理解前端路由的工作原理
前端·react.js·前端框架
Mr_Xuhhh2 小时前
重生之我在学环境变量
linux·运维·服务器·前端·chrome·算法
永乐春秋3 小时前
WEB攻防-通用漏洞&文件上传&js验证&mime&user.ini&语言特性
前端
鸽鸽程序猿3 小时前
【前端】CSS
前端·css
ggdpzhk3 小时前
VUE:基于MVVN的前端js框架
前端·javascript·vue.js