js是词法分析型的链式作用域

前言

    js的作用域链一直是一个理论上看似简单,但实际使用中又经常出错的知识点。想要在开发中规避一系列问题,就有必要在基础理论,尤其是编译原理的角度去理解js作用域的产生以及结构。下面就笔者的一些学习经验简单做出总结,行文较为粗略,如有不懂可以留言。这里先抛出一句话“js是词法分析型的链式作用域”,具体怎么回事下文会讲解。

1、执行环境

    这是一个非常重要的概念,执行环境定义了变量或者函数有权访问的其他数据,决定了他们的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有函数都保存在这个变量对象中。全局执行环境是最外层的执行环境。

    实际中,每一个函数内部都有自己的执行环境,执行流进入每一个函数时,都会把该函数的环境推入一个执行栈,结束再弹出。

2、作用域链&执行栈

    所谓的作用域链是一个指向执行环境中变量对象的指针列表,只引用但不包含变量对象。根据上文的意思,假设执行到一个函数foo,执行流需要将该函数的执行环境推入执行栈中,而在这个过程中,解析器会创建foo函数的作用域链(而真正的作用域结构在书写代码时已经由词法作用域规则决定好,后文继续讲解)。

    作用域链如之前所说,是一个指针列表,那么这个列表包含了什么呢?其实是从头部开始,依次包含了当前foo函数的变量对象(包括foo的局部变量、属性、方法等)、foo函数外层函数(假设有)的变量对象、......、直到全局作用域的变量对象(也就是window)。因此解析器在生成这个列表后,执行函数内部代码时,就可以按照这个链式的结构、一层一层地递进查找,直到找到全局作用域。

    这时我们可以发现,作用域链的用途,其实就是是保证对执行环境有权访问的所有变量、函数的有序访问。链的最前端始终是该函数的活动对象,然后是外层环境,然后再外层,直到全局,这个过程称为标识符解析。但任何执行环境都不可向下搜索(如果需要这样做,这里就需要闭包了,此处不做过多说明)。

3、词法分析

    如果看过《你不知道的JavaScript》,应该记得该书的第一章第一节就详细的讲解了js的编译原理和词法分析原理。这里简要说明一下。

    首先我们经常说js是解释型的语音而非编译型,这里主要是因为js往往由解析器来逐行解析并执行,但是事实上它依然是一种编译语言,只不过不像c这种可以提前编译成二进制文件并且可以移植使用,js依赖于解析器和引擎的分析,但是在这个过程中依然包含了传统编译语言的几个步骤:词法分析、语法分析、代码生成。

    拿那个最经典的var a = 2做例子,如果按照逐行解析的逻辑去看,好像到这里就是声明a然后直接赋值为2,但实际中并不是,而是编译器先声明一个变量a,默认值为undefined,系统开辟一块内存给这个变量。这个阶段其实就是将所有变量声明都完成,而不执行其他操作。

    等到达代码生成阶段,会产生二进制机器码给引擎来执行,而在这个过程中发现a=2的指令后,再去询问作用域链,查找a的有效位置,然后执行赋值。

    那么我们回到词法分析和作用域链的关系上来。由于编译器在第一阶段已经将所有的变量声明都词法化,所以作用域链的结构会完全依赖于声明变量或者函数时的代码嵌套结构。一句话就是,js的作用域链关系是根据书写时的代码结构所决定的,而不是动态查找的。这一点非常重要,接下来我们举个例子。

    

    这里虽然foo函数在bar的内部被调用,但是因为声明foo的时候是在全局作用域,也就是说foo的作用域链顶端只包含了全局作用域,因此即使是在别的函数内部被调用,也会按照声明函数时候的作用域链去查找到全局变量a值为2,而不是按照这段代码直观上的意思去bar里面找到局部变量a然后输出3!再看一个例子:

    

    这回我们将bar函数的局部变量直接改为b,然后foo打印b。直观上看总觉得在内部执行foo时应该会向上查找到b然后输出3。但是依然是错的,因为js是按照词法作用域查找变量的,foo只会向定义时所在的全局作用域查找b,最终没有找到,因此输出信息直接报错!

4、作用域是在声明函数时还是执行函数时创建?

    有了上面的理论学习,这个问题其实就很好回答了,显然作用域的真正结构是由函数声明时就已经决定好的。实际上在内部创建函数时,会创建一个预先包含全局变量对象的作用域链,并保存在内部的[[Scope]]属性中;当调用函数被时会创建执行环境,然后复制Scope属性构建作用域链,并将函数的活动对象推入前端。

    因此实际中作用域链这个对象结构的生成是在执行阶段,但是真正的作用域则是在声明时就已经确定的。

5、var a = b式赋值?

    (function() {
       var a = b = 5;
    })();
    console.log(b);//5!!!

    这里实际经历的过程时对a进行局部变量声明,但是由于b没有声明关键字,所以默认声明为全局变量,所以可以在外部打印。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值