js解析过程
JS是一门编译语言,在浏览器运行时是由JS解释器进行分析编译,JS的解析过程可分为三步: 语法检查阶段、预解析阶段、执行阶段
一、语法检查
- 词法分析:把js的字符流转换成标记流。
- 语法分析: 把标记流产生的记号按照ECMAScript标准生成语法树 (把收集到的信息存储到数据类型中)
词法语法分析阶段主要是对代码进行分析,将词法单元(如:关键字,标识符,运算符等)转换成语法树。这个阶段主要检查代码有没有语法错误和创建作用域等,为js进一步处理和执行提供基础
二、预解析
- 变量和函数声明提升:将变量声明和函数声明提升至当前作用域的顶部。 对函数表达式进行变量声明并提升,但不会给予赋值 。
- 创建作用域链:确定各个作用域之间的关系,创建作用域链,以便在执行阶段准确查找变量和函数 。
在预解析阶段,只有声明本身会被提升,而赋值不会被提升,而且会有初始值。因此可以在变量和函数声明之前对其进行访问(不建议这么做)。声明变量和函数声明也是有权重的:函数声明>声明变量; 在同时设置了相同名称的变量和函数时,JS会优先函数声明。函数表达式其实也是声明变量。
三、执行阶段
- 变量初始化赋值: 根据预解析阶段的准备,实际对变量进行初始化赋值。
- 函数调用: 执行函数调用,包括参数传递,执行函数体内的代码
- 执行上下文管理: 创建和管理执行上下文(Execution Context),处理调用栈,记录函数调用和局部变量。
- 垃圾回收: 清理不再使用的变量和函数,释放内存。
预解析之后就是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垃圾回收方式大致分为引用计数法、 标记清除法
- 引用计数法:每一个变量、对象都有一个引用计数,统计被多少个变量、对象引用;当引用计数为0的时候,就表明是一个无用变量、对象,就可以被回收;这种方式简单,但是有一个严重问题:无法处理循环引用问题;例如:两个对象互相引用,但是没有其他引用,本质上这两个对象应该就是无用对象,应该被清理,但是它们的引用计数都不为0,不会被清理;引用计数法是js最初开始时候采用的垃圾回收机制,在旧版ie浏览器可能会用到。
- 标记-清除法:从根对象开始遍历所有可达对象(被引用或使用),将他们标记为活动对象;在清除阶段,清除所有未被标记的对象;现代浏览器以标记清除法为主结合其他算法,如分代回收、增量标记等算法进行垃圾回收机制。 注:可达对象是指那些能够从根对象(如全局对象、当前执行上下文中的局部变量等)访问到的对象。换句话说,如果某个对象可以通过某条路径被访问到,那么它就是可达的。
js垃圾回收机制主要采用标记-清除法,即先标记再清除。 js会定期进行垃圾回收(避免浪费性能);接下来我们从全局作用域、函数作用域、块级作用域角度描述一下js是如何进行垃圾回收的
- 全局作用域:全局环境是js最外层环境,js垃圾回收机制会扫描所有全局变量对象,将可以访问的对象标记为可达对象。在垃圾回收机制清理阶段,清除掉没有被标记为活动对象的对象。全局定义的变量和可达对象,会在整个页面(根环境)卸载的时候才会被清除,所以尽量少定义全局变量和对象。
- 函数环境:js执行到函数环境时,创建执行上下文环境,垃圾回收机制扫描当前环境下的变量对象,把可以访问到的变量对象标记为可达对象,在函数执行完之后,函数的执行上下文就被销毁,函数内部的变量对象无法通过全局环境进行访问,就变成了不可达对象,在js垃圾回收机制清除阶段就会被清除回收。
- 块级环境:块级环境是es6之后才有的,如if,for等在其中使用let、const定义变量就会形成快级作用域。js在执行块级作用域时,会扫描当前环境下的变量对象,把可以访问到的变量对象标记为可达对象,在块代码执行完成之后,块级执行上下文环境被清除,变量对象无法通过外部环境进行访问,成为不可达对象,在垃圾回收机制的清理阶段就会被清除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
//1.变量 let a = 111 //全局变量只有在页面卸载 的时候才会被清理 //2.对象 let b = { //b对象引用堆中的一个对象数据,没有解除引用关系,不会被垃圾回收机制回收,一直到页面卸载才会被清理 a: 111 } //对象被清除的某些情况 let c = {//复杂数据类型,{b:222}被存入堆中,c在栈中存入的是一个指向堆中{b:222}的引用 b: 222 } c = [666] //c变量改变了对象的引用:由原先的{b: 222}改变成了对[666]的引用,这个时候堆中的{b:222}数据对象就成了不可达对象,就会在垃圾回收阶段被清除回收 c = null //c置空了引用,堆中[666]就成了无用的对象,就会被垃圾回收机制回收 //函数 function fn(){//当前函数代码是存在堆中的。 let d = 333 let e = {c:333} } fn()//当执行当前函数的时候,js会创建一个执行上下文,推入到执行栈中;当函数执行完成执行上下文被清除,在当前环境下的变量对象就成了不可访问对象,在垃圾回收极端就会被清除回收 //闭包无法清除变量的情况 function fn1(){ let f = 6 let f1 = 7 function callback() { let f2 = 8 return console.log(f); } return callback } fn1()()//在执行fn1()的时候运行到callback,callback就会被被存放到堆中,因为里面有对f的访问(即使fn1()()执行完callback代码依然存在依然保留对f的访问),f就会一直被保存在内存中,f就是可达的,所以f就不会被清除,而f1没有被任何地方访问就成了不可达对象,会被垃圾回收机制回收。在执行fn1()()时候callback被执行,清除callback的执行上下文之后f2成为不可达对象,也会被垃圾回收机制回收,f仍然被堆中的callback所访问,所以f就一直存在内存中无法被垃圾回收机制清除 |
- 不会被清除的情况
- 全局作用域变量:全局作用域的变量在浏览器打开的时候会一直被标记为"进入环境",只有在关闭浏览器的时候才会被标记为"离开环境"才能被清除;所以应该尽量减少定义全局变量或者使用严格模式
- 闭包:当调用闭包的那个对象一直都没有被清除(比如在全局调用闭包,或者异步调用闭包的时候),那么闭包里的变量就一直存在不会被清除,做法是在调用完之后把调用闭包的那个对象给清空为'null'
- 定时器和回调:没有清除定时器,那里边的变量就会一直存在不会被销毁;能清除的定时器一定要清除
- 循环调用:当两个或多个对象之间存在相互引用,并且没有被其他对象引用,就会发生循环引用,从而导致不会被垃圾回收机制回收。
- Dom元素的引用:我们通常会将Dom引用放进一个数组或者对象中,当我们不需要这个对象了把这个对象删除了,但是这个对象的引用还是依然存在的,这个时候就需要手动将这个Dom的引用删除:整个对象为'null'或者删除这个属性