1、预编译
从执行的角度来说JS语言是解释性语言,是解释一行执行一行: 在JS运行代码是时,js引擎的工作流程如下:
1、检查通篇的语法错误,如果这个时候有错误就报SyntaxError
2、预编译(函数执行前做的事情)
3、解释一行执行一行
1-1:预编译的流程:
AO={
1、寻找形参和变量声明
2、实参值赋给形参
3、寻找函数声明
4、执行
}
GO={
1、找变量
2、找函数声明
3、执行
}
2、作用域: 作用域[[scope]]是JS引擎内部固有的隐式属性,是函数被定义或者程序开始的时候产生(这个被定义指的是在JS运行是发现有函数被定义,并不是编写代码的时候的定义),是用来存储作用域链的容器。
3、作用域链: 作用域链是存储执行上下文地址的容器,是由不确定数量的独立AO或GO的引用地址有序的排在一起形成的一种链条,数据结构:先进后出。 执行上下文有2种:
1、全局执行上下文GO(global Object)
2、函数执行上下文AO(Active Object)
4、程序执行: 全局预编译完成之后程序开始执行。 执行期JS的工作流程:
4-1、遇到有函数被调用,在调用前对该函数进行预编译,生成执行上下文AO,然后把这个AO的引用地址保存在这个函数的作用域链的首位。
4-2、函数执行完成之后,这个AO的引用地址就会从作用域链中删除,
4-3、JS引擎检查这个AO有没有被其他地方引用到,如果没有,就销毁,如果有就不做任何处理。
通过以下代码来分析程序执行期的工作流程对应的作用域、作用域链、GO、AO变化:
function fn1() {
var a = 1
function fn2() {
var b = 2;
console.log(++a);
}
return fn2
}
var c = 3
var fn3 = fn1();
fn3();
为了区分函数fn1和fn2的AO,下文中:AO(fn1)表示fn1函数执行期上下文的引用地址,AO(fn2)表示fn2函数执行期上下文的引用地址,GO表示是全局执行期上下文对象的引用地址。
第1步: 全局预编译,生成的作用域、作用域链及执行上下文,发现fn1函数被定义,创建fn1的作用域、然后copy一份全局的作用域链,赋值给fn1的作用域链,如下: GO:
global[[scope]]=[scope chain]
global[[scope chain]]=[GO]
GO = {
this: window,
window: object,
fn1: function,
c: undefined,
fn3: null
}
fn1[[scope]]=[scope chain]
fn1[[scope chain]]=[GO]
第2步: 全局开始执行,执行c = 3语句时,GO如下:
GO = {
this: window,
window: object,
fn1: function,
c: 3,
fn3: null
}
第3步: 执行fn3 = fn1()语句时,fn1的AO开始生成,返回的执行上下文地址,保存在它作用域链的第0位,此时对应的作用域、作用域链,AO、GO如下:
global[[scope]]=[scope chain]
global[[scope chain]]=[GO]
GO = {
this: window,
window: object,
fn1: function,
c: 3,
fn3: null
}
fn1[[scope]]=[scope chain]
fn1[[scope chain]]=[AO(fn1)、GO]
AO(fn1) = {
a: undefine,
fn2: function,
}
第4步: fn1开始执行,a赋值为1 ,发现fn2被定义,然后创建fn2的作用域和作用域链,然后copy一份fn1的作用域链赋值给fn2的作用域链,这个时候就闭包产生了。对应的作用域、作用域链、GO、AO如下:
global[[scope]]=[scope chain]
global[[scope chain]]=[GO]
GO = {
this: window,
window: object,
fn1: function,
c: 3,
fn3: null
}
fn1[[scope]]=[scope chain]
fn1[[scope chain]]=[AO(fn1)、GO]
AO(fn1) = {
a: 1,
fn2: function,
}
fn2[[scope]]=[scope chain]
fn2[[scope chain]]=[AO(fn1)、GO]
第5步: fn1执行完毕,把fn2的引用地址返回给全局变量fn3保存着,此时JS引擎把fn1的作用域链中的AO(fn1)删除, 接下来JS引擎检查fn1的AO地址有没有被地方引用,发现被fn2的作用域链保存着,就不执行销毁fn1的AO。由于fn1的执行上下文AO对象,不被释放,可能会导致内存泄漏。对应的作用域、作用域链、GO、AO如下:
global[[scope]]=[scope chain]
global[[scope chain]]=[GO]
GO = {
this: window,
window: object,
fn1: function,
c: 3,
fn3: function(fn2)
}
fn1[[scope]]=[scope chain]
fn1[[scope chain]]=[GO]
AO(fn1) = {
a: 1,
fn2: function,
}
fn2[[scope]]=[scope chain]
fn2[[scope chain]]=[AO(fn1)、GO]
第6步: fn3开始执行,因为fn3保存的是fn2的引用地址,所以fn3的执行,就是fn2的执行。fn2执行之前,开始预编译,生成执行上下文AO(fn2),此时对应的作用域、作用域链、AO、GO如下:
global[[scope]]=[scope chain]
global[[scope chain]]=[GO]
GO = {
this: window,
window: object,
fn1: function,
c: 3,
fn3: function(fn2)
}
fn1[[scope]]=[scope chain]
fn1[[scope chain]]=[GO]
AO = {
a: 1,
fn2: function,
}
fn2[[scope]]=[scope chain]
fn2[[scope chain]]=[AO(fn2)、AO(fn1)、GO]
AO(fn2) = {
b: 1,
}
第7步: fn3执行完毕,JS引擎把fn3的作用域链中的AO(fn2)进行删除,然后检查fn2的AO地址有没有被其他地方引用,发现没有,最后执行销毁fn2的AO。最后内存中对应的作用域、作用域链、AO 、GO如下:
global[[scope]]=[scope chain]
global[[scope chain]]=[GO]
GO = {
this: window,
window: object,
fn1: function,
c: 3,
fn3: function(fn2)
}
fn1[[scope]]=[scope chain]
fn1[[scope chain]]=[GO]
AO(fn1) = {
a: 1,
fn2: function,
}
fn2[[scope]]=[scope chain]
fn2[[scope chain]]=[AO(fn1)、GO]
5、闭包的利、弊与使用场景: 通过以上分析:
1、闭包的利:可以延长变量的生命周期。
2、闭包的弊:过多的闭包可能会导致内存泄漏。
3、闭包的使用场景:缓存某个变量的实时状态,隔离作用域防止变量污染