前端面经高频手撕-作用域和词法环境

199 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情

在上一篇《初识闭包》中,我们初步认识了闭包的基础概念。在继续学习闭包时,我们绕不开作用域的概念,因为闭包依赖于词法作用域。 所以在此我们先系统的复习下作用域的概念。

何为作用域

先看下mdn的作用域的表述: 作用域是当前的执行上下文,值 (en-US)和表达式在其中“可见”或可被访问。如果一个变量 (en-US)或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行。

这里的值指的是变量赋值。在js中有两种值,一种是原始值,一种是引用值。

原始值即undefined,null,布尔,数字,字符串和Symbol。原始值在保存时保存的是实际值。原始值不可改变, 当我们进行改变的时候,实际上是一个重新赋值的过程。

引用值是由多个数值构成的对象 ,在js中我们是无法直接访问内存位置的,所以不能直接操作内存空间。在操作对象时,实际上是对该对象的引用进行操作,而不是对对象本身进行操作。

表达式

原始的表达式是最基础的原始值,例如

12;
'aa';

这就是最原始的表达式。

对象数组的初始化,函数的定义这类也是表达式。

[1,2,3]
{a:1,b:2}
function (){}

当有了上面的基础表达式之后,我们通过一个赋值表达式,将值和变量关联起来。即=

a=[1,2,3]

除此之外,还有逻辑表达式,算数表达式,关系表达式等。不做一一展开,所有一些最终与赋值表达式配合,赋予了变量。

父子关系

在作用域关系中,是存在父子关系的,即外部作用域和内部作用域,外部作用域视为内部作用的父作用域。 比较简单的我们可以看函数的作用域。

function out(){
    let outIndex=1
    
    function innert(){
        let innertIndex=2
        console.log(outIndex)
    }
    console.log(innertIndex)
}

在上面的例子中,存在两个函数,形成嵌套关系,其中外部的无法访问内部的,但是内部可以访问外部的。

这就是子作用域可以访问父作用域,而父作用域无法访问子作用域。

作用域的类型

常见作用域有全局作用域,局部作用域,块极作用域,函数作用域。其他非常规的可以参照有光的文章。

何为词法环境

现在关于闭包的解释中,我们会看到一句话:闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合

讲起词法环境,要先说下其和作用域的关系。

作用域其实是执行上下文的一部分,其概念来源于es3.在es3中,将执行上下文分为三个属性:

  • scope:作用域,也常常被叫做作用域链。
  • variable object:变量对象,用于存储变量的对象。
  • this value:this 值。

在es3的基础上执行时,会将上下文压入上下文栈,并保存作用域链到[[scope]]上, 以此作为上下文的后续使用。

到了es5之后,上述的三个属性,变为

  • lexical environment:词法环境,当获取变量时使用。
  • variable environment:变量环境,当声明变量时使用。
  • this value:this 值。

在新规范下,创新执行上下文时,生成如下的结构。

ExecutionContext = { 
    ThisBinding = <this value>, 
    LexicalEnvironment = { ... }, 
    VariableEnvironment = { ... }, 
}

可以看出来,在新规范不再把变量的关系,父子作用域保存在scope中,而是单独提出了词法环境作为数据的保存。

最后

在es2018之后,进一步对上下文环境做了规范,增加很多新的属性。但是因为遵循历史的原因,我们很多开发人员还是习惯的称呼为作用域,或者词法作用域。