JS执行上下文,作用域链,闭包

代码的运行(执行)环境

在JS中有三种代码运行环境
Global Code

JavaScript代码开始运行的默认环境

Function Code

代码进入一个JavaScript函数

Eval Code

使用eval()执行代码

执行上下文

执行上下文的原文解释请点我查看

【要知道的:】

JS代码在执行前,JS引擎总要做一番准备工作,这份工作其实就是创建对应的执行上下文

同时也是为了表示代码处在不同的运行环境,才引出了执行上下文(Execution context,EC)的概念

执行上下文栈(Execution context stack,ECS):即当JS代码在执行的时,会在执行环境中进入到不同的执行上下文。而这些执行上下文就构成了一个ECS

执行上下文有且只有三类:

  1. 全局执行上下文
  2. 函数上下文
  3. 与eval上下文

由于eval一般不会使用,这里不做讨论。

例如对如下面的JavaScript代码:

var a = "global var";

function foo(){
    console.log(a);
}

function outerFunc(){
    var b = "var in outerFunc";
    console.log(b);
    
    function innerFunc(){
        var c = "var in innerFunc";
        console.log(c);
        foo();
    }
    
    innerFunc();
}


outerFunc()

代码首先进入Global Execution Context,然后依次进入outerFunc,innerFunc和foo的执行上下文,执行上下文栈就可以表示为:
在这里插入图片描述
当JavaScript代码执行的时候,第一个进入的总是默认的Global Execution Context,所以说它总是在ECS的最底部。

执行上下文创建阶段

JS执行上下文的创建阶段主要负责三件事:

  1. 确定this
  2. 创建词法环境组件(LexicalEnvironment)
  3. 创建变量环境组件(VariableEnvironment)

确定this

确定this

this相关例题

创建词法环境组件

词法环境是一个包含标识符变量映射的结构( 标识符:变量)

这里的标识符表示变量/函数的名称;
变量是对实际对象【包括函数类型对象】或原始值的引用。即:age(变量或函数的名称): 0x88 (对象或原始值的引用)

词法环境由环境记录与对外部环境引入记录两个部分组成。

【环境记录】:存储当前环境中的变量和函数声明的实际位置
【外部环境引入记录】:保存自身环境可以访问的其它外部环境

由于不同的执行上下文,因此词法环境组件分为两大类:
全局执行上下文 =》全局词法环境组件

外部环境的引入记录为null,因为它本身就是最外层环境;除此之外(对象)环境记录包含了当前环境下的所有(使用let和const声明的)属性、方法位置

函数执行上下文 =》函数词法环境组件

函数词法环境的外部环境引入可以是全局环境,也可以是其它函数环境,这个根据实际代码而来;(声明性)环境记录包含了用户在函数中定义的所有(使用let、const声明的)属性方法,和一个arguments对象。

创建变量环境组件

变量环境可以说也是词法环境,它具备词法环境所有属性,一样有环境记录外部环境引入。在ES6中唯一的区别在于:

  • 词法环境用于存储函数声明与let const声明的变量

  • 变量环境仅仅存储var声明的变量。
    在这里插入图片描述
    举个栗子

let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20, 30);

在这里插入图片描述
不知道你有没有发现,在执行上下文创建阶段,函数声明与var声明的变量在创建阶段已经被赋予了一个值,var声明被设置为了undefined函数被设置为了自身函数,而let const被设置为未初始化。

现在你总知道变量提升与函数声明提前是怎么回事了吧,以及为什么let const为什么有暂时性死域,这是因为作用域创建阶段JS引擎对两者初始化赋值不同。

执行上下文执行阶段

代码执行时根据之前的环境记录对应进行赋值

比如早期var在创建阶段为undefined,如果有值就对应赋值;

像let const值为未初始化,如果有值就赋值,无值则赋予undefined。

案例分析

【案例】

   <script>
      var scope = "global"; 
      function fn1(){
         return scope; 
      }
      function fn2(){
         return scope;
      }
      fn1();
      fn2();
   </script>

上面代码执行如下:
在这里插入图片描述

【案例】

function foo(i) {
    var a = 'hello';
    var b = function privateB() {
		...
    };
    function c() {
    	...
    }
}
foo(22);

对于上面的代码,可以用下面的伪代码来描述执行上下文的创建过程:

在这里插入图片描述
【案例】

var a  = 1
let b = 2
function foo(i) {
    var a = 'hello';
    let d = 2 
    var b = function privateB() {
		...
    };
    function c() {
    	...
    }
}
foo(22);

