理解作用域和作用域链


作用域在预编译阶段确定,但是作用域链是在执行上下文的创建阶段完全生成的。

执行上下文、变量对象

作用域需要引用执行上下文变量对象的概念,先简单了解如下。

在 ECMASscript 中的代码有三种类型:global, functioneval(依次为全局代码、函数内代码、eval 内代码)。我们的代码在执行过程中会形成执行上下文,一个执行上下文可以激活另一个上下文,就好比一个函数调用了另一个函数(或者全局的上下文调用了一个全局函数),然后一层一层调用下去。逻辑上来说,这种实现方式是栈,我们可以称之为上下文堆栈。当一段程序开始时,会先进入全局执行上下文环境 [global execution context], 这个也是堆栈中最底部的元素。此全局程序会初始化生成必要的对象 [objects] 和函数 [functions]。 在全局上下文执行的过程中,它可能会激活一些方法(已经初始化过的),然后进入他们的上下文环境并将新的元素压入堆栈。在这些初始化都结束之后,这个系统会因事件触发一些方法,然后进入一个新的上下文环境。活动的执行上下文在逻辑上组成堆栈,这些堆栈我们可以当做是一个待编译队列,它的活动或执行顺序影响着机器最终编译修改的结果是不是我们想要的结果相同。

变量对象 (VO) 是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的·变量 (var 变量声明)和·函数声明 (FD)。变量对象在每次进入上下文时创建,并填入初始值,值的更新出现在代码执行阶段。

执行上下文的代码被分成两个基本的阶段来处理:1. 进入执行上下文;2. 执行代码。变量对象的修改变化与这两个阶段紧密相关。

作用域

作用域指的是变量的适用范围,即在程序的执行上下文(可执行代码)中变量的可访问性。根据变量可访问范围作用域有全局作用域和局部作用域两种类型,在 EcmaScript 中局部变量只能通过“函数 (function)”代码类型的执行上下文创建。在函数内部定义的变量与内部函数,在外部非直接可见并且不污染全局对象。

全局作用域的变量对象可以在其它所有上下文环境中访问,拥有全局对象的变量包括:1、全局上下文中的函数和变量。2、未经定义直接赋值的声明。3、window 对象的所有属性。

注:当声明一个全局变量的时候,实际上是定义了全局对象 window 的一个属性。

var a = 1;
console.log(window.a);    //1

b = 2;
console.log(window.b);    //2
ecmascript(ES6 之前)中没有块级作用域,只有在函数中有局部作用域,且其变量在外部不可访问。

for(a=0;a<4;a++){
    
}

var b = 2;
function fun(){
    var c = 8;
    if(!b){
        var b = 6;
    }
    console.log(b);    
}
fun();    //6
console.log(a);    //4
console.log(b);    //2
console.log(c);    //报错 c is not defined

作用域链

在 EcmaScript 中没有静态作用域(没有私有属性和方法),不过它可以给构造函数即函数对象提供属性和方法,其中一个内部属性 [[Scope]] 属性 — 它包含了函数内部上下文所有变量对象(包括父变量对象)的集合,这个集合被称为函数的作用域链。此链用来变量查询。例如,当一个函数在自身函数体内需要引用一个变量,但是这个变量并没有在函数内部声明(或者也不是某个参数名),那么这个变量就可以称为自由变量 [free variable]。那么我们搜寻这些自由变量就需要用到作用域链。函数上下文的作用域链在函数调用时创建的,包含活动对象和这个函数内部的 [[scope]] 属性。在一个函数上下文中,变量对象被表示为活动对象 (activation object)。当函数被调用者激活,这个特殊的活动对象 (activation object) 就被创建了。它包含普通参数 (formal parameters) 与特殊参数 (arguments) 对象(具有索引属性的参数映射表)以及 this。活动对象在函数上下文中作为变量对象使用,被推入上下文最前端,执行完后被销毁。[[scope]] 是所有父变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。

var a = 1;
function fun(){
    var b = 2;
    
    function fn(){
        var c = 6;
        console.log(a+b+c);
    }
    fn();
}
fun();    //9

我们已经知道 ecmascript 会给函数提供 [[scope]] 属性 ,fn 函数在创建时获得 [[scope]] 属性,通过该属性访问到所有父上下文的变量。“fn”上下文的作用域链为:fnContext.Scope = [ fnContext.Ao + funContext.Ao + globalContext.VO ]。

在 ECMAScript 中,闭包与函数的 [[scope]] 直接相关。实际上,闭包是函数代码和其 [[scope]] 的结合。[[scope]] 在函数创建时被存储,当函数进一步激活时,在变量对象的这个词法链(静态的存储于创建时)中,来自较高作用域的变量将被搜寻。

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

function father(fn){
    var a = 2;
    (function(){
        fn();
    })();
}
father(fun);    //1;

函数可以作为参数被传递给其他函数使用 , 这里的 father 函数并不是 fun 函数的父作用域,fun 函数父作用域 globalContext 中取得 a 的值为 1。

在一般情况下,一个作用域链包括父级变量对象(variable object)(作用域链的顶部)、函数自身变量 VO 和活动对象(activation object)。在代码执行过程中,如果使用 with 或者 catch 语句就会改变作用域链。不推荐使用 with,在 ES5 严格模式中该标签已被禁止。推荐的替代方案是声明一个临时变量来承载你所需要的属性。


参:http://www.cnblogs.com/TomXu/archive/2012/01/18/2312463.html