linux 安装包 在此作用域中尚未声明_老生常谈,来深入聊聊JS作用域

前言

最近重拾了一下JavaScript的一些基础,作用域就是其中一个比较重要的概念。理解它有助我们更好理解JavaScript代码的执行。我们知道,储存变量值几乎是所有编程语言最基本的功能之一,可以对变量的值进行访问和修改,正是这种储存和访问变量的能力使得程序有了状态。那么变量是如何储存,并且是如何去访问它们的?这些问题也说明了我们需要一套设计良好的规则来存储变量,并且之后可以方便的访问它们。没错,这套规则就是我们所说的作用域了。下面我们探讨一下,JavaScript是如何设置这些作用域规则的。

先讲讲编译

JavaScript是一门动态编译的语言,在讲作用域之前,简单的讲讲传统编译语言一般会经历的三个步骤:

  • 词法分析
    此过程会将由字符组成的字符串分解成有意义的代码块,即所谓的词法单元(token)。例如,var a= 1;这句简单的赋值语句,会被分解成:var、a、=、2、;等这些词法单元。结构大致如下,会生成tokens数组,其中每个token是词法分析的最小单元,不能再分解
var a = 1;tokens:[    { "type": "Keyword","value": "var" },    { "type": "Identifier","value": "a" },    { "type": "Punctuator","value": "=" },    { "type": "Numeric","value": "1" },    { "type": "Punctuator","value": ";" }]
  • 语法分析(Parse)
    此过程是将词法单元流即上面的tokens数组,转换成一个由元素逐级嵌套所组成的程序语法结构的树,即抽象语法树(AST)。在这个过程中会校验语法,有错误会抛出语法错误。

大致结构如下:

// AST的结构大致如下:{    type,    sourceType,    start,    end,    ...    // body是一个数组,包含多个内容块对象,即statement,类型大致有:变量声明,函数定义,if语句,while循环等。    body: [{        type,        start,        end,        kind,        // 变量的内容块,也是个数组        declarations:[            {                type,                start,                end                ...            }        ]    }]}var a = 1;// AST结构如下{    "type": "Program",  "start": 0,  "end": 12,  "body": [      "type": "VariableDeclaration", //变量声明      "start": 0,      "end": 10,      "declarations": [        {           "type": "VariableDeclarator", // 变量声明          "start": 4,          "end": 9,          "id": {            "type": "Identifier", //标识符            "start": 4,            "end": 5,            "name": "a"          },          "init": {            "type": "Literal",            "start": 8,            "end": 9,            "value": 1,            "raw": "1"          }        }      ],      "kind": "var"    }  ],  "sourceType": "module"}
  • 代码生成
    将AST转换为可执行的过程被称为代码生成,当然这个过程比较复杂,这里我们只是有个大概的意识,简单的讲就是可以将 var a = 1; 的AST转化为一组机器指令,用来创建一个叫做 a 的变量,分配内存并将一个值储存在a中。

这里讲编译只是大概的说明了解下代码大致编译过程,给我们有一个整体的代码运行的流程,简单了解引擎可根据需要创建存储变量即可,当然JavaScript编译过程要复杂得多,笔者也只是简单了解皮毛而已,没关系,我们这里讨论的重点还是作用域哈哈。

对于JavaScript来说,大部分情况下编译发生在代码执行前的几微秒,甚至更短的时间内,比如对简单的一句赋值语句 var a= 1,编译器会先进行编译,做好执行它的准备,然后会马上执行它。

理解作用域

1、三部曲

在JavaScript中对程序处理执行,一般是由引擎,编译器,以及作用域这三个配合完成的。

  • 引擎
    负责JavaScript程序的编译以及执行过程。
  • 编译器
    负责语法分析以及代码生成等。
  • 作用域
    负责收集维护所有声明的标识符(变量)组成的一系列查询,确定代码执行时对这些标识符的访问权限。

它们三者之间会协同工作,一起来完成JavaScript代码的执行。

比如处理var a = 1;这段简单的程序,大致是这么处理的:1、引擎编译执行的时候,遇到var a编译器去会询问作用域是否已有该变量存在其中,若有编译器会忽略该声明继续编译,否则会在作用域中声明一个新的变量,并命名为a。2、接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 1 这个赋值操作,引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫做 a 的变量,若有引擎就会使用这个变量,否则,引擎就会继续查找该变量,最终找到就会赋值给它,若没有找到则会抛出异常。

总结:变量的赋值操作会有两个步骤:1、编译器在当前作用域声明一个变量,已声明则忽略;2、运行时引擎会在作用域查找该变量,若找到就会进行赋值,否则抛出异常。