执行环境创建的分析图解如下:

在这里插入图片描述

【案例】

(function(){
    console.log(bar);
    console.log(baz);
    
    bar = 20;
    console.log(window.bar);
    console.log(bar);
    
    function baz(){
        console.log("baz");
    }
    
})()

运行这段代码会得到"bar is not defined(…)"错误。当代码执行到"console.log(bar);“的时候,会去AO中查找"bar”。但是,根据前面的解释,函数中的"bar"并没有通过var关键字声明,所有不会被存放在AO中,也就有了这个错误。

注释掉"console.log(bar);",再次运行代码,可以得到下面结果。"bar"在"激活/代码执行阶段"被创建。
在这里插入图片描述
【案例】

<script>
    var c = 1;
    function c(c){
        var c = 2;
        console.log(c);
    }
    c(3);
</script>

运行结果: c is not a function
上面的代码相当于如下所示:

<script>
    //变量提升
    var c; 
    //函数提升
    function c(c){
        var c = 2;
        console.log(c);
    }
    c = 1;
    c(3);
</script>

在这里插入图片描述

【案例】运行下面的代码,会依次输出什么,创建了几个执行上下文

<script>
    console.log('gb begin:'+i);
    var i = 1;
    foo(1);
    function foo(i){
        if(i == 3){
            return;
        }
        console.log('foo() begin:'+i);
        foo(i+1);
        console.log('foo() end:'+i);
    }
    console.log('gb end:'+i);
</script>

在这里插入图片描述

执行结果:

gb begin:undefined
demon.html:38 foo() begin:1
demon.html:38 foo() begin:2
demon.html:40 foo() end:2
demon.html:40 foo() end:1
demon.html:42 gb end:1

在这里插入图片描述

闭包

   <script>
      function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){//注:i是outer()的局部变量
            result[i] = function(){
               return i;
            }
         }
         return result;//返回一个函数对象数组
         //这个时候会初始化result.length个关于内部函数的作用域链
      }
      var fn = outer();
      console.log(fn[0]());//result:2
      console.log(fn[1]());//result:2
   </script>

返回结果很出乎意料吧,你肯定以为依次返回0,1,但事实并非如此
来看一下调用fn0的作用域链图:
在这里插入图片描述
可以看到result[0]函数的活动对象里并没有定义i这个变量,于是沿着作用域链去找i变量,结果在父函数outer的活动对象里找到变量i(值为2),而这个变量i是父函数执行结束后将最终值保存在内存里的结果。
由此也可以得出,js函数内的变量值不是在编译的时候就确定的,而是等在运行时期再去寻找的

那怎么才能让result数组函数返回我们所期望的值呢?

看一下result的活动对象里有一个arguments,arguments对象是一个参数的集合,是用来保存对象的。
那么我们就可以把i当成参数传进去,这样一调用函数生成的活动对象内的arguments就有当前i的副本。
【改进之后:】

   <script>
      function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){
            //定义一个带参函数
            function arg(num){
               return num;
            }
            //把i当成参数传进去
            result[i] = arg(i);
         }
         return result;
      }
      var fn = outer();
      console.log(fn[0]);//result:0
      console.log(fn[1]);//result:1
   </script>

在这里插入图片描述

虽然的到了期望的结果,但是又有人问这算闭包吗?调用内部函数的时候,父函数的环境变量还没被销毁呢,而且result返回的是一个整型数组,而不是一个函数数组!
确实如此,那就让arg(num)函数内部再定义一个内部函数就好了:
这样result返回的其实是innerarg()函数

   <script>
      function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){
            //定义一个带参函数
            function arg(num){
               function innerarg(){
                  return num;
               }
               return innerarg;
            }
            //把i当成参数传进去
            result[i] = arg(i);
         }
         return result;
      }
      var fn = outer();
      console.log(fn[0]());
      console.log(fn[1]());
   </script>

当调用outer,for循环内i=0时的作用域链图如下:
在这里插入图片描述

由上图可知,当调用innerarg()时,它会沿作用域链找到父函数arg()活动对象里的arguments参数num=0.
上面代码中,函数arg在outer函数内预先被调用执行了,对于这种方法,js有一种简洁的写法

    function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){
            //定义一个带参函数
            result[i] = function(num){
               function innerarg(){
                  return num;
               }
               return innerarg;
            }(i);//预先执行函数写法
            //把i当成参数传进去
         }
         return result;
      }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值