理解javascript中的变量提升和执行上下文

一、写在前面

在上一篇文章,简单介绍了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()函数。所以并不能访问到其内部的局部变量。


到此。全部执行完毕。

五、总结

执行上下文大概包括以下几点:

  • 单线程
  • 同步执行,只有栈顶的执行上下文处于执行中。
  • 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
  • 函数执行上下文的个数没有限制,每次函数调用都会创建一个执行上下文,即使是调用的自身函数。

思考一下:为什么会发生变量提升呢?

我们可以从变量对象的创建过程去理解:
变量对象的创建经历了这些过程

  1. 建立arguments对象,检查当前上下文中的参数,建立该对象下的属性与属性值。
  2. 检查当前上下文的函数声明,也就是指用function关键词声明的函数。然后在变量对象(VO)中建立一个属性,属性值为指向该函数所在堆内存中的地址引用。
  3. 检查当前上下文中的变量声明,没找到一个变量声明,就在VO中建立一个属性,属性值为undefined。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值