javascript执行上下文、作用域与闭包(第一篇)---执行上下文

在这个系列文章里,我首先要说的是,闭包是和执行上下文,作用域是有紧密的联系的,不能单独的去理解闭包,否则就容易走入死胡同。

这篇讲执行上下文。

一、什么是执行上下文

我们可以将执行上下文看作代码当前运行的环境。代码的运行环境分为三种:

  1. 全局级别的代码 – 这个是默认的代码运行环境,一旦代码被载入,js引擎最先进入的就是这个环境
  2. 函数级别的代码 – 当执行一个函数时,运行函数体中的代码
  3. Eval的代码 – 在Eval函数内运行的代码(这个不常使用,也不推荐使用,故不作了解)

其实,主要就是全局执行上下文和函数执行上下文。下面举一个简单的例子:

这里写图片描述

上图中,一共用4个执行上下文,紫色边框括起来的部分代表全局的上下文;绿色边框括起来的部分代表person函数内的上下文;蓝色边框括起来的部分代表person函数内的firstname函数的上下文;橙色边框括起来的部分代表person函数内的lastname函数的上下文。

注意,不管什么情况下,只存在一个全局的上下文,该上下文能被任何其它的上下文所访问到。函数上下文的个数是没有任何限制的,每到调用执行一个函数时,js 引擎就会自动新建出一个函数上下文。在外部的上下文中是无法直接访问到其内部上下文里的变量的,但是内部上下文可以访问到外部上下文中的的变量。

二、执行上下文堆栈

在浏览器中,javascript引擎的工作方式是单线程的。也就是说,某一时刻只有唯一的一个事件是被激活处理的,其它的事件被放入队列中,等待被处理。下面的示例图描述了这样的一个堆栈:

这里写图片描述

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

三、执行上下文的建立过程

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

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

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

(executionContextObj = {
            variableObject: { /* 函数中的参数对象并给参数赋值, 内部的变量以及函数声明 */ },
            scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ },
            this: {} 
          }
四、执行上下文建立阶段以及代码执行阶段

上述第一个阶段即执行上下文建立阶段的具体过程如下:

当代码执行到当前上下文中的调用函数的代码的时候即在执行被调用的函数体中的代码以前,开始创建函数执行上下文。

  1. 进入执行上下文第一个阶段-建立阶段:

    A.建立VariableObject对象(简称VO):
    1.建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值
    2.检查当前上下文中的函数声明。每找到一个函数声明,就在VariableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用,如果上述函数名已经存在于VariableObject下,那么则忽略后面的函数声明。
    3.检查当前上下文中的变量声明。每找到一个变量声明,就在VariableObject下面用变量名建立一个属性,该属性值为undefined。如果变量属性名和函数属性名重复,则不建立新的变量。

    B.初始化作用域链

    C.确定上下文中this的指向对象

    执行上下文第二阶段–代码执行阶段:

  2. 执行函数体中的代码,一行一行地运行代码,给VariableObject中的变量属性赋值。

下面来看个具体的示例:

function foo(i) {
            var a = 'hello';
            var b = function B() {

            };
            function c() {

            }
        }

        foo(22);

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

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

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

 fooExecutionContext = {
            variableObject: {
                arguments: {
                    0: 22,
                    length: 1
                },
                i: 22,
                c: pointer to function c()
                a: 'hello',
                b: pointer to function B()
            },
            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.

问题1: 为什么我们可以在声明foo变量以前就可以访问到foo呢?

因为在上下文的建立阶段,先是处理参数声明,接着是函数的声明,最后是变量的声明。那么,发现foo函数的声明后,就会在variableObject下面建立一个foo属性,其值是一个指向函数的引用。当处理变量声明的时候,发现有var foo的声明,但是variableObject已经具有了foo属性,所以函数声明会忽略后来的变量声明。

问题2: 为什么bar是undefined呢?

因为bar是变量的声明,在建立阶段的时候,被赋予的默认的值为undefined。由于它只要在代码执行阶段才会被赋予具体的值,所以,当调用typeof(bar)的时候输出的值为undefined.

可以看到,在声明的时候,如果遇到同名的函数声明和变量声明,处理的优先级是不一样的,如下:

function foo(){};//函数声明
var foo=function(){}//函数变量,与var foo=1;都是声明了foo这个变量,属于变量声明

举两个具体的例子来说明这个问题,

函数声明和变量声明同名:

(function(){
    console.log(typeof foo); //function
    var foo;
    function foo(){}; //因为在执行上下文的准备阶段中先找函数声明,后找变量声明,所以取前面的函数声明,忽略后面的同名的变量声明,即在fooExecutionContext里会生成一句:foo: pointer to function c(),即foo的属性值为指向函数c()的指针

    foo = "foo";  //执行上下文的执行阶段,会对变量进行赋值,会把foo的值赋为"foo"
    console.log(typeof foo); //string.对foo的string赋值会覆盖掉foo的原来的属性值,它原来的属性值为指向函数c()的指针,现在在fooExecutionContext里名为foo的属性为foo: "foo";

})();

或者,两个变量同名,其中一个变量为函数。

function a(){

    alert(typeof foo);//undefined

    var foo=function(){

    };//同名的变量,只不过其中一个赋值为函数。按照在执行上下文的准备阶段中先找函数声明,后找变量声明,但两个都为变量,他俩优先级相同,js引擎默认取前面的声明,忽略后面的声明。故最终fooExecutionContext里会生成一句:foo: undefined。
    var foo="foo";

    alert(typeof foo);//string,因为执行上下文的执行阶段,会对变量进行赋值,这个就是按照代码的先后顺序执行了,所以typeof foo为string.
    };

 a();

最后总的来说,在建立执行上下文阶段,声明不重要,重要的是执行阶段的赋值,不管建立执行上下文阶段的时候一个属性的声明是怎样的,仍旧可以被赋值为不同于声明类型的值,这也就是为什么javascript是弱类型的语言了。

关于在执行上下文的第一阶段–准备阶段里变量的声明总结起来就是三条:

  • 函数形参在声明的时候已经指定其形参的值

  • 之前的函数声明会忽略之后再声明的同名声明,不管后来同名的声明是函数变量还是普通变量

  • 普通变量的声明,不会覆盖以前的同名声明,会被忽略此次声明

到这里,执行上下文就告一段落了,下一篇讲作用域。


下一篇: javascript执行上下文、作用域与闭包(第二篇)—作用域

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值