作用域、执行上下文和闭包

235 阅读11分钟

作用域和作用域链

作用域
作用域是指定程序源代码定义变量的区域。
作用域规定了如何查找变量,也就是当执行代码对变量的访问权限。
Javascript采用词法作用域(lexical scoping), 也就是静态作用域(定义时,就已经确定作用域 )

作用域链
在《Javascript 深入之变量对象》中讲到,当查找变量的时候,会先从当前上下文的变量中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样有多个执行上下文的变量对象构成的链表就叫做作用域链。

静态作用域和动态作用域

因为JS采用的是词法作用域,函数的作用域在函数定义的时候就决定了。这是因为函数有一个内部属性[[scope]],当函数创建的时候,就会保存所有父变量对象到其中,我们可以理解为[[scope]]就是所有父变量对象的层级链,但是注意:[[scope]]并不代表完整的作用域链。
而与词法作用域相对的是动态作用域,动态作用域是在函数调用的时候才决定的。

var value = 1;
function foo() {
    console.log(value);
}
function bar() {
    var value = 2;
    foo();
    console.log(value);
}
bar(); // 1 2

JS采用静态作用域,执行过程如下:
执行foo函数,先从foo函数内部查找是否有局部变量value,如果没有,根据声明的位置,查找上面一层的代码,也就是value = 1;若以最后打印1。

假设JS采用动态作用域,执行过程下:
执行foo函数,先从foo函数内部查找是否有局部变量value。如果没有,就从调用的地方往上层查找,也就是bar函数内部查找value变量,所以输出会是2。

执行上下文

简而言之,执行上下文是评估和执行JS代码的环境的抽象概念,每当JS代码在运行的时候,他都是在执行上下文中运行。

执行上下文的类型

js中有三种执行上下文类型。

  • 全局执行上下文。这是默认或者说基础的上下文,任何不再函数内部的代码都在全局上下文中。它会执行两件事儿:创建一个全局的window对象(浏览器情况下),并且设置this的值等于这个全局对象,一个对象中只会一个全局执行上下文。
  • 函数执行上下文。每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有他自己的执行上下文,不过是在函数被调用时创建的,函数上下文可以有任意多个,当一个新的执行上下文被创建,他会按定义的顺序执行一类步骤。
  • Eval函数执行上下文。执行在eval函数内部的代码也会有它属于自己的执行上下文。

执行栈

执行栈,是一种拥有LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

当JS引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈,每当引擎遇到一个函数调用,他会为该函数创建一个新的执行上文并压入栈的顶部。

引擎会执行哪些执行上下文位于顶部的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

怎么创建执行上下文

上文中知道了JS怎么执行上下文,现在我们学习JS引擎是怎么创建执行上下文的。
创建执行上文有两个阶段

  1. 创建阶段
  2. 执行阶段

The Create Phase

在JS执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事。

  1. this值的绑定
  2. 创建词法环境组件
  3. 创建变量环境组件

所以执行上下文在概念上表示如下:

ExecutionContext = {
    thisBinding = <this value>,
    LexicalEnvironment = { …… },
    VariableEnvironment = { …… }
}

this 绑定

在全局执行上下文中,this的值指向全局对象。(在浏览器中,this引用Window对象)。

在函数执行时,this值取决与该函数如何被调用的。如果他是被一个引用对象的调用,那么this会被设置成那个对象,否则this的值被设置为全局对象或者undefiend(严格模式下)。

词法环境

它在ES6官网文档定义为:词法环境是一种规范类型,给予ECMAScript代码的词法嵌套结构来定义标记符和具体变量和函数的关联。一个词法环境有环境记录器和一个可能的引用外部词法环境的空值组成。

简单来说词法环境是一种持有标识符-变量映射的结构。这里的标识符值得是变量/函数的名字,而变量是对实际对象[包含函数类型对象]活原始数据的引用。
现在,在词法环境的内部有两个组件:

  1. 环境记录器。存储变量和函数声明的实际位置。
  2. 一个外部环境的引用。意味着它可以访问其父级词法环境(作用域)

词法环境的两种类型:

  • 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是null。它拥有内建Object/Array等、在环境记录器内的原型函数(关联全局对象,比如window)还有任何用户定义的全局变量,并且this的值指向全局对象。
  • 函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任意包含次内部函数的外部函数。

