注:在第一章中讲到的,我们将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域和嵌套子
作用域中根据标识符名称进行变量查询。作用域有两种工作模式,第一种是最为普遍的,被大多数编程
语言采用的词法作用域(该书中主要介绍的就是词法作用域),另一种就是动态作用域。
2.1 词法阶段
(1)上一章中介绍过编译器第一步为‘分词/词法分析’,词法化的过程中会对源代码中的字符进行检查,
如果是有状态的解析过程,还会赋予词法语义。
(2)词法作用域:就是定义在词法阶段的作用域,简单理解就是:词法作用域就是我们在写代码时将
变量和块级作用域写在哪里决定的,因为词法分析器在词法分析时会保持作
用域不变(大多情况下)。
注:事实上还是可以有欺骗词法作用域的方法,让词法分析后依然可以修改作用域。
但是这不是一个好的实践!
(3)作用域查找会在找到第一个匹配的标识符后停止,在多成嵌套的作用域中,碰到相同的标识符变
量名时,外层作用域会遮蔽内层作用域,所以在嵌套作用域中可以有同名标识符而不会产生覆盖。
(4)嵌套作用域中,当需要在内层作用域中访问全局作用域中相同标识符时可以使用window.***访问。
但是非全局作用域的情况下就无法躲过遮蔽。
(5)无论函数在哪里调用,也无论如何被调用,它的词法作用域都是在它声明的地方决定的。例:
var a = 6; function out(){ console.log(6); } function test(){ var a = 8; out(); } test(); //6
2.2 欺骗词法
(1)在js中有两种方式在运行时修改词法作用域(弊大于利),eval()和with()。
(2)eval:接收一个字符串参数,并将里面的内容视为好像是在书写的时候就在这个位置的代码。
也就是说:可以将自动生成的代码通过eval放置到这个位置上就像书写的时候就存在一样。
var a = 6; function test(){
//执行时修改了词法作用域 eval('var a = 8;'); console.log(a); } test(); //8
/*严格模式下*/
var a = 6;
function test(){
'use strict';
eval('var a = 8;');
console.log(a);
}
test(); //6
注:在严格模式下,eval()有自己的词法作用域,意味着方法中的声明不会影响和修改其所在词法作用域
可以实现相同效果的方式还有setInterval(),setTimeout()和new Function()。前面两种已经过时且不
提倡使用。new Function相较eval更安全些,但也要避免使用,总之避免需要在运行时修改词法作用
域的情况。
(3)with:首先with的用法如下:
var obj = { a:1, b:2, c:3 }; with(obj){ a='aaa'; b='bbb'; c='ccc'; }; console.log(obj); //{a:'aaa',b:'bbb',c:'ccc'}
再看看他的一些问题:
function test(obj){ with(obj){ b = 'bbb'; } } var obj1 = { a:1, b:2 }; var obj2 = { a:1 }; test(obj1); test(obj2); console.log(obj1,obj2,b); //{a:1,b:'bbb'},{a:1},'bbb'
最后的结果是:obj1中的b得到了修改,obj2中由于没有属性b所以依然保持原状态,并没有想象中的
自动创建一个属性b,反而出乎意料的是自动创建了一个全局变量b并为其赋值。
最后我的理解是:with会自动创建一个词法作用域,这个词法作用域与传入的对象的属性相关联,当
该词法作用域中的变量可以在传入对象中找到相应属性名的时候为其赋值,当找不
到的是会执行LHS,向外层找,找不到时在全局环境下自动创建一个变量并赋值。
(4)无论是eval或with的使用都会对性能有很大的影响。书中描述:
JavaScript引擎会在编译阶段进行数项优化,其中有些优化是依赖能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。
但是如果引擎在代码中发现eval和with,它只能简单的假设关于标识符的位置的判断都是无效的,因为无法明确知道eval中会接受什么样的代码,这些代码对词法作用域有什么修改。因此最简单的做法是不做任何优化。
总结:JavaScript采用的是词法作用域,即变量和函数的作用域是在其编写时候的位置确定的。有两种方式来来在运行
的时候修改词法作用域,但是会极大的影响性能,所以避免碰到这种需要在运行时修改词法作用域的情况,且避
免使用eval和with。