在我的上一篇博客中我说道在编译器的第一个工作阶段:分词/词法分析也叫词法化,就是对编写的代码进行检查,如果是有状态的解析还会赋予单词语义。像这种在我们编写代码时,将变量和作用域写在哪里决定的就叫做词法作用域,当词法分析器处理代码时候会保持作用域不变(大部分情况下)。
相对的还有一种作用域叫做动态作用域,也就是在运行时就被动态确定的形式。在动态作用域中我们并不关心函数和作用域是如何声明的,只关心它们从何处调用。
接下来举个例子来说明这两个做作用域的区别:
function foo(){
console.log(a)
}
function bar(){
var a = 3;
foo();
}
var a = 1;
bar();
猜一下这一段js代码在不同的作用域情况下会输出什么?
词法作用域下:
会输出:1; 因为JavaScript的作用域是词法作用域(大部分是!!!,除了this),在bar中调用foo之后会去执行foo,而在foo中要出输出a,这个时候就要执行上一篇博客所说的,去执行一个RHS查找,因为在foo作用域中没有找到a,所以就去上级作用域去查找,于是就找到了a的值是1.
动态作用域下
会输出:3; 因为是在动态的作用域下,所以不关心函数和作用域是在何处声明的,只关心它们从何处调用的,换句话说,就是关心调用链和调用栈,当在foo中要去输出a的时候,因为foo中没有a,所以要顺着它的调用栈也就是调用它的地方去查找a,跟词法作用域不同的是它不是根据嵌套的词法作用域上找的,由于foo是在bar中调用的,所以引擎会去检查bar的作用域,并且找到了a,值为3。
作用域是严格包含的
上面我们说了两个作用域的区别,下面我们主要说说词法作用域。
作用域的嵌套
这个大家都知道,所以只说说细节
//全局作用域,里面有一个标识符 a
function aaa(a){
//包含aaa的作用域 有a,b,bbb三个标识符
var b = a + 1;
function bbb(c){
//包含bbb的作用域,有c一个标识符
console.log(a,b,c)
}
bbb(b+1)
}
aaa(1);// 1 2 3
从上面的代码可以看出作用域是层层嵌套的,并且是严格嵌套的,没有那个作用域会出现两个父级函数作用域
并且作用域的查找是从内往外的,会在找到第一个匹配的标识符时停止(这里不考虑找不到的情况),在多层的嵌套作用域中可以定义同名的标识符,叫做“遮蔽效应”。
欺骗词法
在上面我们说了词法作用域是在写代码的时候就确定的,但是也可以通过一些方法来欺骗词法作用域。
在JavaScript中的eval()函数可以接受一个字符串为参数,并将其中的内容视为好像在书写的时候就存在这个作用域里面了一样
function foo(str,b){
eval(str)
console.log(a,b);
}
var a = 2;
foo("var a = 1",3);// 1 3;
在上面的代码中可以看到foo接受了一个字符串和一个数值当作参数,其中在foo中将str传入eval()函数中,所以此时接受到的字符串“var a = 1‘就像是书写时就存在的一样,所以当要输出a的时候就不会去使用外面的a = 2的值,而是使用a = 1(因为是从内向外找的,当找的第一个匹配的标识符就停止)。
但是这种办法再严格模式下是无法使用的,而且会有性能上的问题
function foo(str,b){
"use strict"
eval(str)
console.log(a,b);
}
var a = 2;
foo("var a = 1",3);// 2 3
可以看到上面的a是直接输出的是2 3,因为在严格模式下eval有自己的作用域,意味着其中的声明是无法去修改所在的作用域的。
性能上也是会有损耗的,因为JavaScript引擎会在编译阶段进行数项的优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并且预先的确定代码的位置,以至于在之后的运行过程中快速找到标识符,但是如果使用的eval,编译器就会无法知道eval会接受什么代码,这些代码会如何修改作用域,所以这时候优化是没有意义的,所以最简单的做法就是完全不做任何优化。
还有一些欺骗词法作用域的办法也有很多,例如with,setTimeout(…)和setInterval(…)的第一个参数,这个第一个参数可以是字符串,也是可以被解释为书写代码阶段就存在的,但是这个功能已经过时,现在不提倡。
在理解了词法作用域之后就可以更好的去理解闭包了。