2
头图

这是《图解 Google V8》第一篇/共三篇:设计思想篇

这个专栏的优点是:写的通俗易懂,没有涉及 V8 源码部分,对于前端还是比较友好的,学完之后能够知道写下一段 js 代码后,V8 背后都做了哪些事情

这个专栏的不足之处是:没有对技术进行深入讲解,只讲了这个技术是用来解决什么问题的,以及它是怎么工作的

所以这个专栏比较时候对 V8 还不了解的同学去学习,增加自己的知识面

下面是我自己学习每一章的总结,主要记录我在这章中学到内容,并不是对这章完整的总结

如何学习谷歌高性能 JavaScript 引擎 V8?

V8 主要涉及三个技术:编译流水线、事件循环系统、垃圾回收机制

  1. V8 执行 JavaScript 完整流程称为:编译流水线
    编译流水线

    编译流水线涉及到的技术有:

    • JIT

      • V8 混合编译执行和解释执行
    • 惰性解析

      • 加快代码启动速度
    • 隐藏类(Hide Class)

      • 将动态类型转为静态类型,消除动态类型执行速度慢的问题
    • 内联缓存
  2. 事件循环系统

    • JavaScript 中的难点:异步编程
    • 调度排队任务,让 JavaScript 有序的执行
  3. 垃圾回收机制

    • 内存分配
    • 内存回收