2、变量查询

在上述了解代码执行的大致过程,我们知道引擎在执行代码时会通过查找变量a来判断它是否已声明过,而查找的过程中由作用域进行协助的,但引擎进行怎样的查找,会影响最终的查找结果。

比如在上述例子中,引擎会为变量a进行LHS查询,还有另外一个查找类型叫做RHS。顾名思义,“L”和“R”的含义,分别是左侧和右侧,即当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。只是从字面这么讲,但不一定意味着就是赋值操作符的左侧或者右侧。

下面代码来讲讲LHS与RHS的区别:

var a = 1; // 明显是LHS查询console.log(a); // 这里对变量a的查询就是一个RHS查询

我们可以看到console.log(a)中的变量a,并没有赋予任何值,只是需要查找并取得a的值,才能将值传递给console.log();而对于a=1;来说,对a的查询则是LHS查询,这里我们可以理解成只是要为=1这个赋值操作找到一个目标

因此,我们可以进一步理解:去查找赋值操作的目标是谁就是LHS查询,而去查找已知目标(变量)是在哪里进行赋值操作的则是RHS查询

我们再看下面代码:

function foo(a) {    console.log(a);}foo(2); // 2

这段代码首先声明了一个函数foo,接着调用它,这里的函数调用就是对foo进行RHS查询,意思是去查找foo的值,并把它给我,并且它最好是一个函数类型的值

其实这段代码也有LHS查询,那就是代码中隐式的a = 2赋值操作,因为在foo调用时,2被当作参数传递给foo函数,此时分配给了参数a,这里需要一次LHS查询,接着console.log(a)就是我们上面说的RHS查询。

接下来我们来捋一捋上面那段代码在执行时会发生什么:1、引擎会在当前作用域中对foo进行RHS查询;而编译器编译过程中已经声明了foo函数,于是引擎找到了foo;2、接着继续在当前作用域对a进行LRS查询编译器将a声明成了函数foo的一个形参了,于是也找到了a;3、以此类推,最终引擎完成了这段代码的解析执行。

3、作用域嵌套

接着我们在上面那段代码基础上再加一个变量:

function foo(a) {    console.log(a + b);}var b = 2;foo(2); // 4

上面代码中多了一个变量b,这里对b进行的是RHS查询,但此时无法在函数foo内部完成,不过可以在其上一级作用域即全局作用域中完成。我们可以得出一个规则:引擎从当前执行的作用域中开始查找变量,若找不到就会逐级往上查找,当抵达最外层作用域时,无论找没找到,查找过程都会停止。其实,这种嵌套作用域一级一级的查找就是我们常说的作用域链了。这种作用域链就好比一幢高楼,引擎执行所在的作用域就是当前楼层,LHS和RHS查询都会在当前楼层进行查找,若没找到,就会上一层楼层(作用域)去查找,直到顶层(全局作用域)。

3c594680d213567bdb60c8ac2587038e.png

异常

上面我们了解的LHS和RHS查询变量的一些简单规则,也了解到两种查询规则的行为是不一样的,尤其是变量还没有进行声明的情况下,如以下代码:

function foo(a) {    console.log(a + b);  b = a;}foo(2);

上面代码在对b进行RHS查询时时在所有作用域中是无法找到该未声明的变量的,此时引擎就会抛出ReferenceError异常。

相比较之下,当引擎执行LHS查询时,如果直到在全局作用域都没有找到该目标变量,那么在非严格模式下就会创建一个具有该名称的变量,并将其给到引擎,注意是在非严格模式下,如果是在严格模式下,则会跟RHS查询那样抛出一样的异常。

接下来,如果RHS查询找到一个变量,但是你执行了不合理操作,比如对一个非函数类型的值进行函数调用,或者引用null或undefined类型的值中的属性,引擎就会抛出TypeError异常。

总结

作用域其实就是一套规则,用于确定在哪里以及如何查找变量,如果查找的目的是对变量赋值,那么就会使用LHS查询,如果目的是获取变量的值,就会使用RHS查询。并且这两种查询都会在当前作用域中开始,找不到时会向上一层作用域继续查找目标变量,直到全局作用域为止。这样一层一层的查询作用域,构成了我们所说的作用域链。不成功的RHS查询会导致抛出ReferenceError异常,而不成功的LHS查询会导致自动隐式创建一个全局变量(非严格模式下),在严格模式下会同RHS查询一样抛出异常。

感谢您阅读我分享创作的文章,文章会同步在本人公众号【前端精神小伙】中,可关注阅读往期文章。

欢迎一起学习和交流,debug佳也将持续为大家分享更多前端技术干货。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值