词法作用域
什么是词法作用域
作用域共有两种主要的工作模型——词法作用域和动态作用域,JS采用词法作用域
简单地说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的
由上面对词法作用域的描述可知,词法作用域的创建发生在预编译阶段,因为词法阶段属于预编译的一个过程
如果还是感觉不太理解,请考虑下面代码:
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar( b * 3 );
}
foo( 2 ); // 2, 4, 12
在这个例子中有三个逐层嵌套的词法作用域(在此我们称之为气泡),看下图:
1包含着整个全局作用域,其中只有一个标识符:foo
2包含着foo 所创建的作用域,其中有三个标识符:a、bar 和b
3包含着bar 所创建的作用域,其中只有一个标识符:c
LHS与RHS的逐层查找便是逐层查找词法作用域,由内到外,由近到远。从外向内访问作用域是禁止的
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定
function foo(){
var a = 200;
bar();
}
function bar(){
console.log(a);//100
}
var a = 100;
foo();
虽然函数bar在foo中被调用,但是bar是定义在全局的,也就是说,bar的词法作用域嵌套关系为(由内到外):bar本身的词法作用域–>全局作用域。所以foo中的a并不影响输出结果
词法作用域怎么形成
你可以简单的认为词法作用域就是函数作用域(即上面图片中展示的相互嵌套的气泡)。但是这是片面的,因为,词法作用域由书写位置决定,那么,我在全局范围上声明一个变量,那么这个变量的词法作用域便是全局。
其实我想说的是不要简单的将词法作用域理解为函数作用域(即上图嵌套的气泡)。词法作用域是针对所有标识符而言的,根据书写位置,预编译阶段便会将对应的标识符放在对应的词法作用域中,然后就形成了词法作用域
所有由函数包裹的代码,都会在这个函数中形成词法作用域。就是上面图片展示的那样
var a = 100;
(function(){//会形成词法作用域,里面有a、b变量
var a = 200;
var b = 300;
console.log(a);//200
}());
console.log(a);/*100,可见立即执行函数中给a赋值的操作并不影响全局a,这更说明了上述观点*/
console.log(b);//访问不到立即执行函数中的b,此处报错
function test(){
var a = 100;
setTimeout(function(){//该匿名函数同样形成词法作用域
var a = 200;
console.log(a);//200
}, 2000);
console.log(a);//100
}
test();
//上述代码虽然存在闭包,但是这儿并没讨论闭包
词法作用域包含了什么
词法作用域中存放着代表变量或函数的标识符,以便进行函数与变量的LHS或RHS查询
查找标识符
在上一个代码片段中,引擎执行console.log(..) 声明,并查找a、b 和c 三个变量的引用。它首先从最内部的作用域,也就是bar(..) 函数的作用域气泡开始查找。引擎无法在这里找到a,因此会去上一级到所嵌套的foo(..) 的作用域中继续查找。在这里找到了a,因此引擎使用了这个引用。对b 来讲也是一样的。而对c 来说,引擎在bar(..) 中就找到了它
遮蔽效应
作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。
考虑下面代码:
var a = 10;
function test(){
var a = 100;
console.log(a);//遮蔽效应,输出100
}
test();
全局变量会自动成为全局对象(比如浏览器中的window 对象)的属性,因此可以不直接通过全局对象的词法名称,而是间接地通过对全局对象属性的引用来对其进行访问。window.a通过这种技术可以访问那些被同名变量所遮蔽的全局变量。
词法作用域查找只会查找一级标识符,比如a、b 和c。如果代码中引用了foo.bar.baz,词法作用域查找只会试图查找foo 标识符,找到这个变量后,对象属性访问规则会分别接管对bar 和baz 属性的访问(在‘LHS与RHS查询’中已经提到过)
块作用域
或许我们经常说——JS中没有块级作用域。这并没有错,但是如果更加深入的说,JS中是存在块级作用域的,只是并不明显或直接
with形式的块作用域
具体请看前面的文案——‘eval与with’
try/catch形式的块作用域
ES3 规范中规定try/catch 的catch 分句会创建一个块作用域。但是这儿有一个值的注意的地方,请看代码:
try{
var b = 100;
undefined();
}catch(error){
var a = 200;
function test(){
console.log('name');
}
}
console.log(b);//100
console.log(a);//200
test();//test
console.log(error);//报错
catch不是生成了块级作用域吗?为什么a能在外部访问?
因为catch生成的块作用域仅对形参(这儿是error)有效,注意这点
let形式的块作用域
ES6引入了let关键字,提供了除var以外的另一种变量声明方式。
用let声明的变量会被绑定到let所在的块中(通常是{ .. } 内部)
if(true){
var aa = 100;
let bb = 200;
}
console.log(aa);//100
console.log(bb);//报错
for(let i = 0; i < 3; i++){...}
//这儿的i也会被绑定到后面的{}中
用let声明的变量在预编译阶段不会进行变量提升
{
console.log(a);
let a = 100;
}
const形式的作用域
const——单词译为‘常量’
ES6 还引入了const,同样可以用来创建块作用域变量,但其值是固定的(常量)。之后任何试图修改值的操作都会引起错误
var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在if 中的块作用域常量
a = 3; // 正常!
b = 4; // 错误!
}
console.log( a ); // 3
console.log( b ); // 报错
ps:本文参考并引用下列书籍
《你不知道的JavaScript》(上卷)