已修改部分 2021-5-14
写完发现果然功力不够,希望抛砖引玉
执行上下文和作用域链
执行上下文和作用域链是两个东西,一动一静:
执行上下文:是在函数调用时产生,不同的参数调用形成不同的执行上下文,多次执行多次产生,函数执行完销毁。
作用域链:是在函数定义时产生,复制父元素的[[scopes]]给子元素的[[scopes]],只会随着父函数执行完退出执行上下文栈而销毁,如果在此期间此函数被返回,就形成了闭包。
那这两者是什么关系呢?执行上下文是一个包含了函数执行时需要的所有数据的一个对象,对,就是可以把它看成一个对象,里面有this的指向、arguments、寻找变量时用到的作用域链,注意执行上下文里的作用域链和函数的作用域链有小小的不同,后面再说。所以是执行上下文包含于作用域链的关系!
运行过程
js的代码运行的过程可以看成是一系列函数执行的过程,因为函数嵌套的关系,我们可以让函数执行时进栈,然后执行,执行完毕后出栈,依次往复直到全局函数执行完。
但每一次执行时会有许多需要用到数据,如this指向、arguments参数等每一次都不一样的数据,我们当然可以把它们都挂在函数这个对象中。
但是我们还可以有另一种做法:根据函数创建出一个新的对象,在这个对象中挂着函数执行需要的全部数据,而且执行完就把这个对象销毁,下一次调用函数时又生成,这个对象就叫做执行上下文。
所以现在变成了执行上下文进栈出栈且当前的执行上下文和当前运行的函数息息相关。
大致流程:根据全局函数创建执行上下文放入栈中—>然后运行此(执行上下文)——》碰到内部函数执行时,注意是执行时不是定义时,又根据此内部函数创建执行上下文——》放入栈中——》运行此(执行上下文)——》依次往复直到全局函数执行完毕。
具体流程:
<script>
let a=1;
function fn1(params) {
let b
let c
console.dir(fn2);
/*[[Scopes]]: Scopes[2]
0: Closure (fn1) {b: undefined}
1: Global {window: Window, self: Window, document: document, name: "", location: Location, …}*/
function fn2() {
let d=3;
console.log(b);
console.dir(fn3);
/*[[Scopes]]: Scopes[3]
0: Closure (fn2) {d: 3} 用到了d,所以创建了d作用域fn2标识的Closure对象。
1: Closure (fn1) {b: undefined, c: undefined} 有b变量是因为复制了父函数的scopes属性,又检索到c的外部变量,但是已有fn1标识的Closure对象,所有直接放入。
2: Global {window: Window, self: Window, document: document, name: "", location: Location, …}
*/
function fn3(){
console.log(c);
console.log(d);
}
}
fn2()
}
fn1();
</script>
复制代码
我刚开始测试时,想着讲清楚了全局函数的运行过程,内部的嵌套函数应该也是一样的逻辑,但因全局函数和局部函数的特殊性,这里需要分别来讲。
为了便于说明先从局部函数的运行说起。
在运行局部函数(fn1)时:
1.根据此局部函数创建执行上下文。(其实执行上下文也是一个对象,里面有很多东西。)
具体创建如链接:【译】理解 Javascript 执行上下文和执行栈
2.此时,此执行上下文中有this的指向,有预编译形成的Local,还有根据此函数的[[scopes]]形成的作用域链,这个作用域链包含中就是函数体的[[scopes]]加上local,就像这样[local,[[scopes]]]。这里注意:函数体中存放的作用域链并不包括自身的local,因为作用域的创建是在函数定义时,还没有调用,没有local;只有父函数的[[scopes]]加上可能有的Closure(3.2和3.3说明函数的作用域链有什么东西)。然后放入栈中并运行此执行上下文。
3.准备工作做完了,正式开始运行,在运行到fn2的定义时,这里需要注意几点
– 3.1 注意是fn2的定义时不是执行时
– 3.2 将当前函数(fn1)的[[scopes]]属性复制一份给fn2的[[scopes]]属性,注意是将父函数的[[scopes]],而不是当前执行上下文的作用域链。
– 3.3 并检索fn2中是否用到了外部变量,如:用到了fn1作用域的变量就创建一个带有一个fn1标识的(用到的哪个作用域的变量就标识哪个)Closure 对象,并入栈到fn2的[[scopes]]属性,但是如果在复制过来的[[scopes]]中里面有此标识的Closure 对象,那就不会创建了,直接将引用的变量放入此Closure 对象中。
<script>
let a=1;
function fn1(params) {
let b
let c
console.dir(fn2);
/*[[Scopes]]: Scopes[2]
0: Closure (fn1) {b: undefined}
1: Global {window: Window, self: Window, document: document, name: "", location: Location, …}*/
function fn2() {
let d=3;
console.log(b);
console.dir(fn3);
/*[[Scopes]]: Scopes[3]
0: Closure (fn2) {d: 3} 用到了d,所以创建了d作用域fn2标识的Closure对象。
1: Closure (fn1) {b: undefined, c: undefined} 有b变量是因为复制了父函数的scopes属性,又检索到c的外部变量,但是已有fn1标识的Closure对象,所有直接放入。
2: Global {window: Window, self: Window, document: document, name: "", location: Location, …}
*/
function fn3(){
console.log(c);
console.log(d);
}
}
fn2()
}
fn1();
</script>
复制代码
再将此Closure 对象放入[[scopes]]的顶部。如果没有用到就不会创建Closure 对象,如例子fn2中如果不打印b变量,fn2的[[scopes]]就只有GO,因为fn1[[scopes]]中只有GO,复制过来也只有GO。此处参考: JavaScript 的静态作用域链与“动态”闭包链
– 3.4 这里再次注意无论fn2是否返回,只要有外部变量的使用就会生成Closure 对象,引用了几个外部作用域内的变量就生成几个Closure 对象,按照作用域链的顺序放入。,区别在于如果返回了此函数fn2,那那片区域的内存的指向有被保存不会被销毁,这也是就是闭包。
注意!!!以上都是在运行到函数定义时完成的。
4.然后就是其他的代码语句,当又运行到函数调用时,重复1-4步。
接下来是在运行全局函数时:
1.根据此全局函数创建执行上下文。也就是GO(其实执行上下文也是一个对象,里面有很多东西。)
具体创建如链接:【译】理解 Javascript 执行上下文和执行栈
2.此时,此执行上下文中有this的指向,作用域链只有[[GO]],然后放入栈中并运行此执行上下文。
3.准备工作做完了,正式开始运行,在运行到fn1的定义时,因为是第一层函数,只需要将GO放入。这里注意:需要像上面局部函数运行时一样检索内部是否用到外部变量吗?其实不需要,因为已经将GO放入了且fn1是第一层函数,它的外部变量一定在GO。
4.然后就是其他的代码语句,当又运行到函数调用时,重复1-4步。
执行过程综上所述
另外需要注意:
1.如果在全局函数中 有let定义的变量,会在第一层局部函数的[[scopes]]的GO之上放入一个script对象,里面存放这些let变量,不管内部是否引用。
2.如果在全局函数直接添加一个{},会覆盖掉1的规则,又会回到只有引入了才会生成Closure 对象的规则,不过这时候生成的不是Closure 对象而是block对象(这2者的区别等待评论区回答)。
3.如果有多个 < script >标签,全局函数中的变量和函数是互通,只有同时在全局函数最外层加{}和变量设置let才能避免,缺一不可。