环境记录器也有两种类型(如上):

  • 声明式环境记录器存储变量、函数和参数
  • 对象环境记录器用来定义出现全局上下文中的变量和函数的关系。

简而言之:

  • 全局环境中,环境记录器是对象环境记录器
  • 在函数环境中,环境记录器是声明式环境记录器。

对于函数环境,声明式环境记录器还包含了一个传递给函数的argumemts对象(此对象存储索引和函数的映射)和传递给函数的参数的length。

抽象地讲,词法环境在为代码中看起来像这样。

GlobalExectionContext = {
    LexicalEnvironment: {
        EnvironmentRecord: {
            Type: 'Object',
            // 在这里绑定标识符
        },
        outer: <null>
    }
}

FunctionExectionContext = {
    LexicalEnvrionament: {
        EnvironmentRecord: {
            Type: 'Decalarative',
            // 在这里绑定标识符
        },
        outer: <Global or outer function environment reference>
    }
}

变量环境

它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

如上所述,变量环境是一个词法环境,所以它有着上面定义的词法环境的所有属性。

在ES6中,词法环境变量环境的一个不同就在于前者用来存储函数声明和变量(let 和 const)绑定,然后后者用来存储var变量绑定。

let a = 20;
const b = 30;
var c ;
function multiply(e, f) {
    var g = 20;
    return e + f + g;
}
c = mutiply(a, b);

执行上下文看起来是这样的:

GlobalExectionContext = {
    thisBinding: <Global Object>,
    LexicalEnvironment: {
        Envrionment: {
            Type: 'Object',
            
            // 这里绑定标识符
            a: < uninitialized >,
            b: < uninitialized >,
            multipy: <func>
        },
        outer: <null>
    },
    VaribleEnvironment: {
        EnvironmentRecord: {
            Type: 'Object',
            // 这里绑定标识符
            c: undefiend,
        },
        outer: <null>
    }
};

FunctionExectionContext = {
    thisBindding: <Global Object>,
    LexicalEnvironment: {
        EnvironmentRecord: {
            Type: 'Declerative',
            // 这里绑定标识符
            Arguments: {0: 20, 1: 30, length: 2}
        },
        outer: <GlobalexicalEnvironment>
    },
    VariableEnvironment: {
        EnvironmentRecord: {
            Type: 'Declarative',
            g: undefiend
        },
        outer: <GlobalLecicalEnvironment>
    }
}

只有调用函数mutiply时,函数执行上下文才会被创建。

上面示例代码中letconst定义的变量并没有关联任何值,但var定义的被设成了undefiend。这是因为在创建阶段是,引擎检查代码中出变量和函数声明,虽然函数声明完全存储在环境中,但是变量值最初设置为undefiend(var情况下),或者未初始化(letconst情况下)。
这就是为什么你可以在声明之前访问var定义的变量,但是声明之前访问let和consr的变量会得到一个引用错误。这就是我们说的变量声明提升。

执行阶段

在这个阶段中,完成对所有变量的分配,最后执行代码

在执行阶段,如果JS引擎不能在源码中声明的实际位置找到let变量的值,他会被赋值为undefiend

思考题

// A
var scope = 'global scope';
function checkscope() {
    var scope = 'local scope';
    function f () {
        return scope;
    }
    return f();
}
checkscope(); // 'local scope'
// B
var scope = 'global scope';
function checkscope() {
    var scope = 'local scope';
    function f () {
        return scope;
    }
    return f;
}
checkscope()(); // 'local scope'

解析:

  1. 进入全局环境上下文,全局环境压入执行栈,contextStack=[GlobalContext],全局上下文环境初始化。
GlobalContext = {
    thisBinding: <Global Object>,
    LexicalEnvironment: {
        Environment: {
            Type: 'Object',
            // 这里是声明标识
            checkscope: <func>
        },
        outer: null
    },
    VariableEnvironment: {
        Environment: {
            Type: 'Object'.
            scope: undefiend
        }
    }
}
  1. 执行checkscope,进入checkscope函数上下文,checkscope被压入环境栈,contextStack = [CheckscopeConext, GlobalContext], 创建其的执行上下文。
