作用域在JavaScript中是非常重要的概念,理解了它对更深入地理解闭包等概念都有很大的帮助,这篇文章就来谈谈我对作用域的理解。
一、全局作用域与局部作用域
在JavaScript中没有块级作用域的概念,它的作用域都是以函数作为划分的。JavaScript的作用域分为全局作用域和局部作用域。能在代码中的任何地方访问到的变量具有全局作用域,只能在固定代码段,例如函数内部,访问到的变量具有局部作用域。
全局作用域主要包括:1. 定义在最外层的变量和函数 2. 没经过声明,直接定义的变量,及不包含var关键字的变量。3. window对象的属性都具有全局作用局。例如:window.setTimeout, window.location
局部作用域主要包括: 1. 函数内部通过var关键字声明的变量 2.函数传入的参数
var foo = 'global'; //全局变量 function(bar) { var goo = 'inside'; //局部变量 env = 1; //全局变量 }
二、作用域链
作用域链定义了函数执行时可访问到的变量的范围。一个函数的作用域链创建分为两个步骤:一是在函数定义时创建的[[scope]]属性中包含的对象,二是在函数执行时创建的活动对象。
1. [[scope]]属性
函数的[[scope]]属性中包含的是函数在创建时所在的作用域中可访问的对象。只有JavaScript引擎可以访问到。例如:
var foo = 1; function add(num1, num2) { var sum = num1 + num2; return sum; } add(foo, 1);
在函数add定义的时候,创建了[[scope]]内部属性,因为add定义在全局作用域中,所以[[scope]]属性中包含了所有的全局变量。
2. 活动对象
函数每次调用都会创建一个执行上下文,这个执行上下文会有自己的作用域链,也就是函数的作用域链。作用域链被创建时,会初始化为[[scope]]属性中包含的对象。接下来,将函数传入的参数arguments,以及函数中声明的局部变量集合起来,组成了活动对象。并把活动对象压入作用域链的最前端。在本例中add函数的活动对象主要包括传入的参数num1、num2以及局部变量sum。
此后在进行变量名解析时,会从作用域链的顶端开始往下查找,找到则返回对应变量,找不到返回错误信息。
当函数执行完毕,执行上下文会被销毁,活动对象也随之销毁。函数的每次调用都会创建新的执行上下文,并关联新的作用域链。
三、改变作用域
要想人为地改变函数的作用域链有两种方法:with语句和try-catch中的catch语句。
1. width语句
当运行到width语句时,会把width的对象的所有属性包裹成一个对象压入作用域链的最前端。
function create() { width(document) { var list = createElement('li'); list.onclick = function() { //dosomething } }; }
在这个例子中会在当前create函数的作用域链的最前端加入document包含的所有属性,例如例子中用到的createElement方法。这样写貌似代码更简洁了,可是却造成了性能问题。因为作用域链的最前端是with对象的所有属性方法,这样函数内部的局部变量都变成作用域链中的第二级了,要查找到局部变量需要先遍历第一级,使查找时间变长。所以一般来讲程序中尽量不要出现with语句。
2. catch 语句
使用try-catch语句当遇到异常跳到catch语句中时,会把catch到的错误对象压入作用域链的最前端。
try{ dosomething } catch(er) { alert(er); }
如上例,当运行到catch语句时,会把er对象压入作用域链的最前端。
三、变量声明提升
JavaScript会提升变量声明,被提升的声明包括var表达式和function函数声明,它们会被提升到当前作用域的最顶部。
bar(); var bar = function() {}; var someValue = 42; test(); function test(data) { if(false) { goo = 1; } else { var goo = 2; } for(var i=0; i<100; i++) { var e = data[i]; } }
如上例子,在变量提升后会变成:
var bar, someValue; //var 表达式声明被提升,但是赋值语句没有跟着提升,现在它们的值是undefined //函数声明被提升 function test(data) { var goo, i, e; //局部变量的声明也会被提升, if(false) { goo = 1; } else { goo = 2; } for(i=0; i<100; i++) { e = data[i]; } } bar(); //出错,因为bar的值还是undefined bar = function() {}; //赋值语句没有提升 someValue = 42; test();
变量声明提升有时会带来一些不容易发现的问题,例如在没有提升前函数foo的false语句看起来是要改变全局变量goo,但是提升后可以清晰地看到它其实改变的是局部变量goo。对于这些情况一定到多注意。我认为能减少这类错误的方法是在函数内部将要用到的局部变量都先声明在最前面,手动提升。而函数声明尽量使用function声明方式而不是赋值语句,这样无论调用语句在前面还是后面都不会出错。