一、写在前面
在上一篇文章,简单介绍了js中的内存空间,这篇文章将通过一个经典题目的分析,来帮助我们理解js代码运行过程中的变量提升和执行上下文。
先放题目:
<script>
//debugger;
var i=5;
function fn(i){
return function(n){
console.log(n+(++i));
}
}
var f=fn(1);
f(2);
fn(3)(4);
fn(5)(6);
f(7);
console.log(i);
</script>
这段代码的运行结果是什么?
在分析题目之前,先介绍一下js中两个重要的概念,变量提升(hosting) 和 执行上下文(Execution Context)。
二、变量提升(hosting)
变量提升(hosting),顾名思义,变量的提升。确实如此,在js中,函数和变量的声明会被提升到最顶部,这一过程发生在代码执行前,可以理解为预编译阶段。
console.log(a);
var a=1;
这段代码的结果是:undefined。它等价为:
var a; //变量声明被提升到顶部
console.log(a);// 此时变量a只有声明而没有赋值,所以结果是undefined
a=1;
注意:函数内部的变量,是一个局部变量,因此函数内部的变量声明只会提升到函数内的最顶部。如下:
console.log(a);
var a=1;
function fn(){
console.log(b);
var b=2;
}
fn();
上述代码等价于:
var a;
console.log(a);
a=1;
function fn(){
var b;
console.log(b);
b=2;
}
fn();
三、执行上下文(Execution Context)
执行上下文(Execution Context)是js中一个最基础,但同时也是最重要的概念,它存在于整个代码的运行过程之中。每当控制器遇到可执行代码的时候,都会创建一个执行上下文,我们可以将执行上下文理解为当前代码的执行环境。一个执行上下文的生命周期可以分为两个阶段。
- 创建阶段
执行上下文会创建变量对象,建立作用域链,确定this的指向。 - 执行阶段
创建完成之后就会开始执行代码,此时会完成形参的赋值和代码的执行。
我们可以知道的是,在一个js程序中,会产生很多个执行上下文,它们以堆栈的方式进行存储。栈底永远是全局的执行上下文,栈顶是当前正在执行的执行上下文。执行完毕之后会从栈中出来(闭包除外)。
四、题目分析
开头代码的运行结果是:
<script>
//debugger;
var i=5;
function fn(i){
return function(n){
console.log(n+(++i));
}
}
var f=fn(1);
f(2);
fn(3)(4);
fn(5)(6);
f(7);
console.log(i);
</script>
分析:
1.创建全局执行上下文,创建变量对象(VO),发生变量提升,i的值是undefined,fn的值是一个地址,指向存储在堆中的函数体(fn的值)。
2.开始执行,将5赋值给i。将fn(1)赋值给f。此处调用函数fn()。创建执行上下文。
此时,f的值为存储console.log(n+(++i))的堆地址。
3.f(2)。此处调用函数f()。创建执行上下文。
此时,f(2)对应的函数就是f(n){console.log(n+(++i));n=2,而EC(f(2))中并没有i的值,所以在函数定义处EC(fn(1))中取的i的值,即i=1。所以f(2)={console.log(2+(++1)) 值为4。
注意,因为执行了++i的操作。所以EC(fn(1))中活动对象i的值变为了2。到此,EC(f(2))出栈。而EC(fn(1))因为被f一直调用 形成闭包,所以不被销毁。
4.fn(3)(4)。fn(3)调用函数fn()。创建一个执行上下文EC(fn(3)),此时返回一个地址,指向堆中{console.log(n+(++i)) ,然后可以理解为f的值变成了新的地址,然后f(4)再进行调用。而EC(f(4))中并没有i的值,所以在函数定义处EC(fn(3))中取的i的值,即i=3。所以f(4)={console.log(4+(++3)) 值为8。到此,EC(f(4))出栈,EC(fn(3))执行完毕,出栈。
5.fn(5)(6)的过程与上一步类似,不再解释。值为12。
6.f(7)。f(7)进行调用,创建执行上下文EC(f(7))。n的值为7,执行代码console.log(n+(++i)),而f(7)中并没有i的值,所以在函数定义处EC(fn(1))中取的i的值,即i=2。所以f(4)={console.log(7+(++2)) 值为10。到此,f(7)执行完毕,出栈。
7.console.log(i)。此处i为全局执行上下文中的i,值为5。因为此处没有调用fn()函数。所以并不能访问到其内部的局部变量。
到此。全部执行完毕。
五、总结
执行上下文大概包括以下几点:
- 单线程
- 同步执行,只有栈顶的执行上下文处于执行中。
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
- 函数执行上下文的个数没有限制,每次函数调用都会创建一个执行上下文,即使是调用的自身函数。
思考一下:为什么会发生变量提升呢?
我们可以从变量对象的创建过程去理解:
变量对象的创建经历了这些过程
- 建立arguments对象,检查当前上下文中的参数,建立该对象下的属性与属性值。
- 检查当前上下文的函数声明,也就是指用function关键词声明的函数。然后在变量对象(VO)中建立一个属性,属性值为指向该函数所在堆内存中的地址引用。
- 检查当前上下文中的变量声明,没找到一个变量声明,就在VO中建立一个属性,属性值为undefined。