关于执行上下文
首先,JS脚本在执行之前,会先经过一个预编译的过程。
如果发生了语法错误,便会报错,无法执行脚本。
除此以外,还会在脚本执行之前,做好一些准备工作,即创建执行环境,也就是"执行上下文"。
执行上下文类型
- 全局执行上下文(Global Object 简称GO)
脚本在执行之前,会生成一个全局执行上下文,这是最基础也是最顶层的执行上下文,里面存放着window全局对象与声明的变量,并且将this指向window。
值得注意的是,一个程序只会存在一个GO。
// 为了直观感受,以对象做执行上下文的列子
var a = 10;
function b() { }
GO = {
window: window,
a: undefined,
b: function b() { },
this: window
}
- 函数执行上下文
每当一个函数被声明时都会创建一个[[scope]]的属性,也就是我们俗称的作用域。
而函数被调用时,会创建一个函数执行上下文(Activation Object 简称AO), [[scope]]里面不仅保存着除本身函数调用时,所产生的函数执行上下文,还保存着其他执行上下文。
需要注意,函数的重复调用,会创建新的AO执行上下文。
// 依旧以对象做执行上下文的列子
var num1 = 5;
var num2 = 6;
function b() {
var num1 = 10;
console.log(num1);//输出10
console.log(num2);//输出6
}
b();
/*
* 1-预编译阶段
* GO = {
* num1 : undefined,
* num2 : undefined,
* b : function b() {}
* }
* GO产生 function b(){} 声明 创建了b.[[Scope]]作用域属性 且把GO存入进去 b.[[Scope]] = [GO]
*/
/*
* 2-脚本执行 b()开始调用 但函数体没执行阶段
* GO = {
* num1 : 5,
* num2 : 6,
* b : function b() {}
* }
* b函数的 执行上下文 创建 b-AO产生
* b-AO = {
* num1 : undefined,
* this : ?
* }
* 默认的指向作者不了解,null or window?,但是后续谁调用this指向谁。下面例子省略this不写了
*/
/*
* 3- b()开始调用 函数体执行阶段
* GO = {
* num1 : 5,
* num2 : 6,
* b : function b() {}
* }
*
* b-AO = {
* num1 : 10,
* }
*/
总结
关于b.[[Scope]]的值,作者用了数组[]来解释,只是为了方便。其实并不是哈,用栈来理解更合适。
回到正题: console.log(num1);//输出10
其实就是根据b.[[Scope]]作用域链一个个执行上文开始寻找num1的变量(索引0开始,便利作用域链数组),b-AO执行上下文存在num1:10,所以直接输出num1的对应值10。
console.log(num2);//输出6 同样道理,先在作用域链数组[0]--> b-AO里面寻找,找不到变量num2. 继续寻找[1]--> GO,发现存在num2,直接输出对应值6。 注意:在b函数执行完以后,b函数的执行上下文AO就会销毁,作用域链由[AO,GO]变成[GO]。
- Eval 函数执行上下文(这个作者不了解,就不谈)
根据上面所诉,谈谈闭包,废话不说,先上代码
var a = 5;
function fn1() {
var a = 10;
return function fn2(){
return a;
}
}
var value = fn1();
console.log(a); // 输出5
console.log(value()); //输出10
/*
* 1- 预编译阶段------------------
* GO = {
* a:undefined,
* fn1:function fn1(){},
* value:undefined
* }
* 函数fn1被声明,创建了Scope属性,fn1的 作用域链 数组形成 fn1.[[Scope]] = [GO]
*/
/* 2- var value = fn1(); fn1()调用阶段,函数体未执行
* GO = {
* a:5,
* fn1:function fn1(){},
* value:undefined
* }
* fn1-AO = {
* a:undefined,
* fn2:function fn2(){}
* }
* fn1被调用 fn1-AO执行上下文创建,fn1的 作用域链 数组改变 fn1.[[Scope]] = [fn1-AO,GO]
* 于此同时 函数fn2被声明,创建了Scope属性,fn2的 作用域链 数组形成 fn2.[[Scope]] = [fn1-AO,GO]
*/
/* 3- var value = fn1(); fn1()调用阶段,函数体执行完毕
* GO = {
* a:5,
* fn1:function fn1(){},
* value:function fn2(){}
* }
* A的AO = {
* a:10,
* fn2:function fn2(){}
* }
*
* fn1函数执行完毕, fn1的作用域链 销毁 fn1.[[Scope]] 不存在了
* fn2作用域链 保持不变 fn2.[[Scope]] = [fn1-AO,GO]
* fn1.[[Scope]]内的fn1-AO销毁, 并不能影响 fn2.[[Scope]]内的fn1-AO
*/
/* 4- console.log(a); // 输出5 全局环境下输出 a 属性的值 去GO里面找 a 所以是5
*
* 分析 console.log(value()); //输出10
* value == function fn2(){} 所以 value() == fn2()
* fn2 被调用 创建了 它自己的执行上下文
* fn2-AO = {} fn2的 作用域链 数组改变 fn2.[[Scope]] = [fn2-AO,fn1-AO,GO]
* return a; 开始执行 现在[0](fn2-AO)寻找,没有, 再寻找[1](fn1-AO),发现变量a,输出该属性的值,也就是10
*/
最后总结
fn1函数所创建的fn1-OA执行上下文,本应该在fn1()执行完毕后销毁。
但fn1函数内的 fn2的[[Scope]]属性,保存住了fn1-OA执行上下文,导致fn1-OA执行上下文销毁不了。
而fn1-OA执行上下文内又存有所声明的变量,以及属性值。
再者GO上声明了变量,去保存了fn2函数,相当于fn2的[[Scope]]的作用域链完整的保存下来。
于是通过 fn2-AO -> fn1-AO -> GO 的顺序,寻找a,并输出对应的值。