JavaScript

JS的解析过程、作用域及作用域链、垃圾回收机制

字数:3246    阅读时间:17min
阅读量:704

js解析过程

JS是一门编译语言,在浏览器运行时是由JS解释器进行分析编译,JS的解析过程可分为三步: 语法检查阶段、预解析阶段、执行阶段

一、语法检查

  1. 词法分析:把js的字符流转换成标记流
  2. 语法分析: 把标记流产生的记号按照ECMAScript标准生成语法树 (把收集到的信息存储到数据类型中)

词法语法分析阶段主要是对代码进行分析,将词法单元(如:关键字,标识符,运算符等)转换成语法树。这个阶段主要检查代码有没有语法错误创建作用域等,为js进一步处理和执行提供基础

二、预解析

  1. 变量和函数声明提升:将变量声明和函数声明提升至当前作用域的顶部。 对函数表达式进行变量声明并提升,但不会给予赋值 。
  2. 创建作用域链:确定各个作用域之间的关系,创建作用域链,以便在执行阶段准确查找变量和函数 。

在预解析阶段,只有声明本身会被提升,而赋值不会被提升,而且会有初始值。因此可以在变量和函数声明之前对其进行访问(不建议这么做)。声明变量和函数声明也是有权重的:函数声明>声明变量; 在同时设置了相同名称的变量和函数时,JS会优先函数声明。函数表达式其实也是声明变量。

三、执行阶段

  1. 变量初始化赋值: 根据预解析阶段的准备,实际对变量进行初始化赋值。
  2. 函数调用: 执行函数调用,包括参数传递,执行函数体内的代码
  3. 执行上下文管理: 创建和管理执行上下文(Execution Context),处理调用栈,记录函数调用和局部变量。
  4. 垃圾回收: 清理不再使用的变量和函数,释放内存。

预解析之后就是js真正的执行阶段了,js引擎会一行一行读取运行代码,变量对象和活动对象都被赋予真实的值,而没有被调用的永远都是undefined;执行函数,创建执行上下文关系,确定this指针;最后进行垃圾回收,把不需要的数据清除保证内存。

我们上面说的js执行仅仅只是js解释器对js解析后同步代码的执行。完整的js执行阶段,还会处理来自微任务队列宏任务队列里的异步代码。(了解完整的JS事件循环机制)

  • js解释器依次执行当前代码,当遇见同步代码就立刻执行。
  • 当遇见类似promise.then/.catch、MutationObserver等的异步代码,就会将当前异步代码放入到微任务队列中去。
  • 当遇见DOM事件、定时器、异步请求时就丢给对应的浏览器线程中,这些线程的某一个事件触发的时候,就会把相应的代码推向宏任务队列中。
  • 当执行完当前宏任务之后(我们把一开始执行的js也当做是一个宏任务),js事件循环机制就会去微任务队列中,查看是否有微任务,如果有,就将最前面的微任务,推入到执行栈中进行执行阶段所做的事情,直至循环完所有微任务;在没有微任务之后, js事件循环机制就回去宏任务队列中,查看是否有宏任务,如果有,就将最前面一个宏任务,推入执行栈做执行阶段所做的事情,直至循环完成所有的宏任务队列。当微任务队列和宏任务队列都没有代码可执行时,才算是完成了整个js的执行阶段。
  • 注意微任务和宏任务中都会有同步代码和微任务、宏任务代码,遇见同步就立即执行,遇见微任务就丢进微任务队列,遇见宏任务就继续丢进对应线程,然后再丢进宏任务队列,然后循环执行js执行阶段所做的事情就行。

作用域和作用域链

我们为了方便存储和读取数据,往往会将不同的数据存放在不同的地方,这些存放数据的地方就可以叫做作用域(作用域就是存储和读取的一套规则),JS中可分为全局作用域、函数作用域、块级作用域(es6才引入使用let、const定义变量的代码块)

  • 作用域
    • 创建: js采用的是静态作用域(词法作用域),也就是在词法分析的时候就把作用域确定好了,之后是不能改变的(evel()和with()可以改变但是消耗性能)
    • 特点:作用域可以嵌套但是不能重叠(重叠就覆盖)
    • 定义:是一种分类存储数据规则
  • 作用域链
    • 创建:作用域链是一个数组 [[Scope]] :[当前变量对象,父级上下文变量对象...];其中当前变量对象是在函数执行之前创建的,第二部分是在函数声明时就已经确定的
    • 特点:变量查找是只能一级一级向上找,不能向下找(子级能访问父级的变量,父级不能访问子级的变量)
    • 定义:是 一种变量查找的规则

