作用域,作用域链,活动对象,执行上下文,静态作用域等

执行上下文(execution context)

   为简单起见,有时也称为环境。它定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
以下是一个执行上下文的创建过程:

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

——–可暂时不读——–
在http://blog.csdn.net/liujie19901217/article/details/52225025 中详细提及了作用域和执行上下文的区别。

函数的每次调用都有与之紧密相关的作用域和执行环境。从根本上来说,作用域是基于函数的,而执行环境是基于对象的(例如:全局执行环境即window对象)。
换句话说,作用域涉及到所被调用函数中的变量访问,并且不同的调用场景是不一样的。执行环境始终是this关键字的值,它是拥有当前所执行代码的对象的引用。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

http://web.jobbole.com/83031/
这篇文章中写了很多执行上下文到底是如何运作的。

它就像一个容器,用来存储当前上下文中所有已定义或可获取的变量、函数等。位于最顶端或最外层的上下文称为全局上下文(global context),全局上下文取决于执行环境,如Node中的global和Browser中的window

Js本身是单线程的,每当有function被执行时,就会产生一个新的上下文,这一上下文会被压入Js的上下文堆栈(context stack)中,function执行结束后则被弹出,因此Js解释器总是在栈顶上下文中执行。在生成新的上下文时,首先会绑定该上下文的变量对象,其中包括arguments和该函数中定义的变量;之后会创建属于该上下文的作用域链(scope chain),最后将this赋予这一function所属的Object

就是说执行上下文可以说就是一个装了已定义或可获取的变量、函数等等的对象,查了一下执行上下文的建立过程,上面一段说在函数执行之时产生,其实它在函数建立之时就产生了,详见下文:
http://blog.csdn.net/hi_kevin/article/details/37761919

—可暂时不读—-

建立阶段的第一步,便是建立了一个叫variableObject对象

variableObject对象

活动对象

建立variableObject对象:
建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值
检查当前上下文中的函数声明:
每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用
如果上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖。

也就是说这个活动对象就是当前最近的这个被调用的函数或者域中的能访问的对象和方法。

那这个建立对象过程是如何的呢?
先了解一下编译和作用域:

编译

比如一个函数test(),

function test() {
 var a = 0;
 alert(a);
}
test();

在js中,调用它但是还没有执行时会发生以下过程。

编译:语法、词法解析等,确定作用域(非作用域链)

该部分主要参阅:https://segmentfault.com/a/1190000007991284

  • a)词法分析
    类似于人在读一篇文言文,此时需要先断句,有时候断句不同意思也就大不相同。var a = 0; 拆分成 var、a、=、0;

  • b)语法分析

    这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。

  • c)词法解析,就是将上面语法分析得到的AST转换成可执行代码。总而言之可以将上面的var a = 0;转换成一个机器指令,创建一个变量a ,为它分配内存,再将数字0存放在里面。

那以上这些步骤在js中,都是谁来负责完成的呢?

  • 引擎
    从头到尾负责整个 javascript 程序的编译及执行过程。

    简单地说,JavaScript解析引擎就是能够“读懂”JavaScript代码,并准确地给出代码运行结果的一段程序。

  • 编译器
    负责语法分析及代码生成。

接着,函数还没有被执行,还是在函数执行之前,此时需要明白作用域是什么,作用域和引擎,编译器都是“同事”。

作用域(scope):

首先,在js中,一切皆为对象。
作用域在《你不知道的javascript(上)》被定义为

负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

而在《javascript权威指南》第6版中,对于变量:

一个变量的作用域,是程序源代码中定义该变量的区域

作用域规定了对于变量或是方法的访问权限的代码空间。
而它的英语,scope,作为名词意思是机会;范围;眼界;我理解的通俗意义上说就是它是一个对变量或者是方法被访问的区域空间的大小的描述,控制数据被使用的界限,就是说变量和方法能够起作用的空间范围

比如:

     function test() {
        var a = 3;
     //其他代码 
    }
    console.log(a);