01 | V8 是如何执行一段 JavaScript 代码的?

  1. 准备基础环境:

    • 全局执行上下文:全局作用、全局变量、内置函数
    • 初始化内存中的堆和栈结构
    • 初始化消息循环系统:消息驱动器和消息队列
  2. 结构化 JavaScript 源代码

    • 生成抽象语法树(AST
    • 生成相关作用域
  3. 生成字节码:字节码是介于 AST 和机器码的中间代码

    • 解释器可以直接执行
    • 编译器需要将其编译为二进制的机器码再执行
  4. 解释器和监控机器人

    • 解释器:按照顺序执行字节码,并输出执行结果
    • 监控机器人:如果发现某段代码被重复多次执行,将其标记为热点代码
  5. 优化热点代码

    • 优化编译器将热点代码编译为机器码
    • 对编译后的机器码进行优化
    • 再次执行到这段代码时,将优先执行优化后的代码
  6. 反优化

    • JavaScript 对象在运行时可能会被改变,这段优化后的热点代码就失效了
    • 进行反优化操作,给到解释器解释执行

02 | 函数即对象:一篇文章彻底搞懂 JavaScript 的函数特点

V8 内部为函数对象添加了两个隐藏属性:namecode

  • name 为函数名

    • 如果是匿名函数,nameanonymous
  • code 为函数代码,以字符串的形式存储在内存中

当执行到一个函数调用语句时,V8 从函数对象中取出 code 属性值,然后解释执行这段函数代码

什么是闭包:将外部变量和函数绑定起来的技术

参考资料:

  1. The story of a V8 performance cliff in React

03 | 快属性和慢属性:V8 是怎样提升对象属性访问速度的?

V8 在实现对象存储时,没有完全采用字典的存储方式,因为字典是非线性的数据结构,查询效率会低于线性的数据结构

常规属性和索引属性

  • 索引属性(elements):数字属性按照索引值的大小升序排列
  • 常规属性(properties):字符串根据创建时的顺序升序排列

它们都是线性数据结构,分别为 elements 对象和 properties 对象

执行一次查询:先从 elements 对象中按照顺序读出所有元素,然后再从 properties 对象中读取所有的元素

快属性和慢属性

在访问一个属性时,比如:foo.aV8 先查找出 properties,然后在从 properties 中查找出 a 属性

V8 为了简化这一步操作,把部分 properties 存储到对象本身,默认是 10 个,这个被称为对象内属性

线性数据结构通常被称为快属性

线性数据结构进行大量数据的添加和删除,执行效率是非常低的,所以 V8 会采用慢属性策略

慢属性的对象内部有独立的非线性数据结构(字典)

参考资料:

  1. V8 是怎么跑起来的 —— V8 中的对象表示
  2. Fast properties in V8

04 | 函数表达式:涉及大量概念,函数表达式到底该怎么学?

变量提升

js 中有函数声明的方式有两种:

  • 函数声明

    function foo() {
      console.log("foo");
    }
  • 函数表达式

    var foo = function () {
      console.log("foo");
    };

在编译阶段 V8 解析到函数声明和函数表达式(变量声明)时:

  • 函数声明,将其转换为内存中的函数对象,并放到作用域中
  • 变量声明,将其值设置为 undefined,并当道作用域中

因为在编译阶段,是不会执行表达式的,只会分析变量的定义、函数的声明

所以如果在声明前调用 foo 函数:

  • 使用函数声明不会报错
  • 使用函数表达式会报错

在编译阶段将所有的变量提升到作用域的过程称为变量提升

立即执行函数

js 的圆括号 () 可以在中间放一个表达式

中间如果是一个函数声明,V8 就会把 (function(){}) 看成是函数表达式,执行它会返回一个函数对象

如果在函数表达式后面加上(),就被称为立即调用函数表达式

因为函数立即表达式也是表达式,所以不会创建函数对象,就不会污染环境

05 |原型链:V8 是如何实现对象继承的?

  • 作用域链是沿着函数的作用域一级一级来查找变量的
  • 原型链是沿着对象的原型一级一级来查找属性的

js 中实现继承,是将 __proto__ 指向对象,但是不推荐使用,主要原因是:

  • 这是隐藏属性,不是标准定义的
  • 使用该属性会造成严重的性能问题

继承

  1. 用构造函数实现继承:

    function DogFactory(color) {
      this.color = color;
    }
    DogFactory.prototype.type = "dog";
    const dog = new DogFactory("Black");
    dog.hasOwnProperty("type"); // false
  2. ES6 之后可以通过 Object.create 实现继承

    const animalType = { type: "dog" };
    const dog = Object.create(animalType);
    dog.hasOwnProperty("type"); // false

new 背后做了这些事情

  1. 帮你在内部创建一个临时对象
  2. 将临时对象的 __proto__ 设置为构造函数的原型,构造函数的原型统一叫做 prototype
  3. return 临时对象
function NEW(fn) {
  return function () {
    var o = { __proto__: fn.prototype };
    fn.apply(o, arguments);
    return o;
  };
}

__proto__prototypeconstructor 区别

prototype 是函数的独有的;__proto__constructor 是对象独有的

由于函数也是对象,所以函数也有 __proto__constructor

constructor 是函数;prototype__proto__ 是对象

typeof Object.__proto__; // "object"
typeof Object.prototype; // "object"
typeof Object.constructor; // "function"
let obj = new Object();
obj.__proto__ === Object.prototype;
obj.constructor === Object;

objObject 的实例,所以 obj.constructor === Object

obj 的是对象,Object 是函数,所以 obj.__proto__ === Object.prototype

参考资料:

  1. 用自己的方式(图)理解 constructorprototype__proto__ 和原型链
  2. 面试官问:JS 的继承

06 |作用域链:V8 是如何查找变量的?

全局作用域是在 V8 启动过程中就创建了,且一直保存在内存中不会被销毁的,直至 V8 退出

而函数作用域是在执行该函数时创建的,当函数执行结束之后,函数作用域就随之被销毁掉了

因为 JavaScript 是基于词法作用域的,词法作用域就是指,查找作用域的顺序是按照函数定义时的位置来决定的。

词法作用域是静态作用域,根据函数在代码中的位置来确定的,作用域是在声明函数时就确定好了

动态作用域链是基于调用栈的,不是基于函数定义的位置的,可以认为 this 是用来弥补 JavaScript 没有动态作用域特性的

07 |类型转换:V8 是怎么实现 1+“2”的?

V8 会提供了一个 ToPrimitive 方法,其作用是将 ab 转换为原生数据类型

  1. 先检测该对象中是否存在 valueOf 方法,如果有并返回了原始类型,那么就使用该值进行强制类型转换
  2. 如果 valueOf 没有返回原始类型,那么就使用 toString 方法的返回值
  3. 如果 valueOftoString 两个方法都不返回基本类型值,便会触发一个 TypeError 的错误。

《图解 Google V8》学习笔记系列

  1. 《图解 Google V8》编译流水篇——学习笔记(二)
  2. 《图解 Google V8》事件循环和垃圾回收——学习笔记(三)

uccs
756 声望88 粉丝

3年 gis 开发,wx:ttxbg210604