作用域(scope)是结构化编程语言中的重要概念,它决定了变量的可见范围和生命周期,正确使用作用域可以使代码更清晰、易懂。作用域可以减少命名冲突,而且是垃圾回收的基本单元。和C、C++、Java等常见语言不同,javascript的作用域不是以花括号包围的块级作用域(block scope),这个特性经常被大多数人忽视,因而导致莫名其妙的错误。例如下面代码,在大多数类C的语言中会出现变量未定义的错误,而在javascript中却完全合法:
1 2 3 4 |
if (true) { var somevar = "value"; } console.log(somevar); //输出 value |
这是因为javascript的作用域完全是由函数来决定的,if、for语句中的花括号不是独立的作用域。
函数作用域
不同于大多数类C的语言,由一对花括号封闭的代码块就是一个作用域,javascript的作用域是通过函数来定义的。在一个函数中定义的变量只对这个函数内部可见,我们称为函数作用域。在函数中引用一个变量时,javascript会先搜索当前函数作用域,或者成为“局部作用域”,如果没有找到则搜索其上层作用域,一直到全局作用域。我们看一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 |
var v1 = "v1"; var f1 = function(){ console.log(v1); //输出 v1 }; f1(); var f2 = function(){ var v1 = "local"; console.log(v1); //输出local }; f2(); |
以上示例十分明了,javascript的函数定义是可以嵌套的,每一层是一个作用域,变量搜索顺序是从内到外。下面这个例子可能有些令人困惑:
1 2 3 4 5 6 |
var scope = "global"; var f = function(){ console.log(scope); //输出 undefined var scope = "f"; }; f(); |
上面代码可能和你预想的不一样,没有输出global,而是undefined,这是为什么呢?这是javascript的一个特性,按照作用域搜索顺序,在console.log 函数访问scope 变量时,javascript 会先搜索函数f的作用域,恰巧在f作用域里面搜索到scope 变量,所以上层作用域中定义的scope 就被屏蔽了,但执行到console.log 语句时,scope 还没有被定义,或者说未初始化,所以得到的就是undefined 值了。
我们还可以从另一个角度来理解:对于开发者来说,在访问未定义的变量或定义了但没有初始化的变量时,获得的值都是undefined。于是我们可以认为,无论在函数内什么地方定义的变量,在一进入函数时就被定义了,但直到var 所在的那一行它才被初始化,所以在这之前引用到的都是undefined值。(事实上,javascript的内部实现并不是这样,未定义变量和值为undefined的变量还是有区别的。)
函数作用域的嵌套
接下来看一个稍微复杂的例子:
1 2 3 4 5 6 7 8 9 10 |
var f = function(){ var scope = "f0"; (function(){ var scope = "f1"; (function(){ console.log(scope); //输出 f1 })(); })(); }; f(); |
上面是一个函数作用域嵌套的例子,我们在最内层函数引用了scope变量,通过作用域搜索,找到了其父作用域中定义的scope变量。
有一点需要注意:函数作用域的嵌套关系是定义时决定的,而不是调用时决定的。也就是说,javascript的作用域是静态作用域,又叫此法作用域,这是因为作用域的嵌套关系可以再语法分析时确定,而不必等到运行时确定。下面的例子说明了这一切:
1 2 3 4 5 6 7 8 9 10 11 |
var scope = "top"; var f1 = function(){ console.log(scope); }; f1(); //输出 top var f2 = function(){ var scope = "f2"; f1(); }; f2(); //输出 top |
这个例子中,通过f2 调用的f1 在查找scope变量时,找到的是父作用域中定义的scope变量,而不是f2 中定义的scope变量。这说明了作用域的嵌套关系不是在调用时确定的,而是在定义时确定的。
再来看个例子:
1 2 3 4 5 6 7 8 9 10 |
var foo = function(){ var a = 3, b = 5; var bar = function(){ var b = 7, c = 11; //此时a=3, b=7, c=11 a += b + c; //此时a=21, b=7, c=11 }; //此时a=3, b=5, c=undefined bar(); //此时a=21, b=5 } |
全局作用域
在javascript中有一种特殊的对象叫做全局对象。这个对象在浏览器中对应的是window对象。由于全局对象的所有属性在任何地方都是可见的,所以这个对象又称为 全局作用域。全局作用域中的变量不论在什么函数中都可以直接引用,而不必通过全局对象。
满足以下条件的变量属于全局作用域:
- 在最外层定义的变量
- 全局对象的属性
- 任何地方隐式定义的变量(未定义直接赋值的变量)
需要格外注意的是第三点,在任何地方隐式定义的变量都会定义在全局作用域中,即不通过var 生命直接赋值的变量。这一点经常被人遗忘,而模块化编程的一个重要原则就是避免使用全局变量,所以我们在任何地方都不应该隐式定义变量。
引申
今天还看到个有趣的例子
1 2 3 4 |
function scope(){ var a=b=2; } scope() |
猜,运行scope()之后,在函数外查找a和b,分别显示什么呢?
答案是:a: ReferenceError: a is not defined b:2
也就是相当于var a=b=2;这样的定义形式,会被解释成var a=2; b=2,b就是全局变量了。所以我们要谨慎用这样的定义方法~
不知道这样的解释有没有出错呢,欢迎提出
最后错了,在chrome浏览器的Console中,输出的都是2
没错的,和稳重解释一样,看下图
你的站真漂亮