这个时候,在函数test中定义的a,在函数外部就不能被输出,因为a变量的作用域的控制之下,外部没有访问这个a的能力。而发生的错误是ReferenceError,这个错误翻译过来也就是引用错误。
因为作用域,a这个装值的容器没有定义就拿过来使用了,我理解的是:就像是你的作文里面提到了《银粉世家》说是张恨水先生的经典作品,但其实人家并没有写过,你就把它写在了你的文章里,此时也就是引用错误ReferenceError。

而这样的一个区域的范围大小也就是作用域的“大小”,上文提及它是取决于定义该变量或方法的区域。
作用域又分为全局作用域以及局部作用域。

全局作用域(Global Scope)

全局作用域:顾名思义,全局就是整个范围,整个空间都能访问到具有全局作用域的变量和方法。也就是在js代码内,任何的地方使用这个方法或是变量都是有定义的。

     var a = 1;
    function test() {
        console.log(a);
    }
    test();//1

在函数外面定义了一个变量a,函数内去输出它,运行函数会发现,这次可以在函数中访问到a了。
此时的a就是具有全局作用域,变量a的值定义在了最外层的函数。
此时的a也同样作为属性被添加到由浏览器内置的对象BOM这个接口提供的window对象上。

如下结果,和上面的片段输出都是1。

var a = 1;
    function test() {
        console.log(window.a);
    }
    test();//1

所有的全局变量和函数,在浏览器中,都是作为window对象的属性和方法创建的。写在最外层的变量和方法,就是全局的。还有不加’var’声明的变量。

局部作用域 (Local Scope)

局部作用域:局部就是部分范围,空间才能访问到,这样的变量和方法是有着局部作用域的
就像最开始的那个定义在函数中的a,在外部就没有办法读取并输出它,因为它的生存范围在函数test中。
似乎能感觉出来函数好像有个”阻挡外界的屏障 ”,这就是因为函数也有作用域,称为函数作用域,也是局部作用域的一种。
在js的ES6之前,js是没有块级作用域的,就是像if这些有花括号的语句就被分为一个块,里面声明的变量外面访问不到。而只有函数作用域,变量在声明它们的函数体以及这个声明的函数体内嵌套的其他函数体内都是有定义的。
如下:这样的if就没办法像函数一样拒绝外部对其变量的访问。

        var a = 1;
        if(a===1) {
            var b = 0;
        }
        console.log(b);//0

作用域,引擎,编译器相互的作用

*明白了作用域是什么,那么作用域是怎么和引擎它们配合的呢?
比如var a = 0;
  首先第一步,先准备好容器 var a ;编译器先问作用域,有没有一个变量名字为a已经存在在你那边,如果有,编译器就继续编译,如果没有,它就让作用域添加一个,此时a的默认初始是undefined,编译器执行的查询是叫RHS。
  第二步,有了容器之后,a=0的操作。编译器会准备好引擎工作时候的代码,然后引擎来问作用域,有没有一个a的变量啊。如果有,引擎就直接使用这个已经存在的变量,给它赋值0,如果没有就会向这个作用域的父级去找,一直到全局,还是找不到,此时如果不是严格模式,引擎就在全局给它加一个变量a,赋值为0,严格模式的话,就会报错了,而错误类型就是上文提过的ReferenceError,此时引擎进行的查询叫LHS。*

总结来说,变量的赋值会执行两个操作,首先编译器会在当前作用域声明一个变量(如果之前没有声明过),然后在运行时引擎会在当前作用域中查找该变量(找不到就向上一级作用域查找),如果能够找到就会对它赋值。

对变量进行赋值所执行的查询叫 LHS(就像是表达式左侧),试图找到容器本身。
找到并使用变量值所执行的查询叫 RHS(就像是表达式右侧),试图找到容器的值。
如果在作用域的操作中没有找到容器,会报错,ReferenceError,而如果找到了但是执行的操作不合理无效会报错,TypeError, 对象用来表示值的类型非预期类型时发生的错误。

变量提升与预编译

在上面提到创建活动对象的时候,会需要检查当前上下文的参数,找到函数声明等,这两个过程就是在进行预编译。
js的代码并不是像我们人眼读的时候一样一行一行往下去第一次读取并执行。
比如下面:

 function test() {
        alert(a);
    }
    test();

