变量的作用域指的是变量的可见性,而生命周期则(存活期)则是从另一个角度考察变量,本文只讨论变量的可见性。
[edit]先看一个“简单”的问题
var a = 100; var b = true; var c = 200; function test() { alert(a); //此处a = ? alert(b); //此处b = ? alert(c); //此处c = ? var a = 200; alert(a); //此处a = ? b = false; alert(b); //此处b = ? if (false) { var c = 50; } alert(c); //此处c = ? } test();
[edit]JavaScript变量作用域的分类
- JavaScript就两种作用域:全局(window)、函数级(function)。函数级(function)不要理解为“块级(大括号{} 级)”,JavaScript中没有块级作用域。
[edit]全局作用域
- 定义在所有函数最外边,使用或不使用var关键字定义的变量都是全局变量(当然忽略var声明变量是不赞成的)。全局变量其实被解析成window对象的一个属性,所以我们可以以 “window.全局变量名”方式访问它,推荐在没有必要的情况下直接使用变量名访问。如下例子演示了全局变量定义最常见的方法:
var msg1 = "This is message 1"; msg2 = "This is message 2"; alert(window.msg1); //This is message 1 使用window关键字访问 alert(window.msg2); //This is message 2 alert(msg1); //This is message 1 省略window关键字访问 alert(msg2); //This is message 2 function otherFunction() {} //其他一些函数或者对象声明代码 var otherObject = {}
- 在函数内(局部变量运行时环境)一样可以定义和获取全局变量。定义的方法就是不使用var关键字,而在局部环境中亦可轻松获得全局变量内容,直接使 用全局变量名引用即可。需要注意的是:如果函数内定义了与全局变量同名的局部变量,那么此时函数体将优先使用自己的局部变量,如果此时你非要使用同名的全局变量,请加上window前缀。举例如下:
var msg1 = "This is message 1"; var msg3 = "This is message 3"; function otherFunction() { msg2 = "This is message 2"; //不使用var关键字,其实也是定义一个全局变量 msg3 = "Message 3"; alert(msg1); //This is message 1 (函数内可以访问到外面定义的全局变量,再深的函数嵌套一样能正确获到这个全局变量,这是JavaScript闭包的其中一种体现) alert(msg3); //Message 3 (局部变量msg3) alert(window.msg3); //This is message 3 (使用window前缀访问同名的全局变量msg3) alert(this.msg3); //This is message 3 (因为otherFunction() 定义在一个全局的环境中,此时otherFunction() 的this也是指window,所以你看到window.msg3是等于this.msg3的) } otherFunction(); //otherFunction函数外面定义的msg1和里面定义的msg2依然是全局变量 alert(window.msg1); //This is message 1 alert(window.msg2); //This is message 2 msg2是在otherFunction() 里面定义的
[edit]局部作用域
- 使用var关键字,在函数体内定义的变量是局部变量,此变量能供其下面所有语句块({})及子函数使用。这个变量在这个函数里任何地方都可以访问到,但却不能在这个函数的外面“直接”访问(闭包允许间接访问,或代理访问,此知识点不在本文讨论范围)。举例如下:
function showMsg() { if (true) { var msg = "This is message"; } alert(msg); //This is message } showMsg(); alert(typeof(msg)); //undefind //这里在if {}大括号内定义的变量msg还能在if外showMsg()内访问到,但在showMsg()外则是无法访问的
- 父函数的变量可以被子函数访问,但子函数的变量却不能被父函数访问,显然这与我们一开始说的函数级作用域是相吻合的。举例如下:
function showMsg() { var msgA = "Message A"; this.setMsg = function(msg) { var msgB = "Message B"; alert(msgA); //Message A (子函数setMsg()可以访问父函数showMsg()的局部变量msgA) } alert(msgB); //msgB未定义(在父函数中不能访问其子函数中定义的变量msgB) } var sm = new showMsg(); sm.setMsg("Message String");
[edit]JavaScript没有块级作用域
- JavaScript中没有块级作用域,即用大括号{}包含的。Java中则有。在main方法中写入下代码:
public static void main(String... args) { for(int i = 0; i < 5; i++) { } { int j=10; } int z = 20; System.out.println(i); // i不可见,语法分析时报错,即编译不通过 System.out.println(j); // j不可见,语法分析时报错,即编译不通过 System.out.println(z); // z可见,输出20 }
但如果在JavaScript:
for(var i = 0; i < 5; i++) { } var obj = {name:"Lily"}; for(var attr in obj) { } { var j=10; } alert(i);//输出4,没有块级作用域 alert(attr); //输出name,没有块级作用域 alert(j);//输出10,没有块级作用域
这也说明一个问题,避免在全局范围内使用for循环同时声明变量,否则会造成全局命名范围的污染。 当然,JavaScript1.7中提出了let关键字声明变量(见https://developer.mozilla.org/cn/New_in_JavaScript_1.7 ),只作用于for语句范围。
for(let i=0;i<5;i++) { //todo } alert(i);//运行时报错,提示i未定义
JavaScript1.7需要这样引用
<script type="application/javascript;version=1.7"/></script>
ps:firefox2+实现了JavaScript1.7
[edit]深入理解
现在回到我们最初提出的问题:
var a = 100; var b = true; var c = 200; function test() { alert(a); //此处a为undefined alert(b); //此处b = true alert(c); //此处c为undefined var a = 200; alert(a); //此处a = 200 b = false; alert(b); //此处b = false if (false) { var c = 50; } alert(c); //此处c为undefined } test();
- 为什么第一次alert(a)是undefined,而第一次alert(b)是true?为什么两次alert(c)都是undefined?
- 我们都明白局部变量的优先级大于全局变量,或者说内围作用域的变量的优先级比外围的高。当JS引擎在当前作用域找不到此变量时,它就往外围的作用域找。不过,在这之前,有一个严肃的问题是,究竟当前作用域存不存在这个变量。像javascript这样的解释型语言,基本分为两个阶段,编译期(下面为符合大多数语言的称呼习惯,改叫预编译)与运行期。在预编译阶段,它是用函数来划分作用域,然后逐层为其以 var 声明的变量(下略称为var变量)与函数定义开辟内存空间,再然后对var变量进行特殊处理,统统赋初始值为undefined。如下图:
var变量所在的作用域 | ||
a | test()作用域 a c | |
b | ||
c | ||
预编译后各作用域的变量的公布状况 |
- 在上面那个例子,当前网页拥有两个a,一个b,两个c,一个test函数。如果在运行期用到除此以外的东东,比如d函数e变量什么的,就会报未定义错误(用eval等非正常手段生成变量与函数的情况除外),此外,它们最多出现未赋值警告。
- javascript的运行期是在为var变量与函数定义分配空间后立即执行,并且是逐行往下执行的。比如上面那个例子:
var a = 100; //为外围作用域的a赋值为100 var b = true; //为外围作用域的b赋值为true var c = 200; //外围作用域的c赋值为200 function test() { //进行test的作用域,我们简称为内围作用域。 alert(a); //此处立即调用内围作用域的a,这时它还没有来得及赋值呢!不过它已经声明过了,因此默认为其赋值为undefined,于是alert为undefined alert(b); //此处调用b时,由于内围作用域并没有声明b,所以去外围作用域找。外围作用域有声明b,并且b已赋值为true,于是alert为true。 alert(c); //此处调用c时,由于内围作用域也声明过c(第13行尽管永远也不会被执行到,但他的var也起到了声明变量的作用),所以也alert为undefined var a = 200; //此处将内围作用域的a赋值为200 alert(a); b = false; //此处将外围作用域的b赋值为false alert(b); if (false) { var c = 50; //此处尽管永远也不会执行到,但他的var也起到了声明变量的作用 } alert(c); } test();
- 作为对比,我们改写一下例子:
var a = 100; var b = true; function test() { alert(a); //此处a为undefined alert(b); //此处b = true alert(c); //此处c为undefined var a = 200; alert(a); //此处a = 200 b = false; alert(b); //此处b = false c = 50; alert(c); //此处c = 50 } test(); alert(c); //此处c = 50 var c = 200;
[edit]作用域全家福
- 各种作用域的关系如下图:
顶层作用域(window)
|
- window作用域:
window.test或者test(未以var声明的变量,浏览器会默认为其加上前缀"window.")
- Object类属性的作用域:
Object.test
- Object的原型的作用域:
Object.prototype.test
- var变量所在的作用域:
var test (函数外通过var声明的变量)
- 闭包的作用域:
(function() { var test })();
- function()的作用域:
function() { var test }
- 思考:
Object.prototype.test = 'wrong'; var test = 'right'; (function f() { alert(test); })();
Object.test = "bbb"; Object.prototype.test = 'ccc'; window.test = "aaa"; (function f() { alert(test); })();
function foo(){ foo.abc = function() { alert('def') } this.abc = function() { alert('xyz') } abc = function() { alert('@@@@@') }; var abc = function() { alert('$$$$$$') } } foo.prototype.abc = function() { alert('456'); } foo.abc = function() { alert('123');} var f = new foo(); f.abc(); foo.abc(); abc();
[edit]需要注意的几个地方及使用技巧
- 为了避免变量混乱或被覆盖,对于局部变量的定义一定不要忘记加上var关键字(必要时我们要变量使用完后主动释放它,即“变量名=null”),同时建议把所有变量集中定义在每个函数体内的开头位置。
- 巧用匿名函数,减少命名冲突或变量污染。如下两段代码其实实现了相同的功能,而第一段代码写法自己可以在那个匿名函数内大胆用自己想用的变量名等,不用担心自己定义的变量覆盖其他人定义或自己其它地方定义的变量。
//定义一个匿名函数,然后把代码丢到这个匿名函数里面,能有效减少命名冲突或变量污染,这是常见JS框架的做法 (function() { var msg = "This is message"; alert(msg); })(); document.write(msg); //msg未定义(匿名函数外的其他方法已无法调用msg这个变量)
var msg = "This is message"; alert(msg);
- 不建议在无须实例化的函数内使用this代替window去访问全局变量。一般情况使用this关键字的函数应当作为JavaScript类来处理。以下函数如果仅当作普通函数调用一下,就不应该出现this关键字,因为这通常是去操作一个全局变量了。例子:
function clsMsg() { this.msg = "This is default message"; this.showMsg = function() { alert(this.msg); } } sMsg = new clsMsg(); sMsg.msg = "This is new message"; sMsg.showMsg();