js垃圾回收机制

我们的浏览器内存是有限的,随着js中的变量、对象增多势必会对浏览器造成很大压力,那么我们就需要在使用完这些变量、对象之后就给他们清除掉;很幸运的是我们的JS有一套自己的垃圾回收机制,在我们不需要这些变量和对象的时候会自动帮我们清除他们。 JS中基础数据类型由于占内存小生命周期短,一般随着作用域移除的时候后会自动被清理,所以JS的垃圾回收主要是对堆中无用对象和无用数据结构的清除。JS垃圾回收方式大致分为引用计数法 标记清除法

  1. 引用计数法每一个变量、对象都有一个引用计数,统计被多少个变量、对象引用;当引用计数为0的时候,就表明是一个无用变量、对象,就可以被回收;这种方式简单,但是有一个严重问题:无法处理循环引用问题;例如:两个对象互相引用,但是没有其他引用,本质上这两个对象应该就是无用对象,应该被清理,但是它们的引用计数都不为0,不会被清理;引用计数法是js最初开始时候采用的垃圾回收机制,在旧版ie浏览器可能会用到。
  2. 标记-清除法从根对象开始遍历所有可达对象(被引用或使用),将他们标记为活动对象;在清除阶段,清除所有未被标记的对象;现代浏览器以标记清除法为主结合其他算法,如分代回收、增量标记等算法进行垃圾回收机制。 注:可达对象是指那些能够从根对象(如全局对象、当前执行上下文中的局部变量等)访问到的对象。换句话说,如果某个对象可以通过某条路径被访问到,那么它就是可达的。

js垃圾回收机制主要采用标记-清除法,即先标记再清除。 js会定期进行垃圾回收(避免浪费性能);接下来我们从全局作用域、函数作用域、块级作用域角度描述一下js是如何进行垃圾回收的

  1. 全局作用域:全局环境是js最外层环境,js垃圾回收机制会扫描所有全局变量对象,将可以访问的对象标记为可达对象。在垃圾回收机制清理阶段,清除掉没有被标记为活动对象的对象。全局定义的变量和可达对象,会在整个页面(根环境)卸载的时候才会被清除,所以尽量少定义全局变量和对象。
  2. 函数环境:js执行到函数环境时,创建执行上下文环境,垃圾回收机制扫描当前环境下的变量对象,把可以访问到的变量对象标记为可达对象,在函数执行完之后,函数的执行上下文就被销毁,函数内部的变量对象无法通过全局环境进行访问,就变成了不可达对象,在js垃圾回收机制清除阶段就会被清除回收。
  3. 块级环境:块级环境是es6之后才有的,如if,for等在其中使用let、const定义变量就会形成快级作用域。js在执行块级作用域时,会扫描当前环境下的变量对象,把可以访问到的变量对象标记为可达对象,在块代码执行完成之后,块级执行上下文环境被清除,变量对象无法通过外部环境进行访问,成为不可达对象,在垃圾回收机制的清理阶段就会被清除。
  • 不会被清除的情况
    • 全局作用域变量:全局作用域的变量在浏览器打开的时候会一直被标记为"进入环境",只有在关闭浏览器的时候才会被标记为"离开环境"才能被清除;所以应该尽量减少定义全局变量或者使用严格模式
    • 闭包:当调用闭包的那个对象一直都没有被清除(比如在全局调用闭包,或者异步调用闭包的时候),那么闭包里的变量就一直存在不会被清除,做法是在调用完之后把调用闭包的那个对象给清空为'null'
    • 定时器和回调:没有清除定时器,那里边的变量就会一直存在不会被销毁;能清除的定时器一定要清除
    • 循环调用:当两个或多个对象之间存在相互引用,并且没有被其他对象引用,就会发生循环引用,从而导致不会被垃圾回收机制回收。
    • Dom元素的引用:我们通常会将Dom引用放进一个数组或者对象中,当我们不需要这个对象了把这个对象删除了,但是这个对象的引用还是依然存在的,这个时候就需要手动将这个Dom的引用删除:整个对象为'null'或者删除这个属性
野生小园猿
励志做一只遨游在知识海洋里的小白鲨
查看“野生小园猿”的所有文章 →

相关推荐