此时会发生错误,也就是上文提到过的ReferenceError。

 function test() {
        alert(a);
        var a = 0;
    }
    test();//undefined

此时未定义,大家都知道,js在给声明了的变量,函数,但是没有赋初始值的时候,是默认填入了一个undefined作为初始值的。
也就是说读取test函数的时候,一开始就已经有了a这样一个变量。

变量提升也就是这个意思,在活动对象填充的过程中,先把变量和函数“认识”,对于var a = 0;其实是var a和a=0,它拆分成了两个这样的步骤。而函数test(),也是读取了这个函数的名字,赋值为undefined,而函数名又其实是对内存地址的引用,我理解这个过程就是划了一块内存空间给这个函数,用这个函数名test指向这样一个地址,这个内存空间是用栈的方式,而这个空间的描述就是作用域。

在执行上下文建立中的第一个环节,此时是函数还没有运行之前,通过引擎,作用域,编译器的相互配合,完成了预编译,把函数的变量和方法全部都“检验”,并且填入了合适的值,存放在了活动对象中。
接来下需要初始化作用域链:

作用域链(scope chain)

   简单按字面来说,作用域链就是由作用域组成的链,它是一个单向的链表结构
  在js中,函数也是对象,它是很多属性的集合。其中ECMA-262标准第三版定义了这样一个属性[[Scope]],FireFox的几个引擎(SpiderMonkey和Rhino)提供了私有属性_ parent _来访问它,这个属性包含了函数被创建时的作用域中对象的集合。而作用域链便是这个集合的一个指针列表。尽管是所有对象都有这个属性,但是只有函数,这个属性才有用。

作用域链只是一个指向变量对象的指针列表。它只是引用实际并不包含变量对象。

在一个函数被定义的时候, 会将它定义时刻的scope chain链接到这个函数对象的[[scope]]属性.

  就是说比如在全局下创建一个函数test,它存在一个属性[[scope]],这个属性这时就链接指向了一个集合名字叫作用域链,英文是scope chain。在函数被创建的时候因为是在全局下,这个集合这时候就加入了对所有的全局变量和方法的指向,如window等。
作用域,作用域链,活动对象,执行上下文,静态作用域等_第1张图片

当函数调用,但是还没有执行的时候,test的执行环境通过复制这个函数的[[scope]]属性中的对象,然后加上这个函数的活动对象作为这个scope chain链的顶端,这样就构成了一个执行环境的作用域链。

当调用这个函数时,通过复制test函数中预先生成的[[scope]]属性的对象,也就是scope chain,然后:
上面分析了半天的活动对象,此时就发挥了它的作用,对它的引用将被加入到执行环境作用域链的顶端,也就是图中的scope chain下有两个格子,从上往下数第一个格子指向活动对象,第二个格子才是原来的全局对象们。此时text()的执行环境的作用域链就生成了。
也就是说在函数声明的阶段,此时scope chain已经有了公共的部分内容,而等到被执行前,新创建的执行环境的作用域链复制了公共的部分,然后才加上这个函数自己的部分,也就是活动对象

this设置后,代码就开始执行了。

此时可回过头往上将执行上下文的(暂时可不读)重看一遍。
当执行后,Js的上下文堆栈又将执行上下文弹出,把控制权交给之前的执行上下文,被弹出的这个执行上下文被销毁。活动对象也被销毁,默认情况下当函数返回时会销毁它的活动对象和作用域链。

词法作用域

词法作用域又叫静态作用域,相对应的是动态作用域。而大部分语言都是采用静态作用域。

   var a = 1;
    function add() {
        a = a + 9;
        alert(a);
    }
    function test() {
        var a = 0;
        add();//10
    }
    test();

比如上面的例子,如果采用的是动态作用域,那么add就应该是输出9。而静态作用域就是指在声明的时候,作用域就已经确定了。上面这个test,作用域链中应该是有底层window,上一个就是add的自身的一堆对象,也就是活动对象,那么a只会在这个线上去寻找,而动态作用域,就会去test里面找a,谁调用这个函数add,它的作用域链会把调用的test加入它的搜索计划。

你可能感兴趣的:(js深入理解)