笔记之浏览器工作原理与实践(3)

537 阅读8分钟

本文是对李兵老师的《浏览器工作原理与实践》课程的记录与整理。

课程地址:浏览器工作原理与实践

《浏览器工作原理与实践》分为几篇笔记,接上文

笔记之浏览器工作原理与实践(1)

笔记之浏览器工作原理与实践(2)

10. this

执行上下文中this
执行上下文中this

全局执行上下文中 this

指向 window 对象

函数执行上下文中 this

  1. 默认情况下,调用函数 this 也是指向 window

  2. 设置函数执行上下文中的 this 值

    1. 函数 call,apply,bind 方法

    2. 对象调用方法设置

      • 在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
      • 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。(谁调用 this 指向谁)
    3. 通过构造函数(new)

      • 首先创建了一个空对象 tempObj;

      • 接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;

      • 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;

      • 最后返回 tempObj 对象。

var tempObj = {};
CreateObj.call(tempObj);
return tempObj;

this 设计缺陷及方案

  1. 嵌套函数 this 不会继承
    • _this:解决本质是将 this 体系转换为作用域体系
    • 箭头函数:本质是箭头函数不会创建自身的执行上下文,this 取决于外部函数
  2. 普通函数中 this 默认指向 window
    • 严格模式下,默认执行一个函数,函数的执行上下文中 this 是 undefined,不指向 window

三、 V8 工作原理

11.栈空间与堆空间

JavaScript 语言类型(动态的弱类型)

  • 静态语言:使用之前需要确定其变量数据类型的称为静态语言。

  • 动态语言:运行过程中需要检查数据类型的称为动态语言。

  • 弱类型语言:支持隐式类型转换的语言

  • 强类型语言:不支持隐式类型转换

    各语言类型
    各语言类型

JavaScript 数据类型

数据类型
数据类型

原始类型

引用类型:Object

古老的坑,typeof null ->Object为什么 "typeof null" 的结果为 "object" ?

内存空间

原始类型的数据值都是直接保存在“栈”中的,引用类型的值是存放在“堆”中的

  1. 栈空间(执行上下文调用栈)
    • 栈空间不会设置太大,主要存放一些原始类型的小数据。
    • 原始类型的赋值会完整复制变量值,引用类型的赋值是复制引用地址
  2. 堆空间
    • 堆空间很大,能存放很多大数据
引用赋值
引用赋值

再谈闭包

function foo() {
    var myName = " 极客时间 "
    let test1 = 1
    const test2 = 2
    var innerBar = {
        setName:function(newName){
            myName = newName
        },
        getName:function(){
            console.log(test1)
            return myName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName(" 极客邦 ")
bar.getName()
console.log(bar.getName())

从内存模型的角度分析这段代码的执行流程

  1. 当 JavaScript 引擎执行到 foo 函数时,首先会编译,并创建一个空执行上下文。
  2. 在编译过程中,遇到内部函数 setName,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了 foo 函数中的 myName 变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包,于是在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量。
  3. 接着继续扫描到 getName 方法时,发现该函数内部还引用变量 test1,于是 JavaScript 引擎又将 test1 添加到“closure(foo)”对象中。这时候堆中的“closure(foo)”对象中就包含了 myName 和 test1 两个变量了。
  4. 由于 test2 并没有被内部函数引用,所以 test2 依然保存在调用栈中。

闭包的核心:1. 预扫描内部函数 2. 把内部函数引用的外部变量保存在堆中

闭包产生过程
闭包产生过程

12.垃圾回收

1.调用栈中数据回收(下移 ESP)

举栗子

function foo(){
    var a = 1
    var b = {name:" 极客邦 "}
    function showName(){
      var c = 1
      var d = {name:" 极客时间 "}
    }
    showName()
}
foo()

执行函数时调用栈如图:

执行函数时调用栈
执行函数时调用栈

ESP 指针:记录当前执行状态的指针,ESP 下移操作就是销毁函数执行上下文的过程。

栈中回收函数执行上下文
栈中回收函数执行上下文

2.堆中数据回收(垃圾回收器)

  1. 代际假说,垃圾回收的基础

    • 第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
    • 第二个是不死的对象,会活得更久。
  2. V8 中将堆分为新生代老生代两个区域

    新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象

    新生区通常只支持 1 ~ 8M 的容量,而老生区支持的容量就大很多了。

    • 副垃圾回收器,主要负责新生代的垃圾回收。
    • 主垃圾回收器,主要负责老生代的垃圾回收。
  3. 垃圾回收器的工作流程

    1. 标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。
    2. 回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
    3. 内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如接下来我们要介绍的副垃圾回收器。
  4. 副垃圾回收器(新生区)

    • 大多数小的对象都会被分配到新生区,所以说这个区域虽然不大,但是垃圾回收还是比较频繁的。

    • 使用Scavenge 算法处理,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域,新加入的对象都会存放到对象区域。

    • 在垃圾回收过程中,首先要对对象区域中的垃圾做标记;标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。

    • 完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去

    • 如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小

    • 新生去空间小,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

    V8堆空间
    V8堆空间
  5. 主垃圾回收器(老生区)

    • 老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。

    • 主垃圾回收器是采用**标记 - 清除(Mark-Sweep)**的算法进行垃圾回收。

    • 标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据

      标记过程
      标记过程
    • 清除过程,清除标记为垃圾数据的过程

      清除过程
      清除过程
    • 标记 - 整理(Mark-Compact),整理内存碎片

      标记整理
      标记整理
  6. 全停顿

    • 由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。全停顿对新生代影响不大,因为空间小,存活对象小,对老生代影响大,会卡死主线程。
    全停顿
    全停顿
    • 增量标记(Incremental Marking)算法:降低老生代垃圾回收的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成

      增量标记
      增量标记