zoukankan      html  css  js  c++  java
  • V8 的 typeof null 返回 "undefined" 的 bug 是怎么回事

    1997 年,IE 4.0 发布,带来的众多新特性中有一个对未来“影响深远”的 DOM API:document.all。在随后的 6 年里,IE 的市场占有率越来越高,直到 2003 年的 95%。

    在这段时间里,产生了两种成千上万的页面。第一种:IE only 的页面,由于超高的市场占有率,开发人员觉得根本不需要考虑兼容性,于是直接使用 document.all,比如:

    document.all("foo").style.visibility = "visible"

    甚至很多网站直接在服务器端判断客户端的 UA,不是 IE 的直接返回一句话:“本站只支持 IE。。。

    第二种页面:开发人员使用 document.all 来识别 IE,只对 IE 展现特殊的效果:

    var isIE = !!document.all
    if (isIE) {
      // 使用 IE 私有的 DOM API,私有 CSS 特性
    }

    那个年代的很多书也都在讲这种判断方法,直到现在,2016年,估计还有少数人这么写,国内出版的垃圾书估计也有可能从别处复制粘贴这样的代码。

    由于第一种页面的大量存在,Opera 在 2002 年发布的 6.0 版本里实现了 document.all,这导致的结果就是,第一种 IE only 的页面有不少可以在 Opera 中正常浏览了,这是好消息,但坏消息是,第二种页面反而都报错了,!!document.all 是 true 了,Opera 被当成 IE 了,Opera 不可能支持 IE 所有的私有特性。当时有不少人给 Opera 反馈 bug,开发人员表示无法修复。

    这段时间里,为了抢占市场占有率,Mozilla 的人也在持续讨论要不要实现 document.all,bugzilla 上有很多历史帖子可查。最终,在 2004 年,JavaScript 之父 Brendan Eich 在 Firefox 0.10 预览版实现了 document.all,但有了 Opera 的前车之鉴,Brendan 在实现 document.all 的时候玩了个小技巧,那就是你可以正常的使用 document.all,但你无法检测到它的存在:

    > document.all + "" 
    "[object HTMLAllCollection]"
    > typeof document.all
    "undefined"
    > !!document.all
    false

    Brendan 取名为“undetected document.all”,但在当时很多人也发现了,document.all 并不是真的检测不到,比如:

    > document.all === undefined
    false
    > "all" in document
    true

    当时 Mozilla 的人也回复了:“这不是 bug,因为做这个改动是被迫的,而且这个改动是违反 ECMAScirpt 规范的,改动越小越好,in 和 === 就不去管了,毕竟极少数的人用 === 和 in 判断 document.all 存在与否”。现如今所有的浏览器都是这样的实现,HTML 5 规范里也是这么规定的

    那段时间 Safari 才刚刚起步,但也有收到来自用户的不支持 document.all 的 bug,2005 年底,Safari 学 Firefox,实现了 undetectable document.all

    2008 年,Opera 在 9.50 Beta 2 版本将自己直接暴露了多年的 document.all 也改成了 undetectable 的,变更记录里是这么写的:“Opera now cloaks document.all”。 Opera 的工程师当年还专门写了一篇文章讲了 document.all 在 Opera 里的变迁,还说到 document.all 绝对值得被展览进“Web 技术博物馆”。

    2008 年底,Chrome 1.0 发布,Chrome 是基于 Webkit 和 V8 的,V8 当然得配合 Webkit 里的 document.all 的实现。

    很戏剧性的是,在 2013 年,连 IE 自己(IE 11)也隐藏掉了 document.all,也就是说,所有现代浏览器里 document.all 都是个假值了。

    在 V8 里的实现是:一个对象都可以被标记成 undetectable 的,很多年来只有 document.all 带有这个标记,这是相关的代码片段和注释

    // Tells whether the instance is undetectable.
    // An undetectable object is a special class of JSObject: 'typeof' operator
    // returns undefined, ToBoolean returns false. Otherwise it behaves like
    // a normal JS object.  It is useful for implementing undetectable
    // document.all in Firefox & Safari.
    // See https://bugzilla.mozilla.org/show_bug.cgi?id=248549.
    inline void set_is_undetectable();
    inline bool is_undetectable();

    然后在 typeof 的实现里,如果 typeof 的参数是 undefined 或者是 undetectable 的,就返回 "undefined":

    Handle<String> Object::TypeOf(Isolate* isolate, Handle<Object> object) {
      if (object->IsNumber()) return isolate->factory()->number_string();
      if (object->IsUndefined() || object->IsUndetectableObject()) {
        return isolate->factory()->undefined_string();
      }
      if (object->IsBoolean()) return isolate->factory()->boolean_string();
      if (object->IsString()) return isolate->factory()->string_string();
      if (object->IsSymbol()) return isolate->factory()->symbol_string();
      if (object->IsString()) return isolate->factory()->string_string();
    #define SIMD128_TYPE(TYPE, Type, type, lane_count, lane_type) 
      if (object->Is##Type()) return isolate->factory()->type##_string();
      SIMD128_TYPES(SIMD128_TYPE)
    #undef SIMD128_TYPE
      if (object->IsCallable()) return isolate->factory()->function_string();
      return isolate->factory()->object_string();
    } 

    今年 2 月份,V8 做了一个改动,就是除了 document.all,要把 null 和 undefined 两个值也标记成 undetectable 的。当时,开发人员清楚的知道这个改动会让 typeof null 返回 "undefined",所以专门改动了 typeof 的实现,并且添加了个对应的测试文件

    assertFalse(typeof null == "undefined")

    看上去很完美,但其实这个改动产生了个 bug,这个 bug 后来流到了 50 和 51 的稳定版里。

    在 4 月份,有人发现了这个 bug,提炼一下重现代码就是:

    for (let n = 0; n < 10000; n++) {
      console.log(typeof null == "undefined")
    }

    Chrome 里执行结果如下:

    在 for 循环执行若干次后,typeof null 会从 "object" 变成 "undefined"。

    我当时也关注了这个 bug,不到一周时间后就修复了。当时 master 分支是 Chrome 52,开发人员觉的 bug 影响不大(我的猜测),就没有合进当时的稳定版 Chrome 50 里。

    其实这个 bug 产生的原因是:V8 还有个优化编译器(optimizing compiler),叫 crankshaft,当代码执行次数够多时,JavaScript 代码会被这个编译器重新编译执行。crankshaft 里有一个它单独使用的 typeof 实现,和普通编译器(full-codegen)用的不一样:

    String* TypeOfString(HConstant* constant, Isolate* isolate) {
      Heap* heap = isolate->heap();
      if (constant->HasNumberValue()) return heap->number_string();
      if (constant->IsUndetectable()) return heap->undefined_string();
      if (constant->HasStringValue()) return heap->string_string();
      switch (constant->GetInstanceType()) {
        case ODDBALL_TYPE: {
          Unique<Object> unique = constant->GetUnique();
          if (unique.IsKnownGlobal(heap->true_value()) ||
              unique.IsKnownGlobal(heap->false_value())) {
            return heap->boolean_string();
          }
          if (unique.IsKnownGlobal(heap->null_value())) {
            return heap->object_string();
          }
          DCHECK(unique.IsKnownGlobal(heap->undefined_value()));
          return heap->undefined_string();
        }
        case SYMBOL_TYPE:
          return heap->symbol_string();
        case SIMD128_VALUE_TYPE: {
          Unique<Map> map = constant->ObjectMap();
    #define SIMD128_TYPE(TYPE, Type, type, lane_count, lane_type) 
      if (map.IsKnownGlobal(heap->type##_map())) {                
        return heap->type##_string();                             
      }
          SIMD128_TYPES(SIMD128_TYPE)
    #undef SIMD128_TYPE
          UNREACHABLE();
          return nullptr;
        }
        default:
          if (constant->IsCallable()) return heap->function_string();
          return heap->object_string();
      }
    }

    那次改动开发人员漏改了这个 typeof 实现,导致了上面的 bug,修复很简单,就是把标红的那句判断挪下面,同时修 bug 的人专门新增了个测试文件,里面 %OptimizeFunctionOnNextCall() 是个特殊函数,可以让某个函数直接被编译进 crankshaft 里执行。

    6 月 8 号,那个 bug 里又有人反馈,说他们的系统因为这个 bug 无法运行了,问什么时候修复代码能合进稳定版里,当时没有相关人员回复。

    6 月 20 号,有人在 reddit 上公开了这个 bug,被人多次转到 twitter 上,那个 bug 下面又有了更多人的回复。

    Chrome 的开发人员意识到问题有点严重了,于是将先前的 commit cherry-pick 进了 Chrome 51 里,Node 也进行了同样的操作

    在这之后,又有三个不知道这个 bug 已经修复了的人报了重复的 bug,issue 621887issue 622628issue 5146

    PS,V8 未来会淘汰 Full-codegen/Crankshaft,使用新的 Ignition(解释器) + Turbofan(编译器) 架构,在 Chrome 50 或者 51 里打开 chrome://flags/#enable-ignition 选项,就会发现 bug 无法重现了。

  • 相关阅读:
    android 网络请求Volley的简单使用
    数据加密,android客户端和服务器端可共用
    非常有用的GitHub链接
    android开发——Android开发中的47个小知识
    EditText禁用系统键盘,光标可以继续使用
    Android Studio 快速开发
    Android系统拍照之后回显并且获取文件路径
    Android为TV端助力 doc里面adb连接出现问题的解决方法
    Android为TV端助力 自定义view中findViewById为空的解决办法
    Android为TV端助力 VelocityTracker 速度追踪器的使用及创建
  • 原文地址:https://www.cnblogs.com/ziyunfei/p/5618152.html
Copyright © 2011-2022 走看看