js 中的执行上下文

先看现象

demo1:
<script>
    console.log(a);//Uncaught ReferenceError: a is not defined
</script>

demo2:
<script>
    console.log(a); //undefined
    var a;
</script>

demo3:
<script>
    console.log(a); //undefined
    var a=10;
</script>

demo4:
<script>
    console.log(this);  // Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
</script>

demo5:
<script>
    console.log(f1);
    //function f1(){
    //  alert('marain');
    //}
    function f1(){
        alert('marain');
    }
</script>

demo6:
<script>
    console.log(f2);//undefined
    var f2=function(){
        alert('marain');
    }
</script>

Javascript中代码的运行环境分为以下三种:

  ● 全局级别的代码 - 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
  ● 函数级别的代码 - 当执行一个函数时,运行函数体中的代码。
  ● Eval的代码 - 在Eval函数内运行的代码。

我们可以将“执行上下文”看做当前代码的运行环境或者作用域

    var global = 'global';
    function one(){
        var one_fuc ='one_fuc';
        var two_fuc ='two_fuc';

        function  two(){
            var two_info='two_info';
            return two_info;                    
        }

        function  three(){
            var three_info='three_info';
            return three_info;  
        }
    }

这里写图片描述

上图中,一共用4个执行上下文。蓝色的代表全局的上下文;红色代表one函数内的上下文;绿色代表one函数内的另外两个函数的上下文。注意,不管什么情况下,只存在一个全局的上下文,该上下文能被任何其它的上下文所访问到。也就是说,我们可以在one的上下文中访问到全局上下文中的global变量,当然在函数two或者three中同样可以访问到该变量。

至于函数上下文的个数是没有任何限制的,每到调用执行一个函数时,引擎就会自动新建出一个函数上下文,换句话说,就是新建一个局部作用域,可以在该局部作用域中声明私有变量等,在外部的上下文中是无法直接访问到该局部作用域内的元素的。在上述例子的,内部的函数可以访问到外部上下文中的声明的变量,反之则行不通。那么,这到底是什么原因呢?引擎内部是如何处理的呢?

执行上下文堆栈

在浏览器中,javascript引擎的工作方式是单线程的。也就是说,某一时刻只有唯一的一个事件是被激活处理的,其它的事件被放入队列中,等待被处理。

我们已经知道,当javascript代码文件被浏览器载入后,默认最先进入的是一个全局的执行上下文。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行上下文堆栈的顶部。浏览器总是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,然后,进入其下的上下文执行代码。这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回到全局的上下文。

这里写图片描述

这里写图片描述

由此可见 ,对于执行上下文这个抽象的概念,可以归纳为以下几点:

  ● 单线程
  ● 同步执行
  ● 唯一的一个全局上下文
  ● 函数的执行上下文的个数没有限制
  ● 每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此。

执行上下文的建立过程

我们现在已经知道,每当调用一个函数时,一个新的执行上下文就会被创建出来。然而,在javascript引擎内部,这个上下文的创建过程具体分为两个阶段:

  1. 建立阶段(发生在当调用一个函数时,但是在执行函数体内的具体代码以前)
    ○ 建立变量,函数,arguments对象,参数
    ○ 建立作用域链
    ○ 确定this的值
  2. 代码执行阶段:
    ○ 变量赋值,函数引用,执行其它代码

实际上,可以把执行上下文看做一个对象,其下包含了以上3个属性:

executionContextObj = {
    variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ },
    scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ },
    this: {}
}

建立阶段以及代码执行阶段的详细分析

确切地说,执行上下文对象(上述的executionContextObj)是在函数被调用时,但是在函数体被真正执行以前所创建的。函数被调用时,就是我上述所描述的两个阶段中的第一个阶段 - 建立阶段。这个时刻,引擎会检查函数中的参数,声明的变量以及内部函数,然后基于这些信息建立执行上下文对象(executionContextObj)。在这个阶段,variableObject对象,作用域链,以及this所指向的对象都会被确定。

上述第一个阶段的具体过程如下:

  1. 找到当前上下文中的调用函数的代码
  2. 在执行被调用的函数体中的代码以前,开始创建执行上下文
  3. 进入第一个阶段-建立阶段:
    ○ 建立variableObject对象:
    i. 建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值
    ii. 检查当前上下文中的函数声明:
    ● 每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用
    ● 如果上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖
    i. 检查当前上下文中的变量声明:
    ● 每找到一个变量的声明,就在variableObject下,用变量名建立一个属性,属性值为undefined。
    ● 如果该变量名已经存在于variableObject属性中,直接跳过(防止指向函数的属性的值被变量属性覆盖为undefined),原属性值不会被修改。

○ 初始化作用域链
○ 确定上下文中this的指向对象

认真思考这个过程,你发现了什么???

4 进入第二阶段 代码执行阶段:
○ 执行函数体中的代码,一行一行地运行代码,给variableObject中的变量属性赋值。

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

在调用foo(22)的时候,建立阶段如下:

这里写图片描述

这里 b undefined 就知道了 为什么 var f1=function(){} 是undefined!!因为它相当于还是变量

由此可见,在建立阶段,除了arguments,函数的声明,以及参数被赋予了具体的属性值,其它的变量属性默认的都是undefined
一旦上述建立阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象如下:

fooExecutionContext = {
    variableObject: {
        arguments: {
        0: 22,
        length: 1
    },
    i: 22,
    c: pointer to function c()
    a: 'hello',
    b: pointer to function privateB()
    },
    scopeChain: { ... },
    this: { ... }
}

我们看到,只有在代码执行阶段,变量属性才会被赋予具体的值。

局部变量作用域提升的缘由

在函数中声明的变量以及函数,其作用域提升到函数顶部,换句话说,就是一进入函数体,就可以访问到其中声明的变量以及函数。这是对的,但是知道其中的缘由吗?相信你通过上述的解释应该也有所明白了。不过在这边再分析一下。看下面一段代码:

(function() {
    console.log(typeof foo); // function pointer
    console.log(typeof bar); // undefined
    var foo = 'hello',
        bar = function() {
        return 'world';
    };
    function foo() {
        return 'hello';
    }
}());

上述代码定义了一个匿名函数,并且通过()运算符强制理解执行。那么我们知道这个时候就会有个执行上下文被创建,我们看到例子中马上可以访问foo以及bar变量,并且通过typeof输出foo为一个函数引用,bar为undefined。

● 为什么我们可以在声明foo变量以前就可以访问到foo呢?
因为在上下文的建立阶段,先是处理arguments, 参数,接着是函数的声明,最后是变量的声明。那么,发现foo函数的声明后,就会在variableObject下面建立一个foo属性,其值是一个指向函数的引用。当处理变量声明的时候,发现有var foo的声明,但是variableObject已经具有了foo属性,所以直接跳过。当进入代码执行阶段的时候,就可以通过访问到foo属性了,因为它已经就存在,并且是一个函数引用。
● 为什么bar是undefined呢?
因为bar是变量的声明,在建立阶段的时候,被赋予的默认的值为undefined。由于它只要在代码执行阶段才会被赋予具体的值,所以,当调用typeof(bar)的时候输出的值为undefined。

这就很好的解释了变量提升,和为什么出现开头的demo的现象。!!!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值