一、JavaScript代码执行过程
JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。
编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。
执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。
作用域是一套规则,作用域链是这套规则的具体实现。作用域在编译阶段确定规则,作用域链在执行阶段确定。
二、作用域
作用域就是用来管理JavaScript引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称(这里指的是变量名和函数名)进行变量查找的一套规则。简言之,作用域是变量与函数的可访问范围,它控制着变量与函数的可见性与生命周期。
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。作用域是最小访问原则,有助于提高代码效率,变量命名问题,帮助跟踪错误。
三、按访问范围划分
1、全局作用域
在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下三种情形拥有全局作用域:
A、最外层函数和在最外层函数外面定义的变量;
B、所有未定义直接赋值的变量;
C、所有window对象的属性。例如window.name、window.location、window.top等等。
2、局部作用域
局部作用域一般只能在固定的代码段访问到,比如函数内部,因此常常有人把局部作用域称为函数作用域。
四、按访问对象划分
1、变量作用域
在JavaScript中,变量的作用域有全局作用域和局部作用域两种。
A、全局变量也称为外部变量,它是在函数外部定义的变量。其作用域是整个源程序,当前页面内有效。
//检查一个全局变量是否被声明if ('a' in window) { // 变量a声明过} else { // 变量a未声明}
B、局部变量也称为内部变量,它是在函数内部定义的变量。其作用域仅限于函数内,方法内有效,离开该函数后再使用这种变量是非法的。
C、全局变量和局部变量的关系
在函数体内,局部变量的优先级高于同名的全局变量。如果在函数内声明的一个局部变量或者函数参数中带有的变量和全局变量重名,那么全局变量就被局部变量所遮盖。
声明全局变量可以不用var,但是声明局部变量一定要加var,否则会修改全局变量。
2、函数作用域
JavaScript没有块级作用域,只有函数作用域。
JavaScript的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的,这意味着变量在声明之前甚至已经可用,也就是常说的声明提前,即JavaScript函数里声明的所有变量(但不涉及赋值)都被提前至函数体的顶部。
代码段1
var a = 1;function funcTest() { if (!a) { //变量提升,a为undefineds var a = 10; } console.log(a);}funcTest();//10
代码段2
var a = 1;function funcTest() { a = 10;//全局变量被修改 return;}funcTest();console.log(a);//10
代码段3
var a = 1;function funcTest() { a = 10;//函数声明提升,修改了局部变量,没有修改全局变量 return; function a() {}}funcTest();console.log(a);//1
A、静态作用域
静态作用域,函数的作用域在函数定义的时候就决定了。
JavaScript采用词法作用域(lexical scoping),也就是静态作用域。
因此,JavaScript作用域在函数定义时确定,而不是在函数调用时确定。
var a = 1;function funcIn() { console.log(a);}function funcOut() { var a = 10; funcIn();}funcOut();//1
当采用静态作用域时,执行funcIn函数,先从funcIn函数内部查找是否有局部变量a,如果没有,就查找上一层作用域,这里刚好是全局作用域,也就是a等于1,所以最后会打印1。
B、动态作用域
动态作用域,函数的作用域在函数调用的时候才决定。bash语言是动态作用域。
var a = 1;function funcIn() { console.log(a);}function funcOut() { var a = 10; funcIn();}funcOut();//10
当采用动态作用域时,执行funcIn函数,依然是从funcIn函数内部查找是否有局部变量a。如果没有,就从调用函数的作用域,也就是funcOut函数内部查找a变量,所以最后会打印10。
五、作用域链
全局函数无法查看局部函数的内部细节,但局部函数可以查看其上层函数的细节,直至全局细节。当需要从局部函数查找某一属性或方法时,如果当前作用域没有找到,就会去上层作用域查找,直到全局作用域,这种组织形式就是作用域链。
作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对有权访问的所有变量和函数的有序访问。
;(function(){ var a = 10; function funcOut(){ var b = 100; function funcIn(){ if(!!a) console.log(a); } funcIn(); } funcOut();})();
作用域链是一个只能单向访问的链表,这个链表上的每个节点就是执行上下文的变量对象(代码执行时就是活动对象),单向链表的头部(可被第一个访问的节点)始终都是当前正在被调用执行的函数的变量对象(活动对象),尾部始终是全局活动对象。总之,由多个执行上下文的变量对象构成的链表叫做作用域链。
六、改变作用域链
一般情况下,在执行上下文运行的过程中,作用域链只会被with语句和catch语句影响。
1、with语句(将指定的对象添加到作用域链中)
with语句是对象的快捷应用方式,可以避免书写重复代码。看上去高效,实际产生了性能问题。
function funcTest() { var a = ''; with(myObj){ a = val; } return a;}
当代码运行到with语句时,执行上下文的作用域链临时被改变了,一个新的可变对象被创建,它包含了参数指定的对象的所有属性。这个myObj对象将被推入作用域链的顶部,意味着函数的所有局部变量现在处于第二个作用域链对象中,因此访问代价更高了。
2、catch语句
当try代码块中发生错误时,执行过程会跳转到catch语句,然后把异常对象推入一个可变对象并置于作用域的头部。在catch代码块内部,函数的所有局部变量将会被放在第二个作用域链对象中。
function funcTest(){ try{ dosth(); }catch(ex){ console.log(ex.message); //作用域链在此处改变 }}
一旦catch语句执行完毕,作用域链就会返回到之前的状态。try-catch语句在代码调试和异常处理中非常有用,因此不建议完全避免。你可以通过优化代码来减少catch语句对性能的影响。一个很好的模式是将错误委托给一个函数处理,例如:
function funcTest(){ try{ dosth(); }catch(ex){ handleError(ex); //委托给处理器方法 }}
优化后的代码,handleError方法是catch子句中唯一执行的代码。该函数接收异常对象作为参数,这样你可以更加灵活和统一的处理错误。由于只执行一条语句,且没有局部变量的访问,作用域链的临时改变就不会影响代码性能了。
七、代码优化
从作用域链的结构可以看出,在运行期上下文的作用域链中,标识符所在的位置越深,读写速度就会越慢。因为全局变量总是存在于执行上下文作用域链的最末端,在标识符解析时,查找全局变量是最慢的。所以,编写代码时,应尽量避免使用全局变量,尽可能使用局部变量。一个好的经验法则是:如果一个跨作用域的对象被引用了一次以上,则先把它存储到局部变量里再使用。
function changeColor(){ document.getElementById("btn").οnclick=function(){ document.getElementById("mycanvas").style.backgroundColor="red"; };}
这个函数引用了两次全局变量document,查找该变量必须遍历整个作用域链,直到最后在全局对象中才能找到。
function changeColor(){ var mydoc = document; mydoc.getElementById("btn").οnclick=function(){mydoc.getElementById("mycanvas").style.backgroundColor="red"; };}
这段代码比较简单,重写后不会显示出巨大的性能提升,但是如果程序中有许多全局变量被反复访问,那么重写后的代码性能会有显著改善。