什么是作用域
作用域是一组定义在何处储存变量以及如何访问变量的规则。
编译器javascript 是编译型语言。但是与传统编译型语言不同,它是边编译边执行的。编译型语言一般从源码到执行会经历三个步骤:
分词/词法分析
将一连串字符串打断成有意义的片段,成为 token(记号)。
解析
将一个 token 流(数组)转化为一个嵌套元素的树,即抽象语法树(AST)。
代码生成
将抽象语法树转化为可执行的代码。其实是转化成机器指令。
比如var a = 1的编译过程:
分词/词法分析: var a = 1这段程序可能会被打断成如下 token:var、a、=、1,空格保留与否得看其是否具有意义。
解析:将第一步的 token 形成抽象树:大致如下:
变量声明: { 标识符: a 赋值表达式: {
数字字面量: 1 } }
代码生成: 转化成机器命令:创建一个称为 a 的变量,并分配内存,存入一个值为数字 1。
理解作用域作用域就是通过标识符名称查询变量的一组规则。
代码解析运行中的角色:
引擎
负责代码的编译和程序的执行。
编译器
协助引擎,主要负责解析和代码生成。
作用域
协助引擎,收集并维护一张所有被声明的标识符(变量)的列表,并对当前执行的代码如何访问这些变量强制实施一组严格的规则。
比如var a = 1的运行:
编译器遇到var a,会首先让作用域去查询 a 是否已经存在,存在则忽略,不存在,则让作用域创建它;
编译器遇到a = 1,会编译成引擎稍后需要运行的代码;
引擎执行编译后的代码,会让当前查看是否存在变量a可以访问,存在则引用这个变量,不存在则查看其他其他。
上面过程中,引擎会对变量进行查询,而查询分为 RHS(right-hand Side)查询 和 LHS(left-hand Side)查询,它们根据变量出现在赋值操作的左手边还是右手边来判断查询方式。
RHS
变量在赋值的右手边时采用这种方式查询,查不到会抛出错误 referenceError
LHS
变量在赋值的左手边时采用这种方式查询,在非严格模式下,查不到会再顶层作用域创建这个变量
嵌套的作用域实际工作中,通常会有多于一个的作用域需要考虑,会存在作用域嵌套在其他作用域中的情况。
嵌套作用域的规则:
从当前作用域开始查找,如果没有,则向上走一级继续查找,以此类推,直至到了最外层全局作用域,无论找到与否,都会停止。
词法作用域作用域的工作方式一般有俩种模型:词法作用域和动态作用域。javascript 所采用的是词法作用域。
词法分析时词法作用域是在词法分析时被定义的作用域。
上述定义的潜在含义即:词法作用域是基于写程序时变量和作用域的块儿在何处被编写所决定的。公认的最佳实践是将词法作用域看作是仅仅依靠词法的。
查询变量:
引擎查找标识符时会在当前作用域开始一直向最外层作用域查找,一旦匹配到第一个,作用域查询便停止。
相同名称的标识符可以在嵌套作用域的多个层中被指定,这成为“遮蔽”。
不管函数是从哪里被调用、如何调用,它的词法作用域是由这个函数被声明的位置唯一定义的。
欺骗词法作用域javascript 提供了在运行时修改词法作用域的机制——with 和 eval,它们会欺骗词法作用域。实际工作中,这种做法并不被推荐,应当尽量避免使用。
欺骗词法作用域会导致更低下的性能。
引擎在编译阶段会对代码做许多优化工作,比如静态地分析代码。但如果代码存在 eval 和 with,导致词法作用域的不固定行为,这一切的优化都有可能毫无意义,所以引擎就会简单地不做任何优化。
eval
eval函数接收一个字符串作为参数,并在运行时将该字符串的内容在当前位置运行。
function foo(str, a) { eval(str); // 作弊! console.log(a, b); } var b = 2; foo("var b = 3", 1); //1,3