老规矩,先上代码,帮忙指点下变量作用域

(function(){
a = 5;
alert(window.a);
var a = 10;
alert(a);
})();

这段代码中的alert,为什么第一个是返回的undefined.按说,不要VAR的a变量,不就是全局变量吗?

阅读 3.9k
5 个回答

这个问题很有趣,但也很难真的完整的回答,至少我会认为它在不同的浏览器品牌中或版本(尤其IE)中,行为有有极大差异。以下只是我的的测试结果与个人理解,如果你在不同的浏览器上测试,或许有不同的答案。为了简化起见,太细节的资料就略过

首先要理解的是,在浏览器全域中有个window物件(在Node.js是global物件),在全域中共有四种可能的宣告变量的方式:

a = 5;
var a = 5;
window.a = 5;
this.a = 5;

再加上ES6(ES2015)的letconst,又会多了两种。这两种先不在这里讨论,我只看这个问题中的var方式。

那么它们会有什么差异?而且有哪些是相等的?用一个简单的测试程式码来看:

"use strict"; //这里切换严格模式

var a = 5 //这里更动试试

console.log(window.a);
console.log(this.a);
console.log(a);

你会得到类似的答案,这三个都是5。在严格模式(strict code/mode)情况下,只有a=5这个测试直接报错无法执行。

第二个测试是加上IIFE(立即执行函式定义),来看这几种情况在函式里的是否能存取:

"use strict"; //这里切换严格模式

a = 5; //这里更动试试

(function(){
  console.log(window.a);
  console.log(this.a);
  console.log(a);
})()

在这组测试中,明显的在严格模式中,产生很多的错误,尤其是a = 5是全部报错,其它的则是在函式中的this.a会报错。

最后一个测试是这个程式码,它只是用来对照这个问题中的在IIFE函式中如果有使用a = 5,a变量会转为全域的情况,当然你也可以代换用其他的window.a试试:

//"use strict"; //这里切换严格模式

(function(){
  a = 5;
  console.log(window.a);
  console.log(this.a);
  console.log(a);
})()

根据测试结果与相关文件来看,window.athis.a是一致的,因为在全域中的this相当于window物件。当然因为this到了函式中是另一回事,一般不会这样直接使用,不过这里的IIFE的呼叫者是全域物件,this也相当于window物件,刚好是个对应到的情况。不过因为严格模式对于函式有非常严密的规定,在函式中的this无法再对应到window物件,这一点是要注意的。

var a = 5;这是一个正常情况下的变量宣告,也是一般情况下应该这样作的方式,而且它需要定义在所有执行或函式呼叫的最上面。它与window.a的定义不同的地方在于,JavaScript会把它视为原始资料类型的定义,而非window物件中的一个属性值,虽然用起来都一样,但内部的实作是有差异的。

要怎么知道差异在哪?最明显的例子是用delete运算符,它是用来删除物件中的属性用的。你可以把上面四种宣告出来的变数用下面的这语句来删除看看,因为delete会回传删除成功与否的布林,所以你可以看到是不是能被删除。不过在严格模式中,delete是无法使用的。

console.log(delete a);

根据测试后,只有var a = 5;这种宣告方式,是无法被删除,它并不真正算是window物件的属性而已。另外藉由对window物件进行(for...in),也可以得到一样的结果。

那么a = 5这种宣告方式又是什么?它其实是隐喻式的window.a宣告方式,也就是说如果window.a = 5是明确地来指定window物件的属性a为5,a = 5就是暗地里把window物件的属性指定为5。

不过在IE8之前中的测试资料显示,a = 5var a = 5的意义相等,这又是浏览器品牌与版本实作的可怕差异之处。所以这个宣告方式,是一种极为不建议的陷阱语法。


回到题目本身的程式码来看,另外有一个变数特性重点,称之为提升(Hoisting),提升的层次是局限在函式中的,大致的情况会是,所有的以var(let与const相同行为)宣告的变量,在执行前会先提升宣告部份到函式区块中的最前面,但要特别注意的是它的赋值(指定值)部份是不会被一并提升的,所以会变为先被赋值为undefined

所以原本的程式码在执行前的变量被提升如下:

(function(){
var a;
a = 5;
alert(window.a);
a = 10;
alert(a);
})();

所以到执行到a = 5这行时,a是一个只在函式区块中的区域变量,而非像上面所言的成为全域window物件中的属性,也因此在window.a要取值时,必定是取到undefined。你可以对比上面的问题,另外宣告一个b=100,看看会不会取得到window.b范例如下:

(function(){
a = 5;
b = 100;
alert(window.a);
alert(window.b);
var a = 10;
alert(a);
})();

这个问题是个典型而常见的陷阱题目,在面试时也常被拿出来考试。要很清楚地回答它需要对JavaScript语言中的内部设计有一定理解。当然,你也发觉有很多地方在最新的浏览器中或严格模式中被修改或拿掉,代表这些都是很容易造成错误的设计。

其他资讯可以参考: http://stackoverflow.com/ques...

变量提升啊,下面有var a 啊,你知道变量提升吗?

程序运行分为两个阶段,第一是编译阶段(在当前作用域注册变量),第二是运行阶段。

程序首先编译这段代码,其他都不管,只关注变量和函数的声明。首先看到var a,就在你的当前作用域IIFE函数中注册a这个变量。

然后进入运行阶段。遇到a = 5时,首先是寻找a这个变量是否已经在当前作用域注册了?如果已注册,就使用当前作用域的a,并给他赋值5.如果在当前作用域没找到已注册的变量a,这时就会向外一层作用域寻找,也就是全局作用域寻找a。

随后执行alert(window.a),这时是寻找window对象的a属性,此时window对象并没有a属性,所以此时结果是undefined。如果你在IIFE函数外赋值a = 20,这时结果就是20了。

随后a = 10寻找变量a并赋值10(运行过程同第三段)。

最后alert(a),查找当前作用域是否有变量a,如果有,则返回该变量的值。否则向外一层寻找,如果在全局作用域都没有找到a,这时程序就会报ReferenceError: a is not defined的错误。

关于作用域,推荐You Don't Know JS

自己待填坑的文章

嗯,变量提升,变量声明会提前到最前面,与输写的顺序无关。

之前的回答感觉有不正确的地方,自己下去又研究了下,`

(function(){
a = 5;
alert(window.a);
var a = 10;
alert(a);
})();`

这个代码中a=5在程序执行时这个a是局部变量a,而不是全局变量a,因为下面的var a=10;导致变量声明提升到程序最顶部,也就是a=5之前了,所以执行a=5时其实是对局部变量的赋值。window.a中的a是window对象的一个属性,因为从未赋值,所以一直是未定义,这句话放在这个匿名函数内的任何位置输出都是未定义。

推荐问题
宣传栏