// checkscope
CheckscopeConext = {
    thisBinding: <Global Object>,
    LexicalEnvironment: {
        Environment {
            Type: 'Object',
            f: <func>,
            Arguments: []
        },
        outer: <GlobalContext>
    },
    VariableEnvironment: {
        Environment: {
            Type: 'Object',
            scope: undefined
        },
        outer: <GlobalContext>
    }
}

3、函数f被初始化,f.[[scope]] = checkscope.scopeChain,继续往下走到return f(), contentStace=[FContext, CheckscopeConext, GlobalContext], 创建其的执行上下文。

FContext = {
    thisBinding: <Global Object>,
    LexicalEnvironment: {
        Environment: {
            Type: 'Object',
            Arguments: []
        }
        outer: <CheckscopeExectionEnvironment>
    },
    VaribaleEnvrionment: {
        Environment: {
            Type: 'Object'
        },
        outer: <CheckscopeExectionEnvironment>
    }
}

4、函数f执行完毕,f的上下文从环境栈中弹出。 5、checkscope函数执行完成,其上下文中环境栈中弹出。

上述是A的执行流程,那么B的流程在细节上基本一直,唯一的去吧在与B的环境栈变化不一样

A:contextStack = [GlobalContext] ----> contextStack = [CheckscopeContext, GlobalContext] ----> contextStack = [FContext, CheckscopeContext, GlobalContext] ----> contextStack = [CheckscopeContext, GlobalContext] ----> contextStack = [GlobalContext]

B: contextStack = [GlobalContext] ----> contextStack = [CheckscopeContext, GlobalContext] ----> contextStack = [FContext, GlobalContext] ----> contextStack = [GlobalContext]

对于理解这两段代码而言最最根本的一点在于,javascript是使用静态作用域的语言,它的作用域在函数创建的时候边已经确定(arguments).

闭包

闭包是一个可以访问外部作用域的内部函数,即使这个外部作用域已经执行结束。

闭包 VS 纯函数

闭包即使哪些引用了外部作用域变量的函数。
为了更好的理解,我们将内部韩式拆成闭包和纯函数两个方面:

  • 闭包是哪些引用了外部作用域中变量的函数。
  • 纯函数是哪些没有引用外部作用域的函数,他们通常反悔一个值并且没有副作用。

返回函数的函数

let value = 1;
function createAdd() {
    function addNumbers(a, b) {
        let res = a + b;
        return res;
    }
    return addNumbers;
}
let add = createAdd();
let sum = add(value, 3); // 4
console.log('sum==========', sum);

执行步骤拆分:

  1. 首先创建了一个变量value并赋值为1。
  2. 在全局执行上下文中声明了一个函数createAdd,函数内部继续声明了一个函数,这个函数的定义存储在createAdd中。
  3. 接下定义了一个add变量,暂时,值为undefined
  4. =的右侧调用了createAdd函数,查找全局执行上下文的内存中名为createAdd的变量,在2中已经创建了这个函数,所以我们执行它。
  5. 执行函数时,创建一个新的createAdd执行上下文,并在createAdd执行上下文创建自有变量。JS引擎将createAdd执行上下文填加到调用栈中。
  6. createAdd中有一个新的函数声明,创建了addNumbers变量。addNumbers只存在于createAdd执行上下文中.
  7. createAdd函数返回了addNumbers的内容。JS在当前执行上下文找到了addNumbers相关内容,并将其返回。
  8. 返回的同时,createAdd执行上下文被销毁。addNumbers变量不再存在。但addNumbers函数定义仍然存在,赋值给了add变量。
  9. 接下来,我们定义了一个新的变量,暂时赋值为undefined
  10. 接下来执行一个名为add的函数,我们在全局执行上下文中查找它,找到了它的定义,这个函数需要两个参数。
  11. 接下来我们查找这两个参数,第一个参数是我们在1中定义的变量value,它的值是1,第二个参数是3.
  12. 现在来执行这个函数,这是创建 add的执行上下文,在它的执行上下文创建了两个变量ab。它们分别赋值为13
  13. add 函数中又定义了一个新的变量res。将变量ab相加得3并赋值给res变量。
  14. res变量从该函数反悔。add函数的执行上下文被销毁,并从调用栈中删除,变量abret不再存在。
  15. 返回的值被分配给我们前面定义的 sum变量。
  16. 如预期,控制台打印3