在之前的介绍中,我们已经知道 Javascript
没有块级作用,只有函数级作用域。
function test() { // a scope
for(var i = 0; i < 10; i++) { // not a scope
// count
}
console.log(i); // 10
}
Javascript
中也没有显示的命名空间,这就意味着一切都定义在全局作用域中。每一次引用一个变量时,Javascript
会往上遍历整个全局作用域直到找到该变量。如果遍历完整个全局作用域仍然没有找到该变量,则抛出一个 ReferenceError
错误。
隐式全局变量
// script A
foo = '42';
// script B
var foo = '42'
上面的两个例子产生不一样的效果。第一个将在全局作用域中定义变量 foo
,而第二个则在当前作用域定义变量 foo
。
我们一定要注意,如果不使用关键字 var
将会带来意想不到的影响。
// global scope
var foo = 42;
function test() {
// local scope
foo = 21;
}
test();
foo; // 21
由于在函数 test
内没用 var
来定义变量 foo
,所以将覆盖函数外部的全局变量 foo
。尽管看上去不是什么大问题,但是如果有成千上万行代码时,这将是个难以追踪的 bug
。
// global scope
var items = [/* some list */];
for(var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// scope of subLoop
for(i = 0; i < 10; i++) { // missing var statement
// do amazing stuff!
}
}
上例中,外部的循环将会在执行第一次的时候就停止,这是因为 subloop
函数内部的变量 i
将会覆盖外部的全局变量 i
。我们只需要在函数内部加上一个 var
就可以避免这个错误,所以我们在定义变量时一定不要忘记加上关键字 var
。除非我们确实希望对外部的全局变量造成影响。
局部变量
Javascript
中局部变量只可以通过两个方式产生,一是通过关键字 var
来声明,一是作为函数的形参。
// global scope
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// local scope of the function test
i = 5;
var foo = 3;
bar = 4;
}
test(10);
此时,函数 test
内部的变量 i
和 foo
是局部变量,而 bar
则会覆盖外部的全局变量 bar
。
提升(Hoisting)
Javascript
将会提升变量声明,这就意味着 var
表达式和函数声明都将被提升到作用域的顶部。
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
表达式和函数 test
的声明都将提升至顶部,因此程序将正常运行并不会报错。
// var statements got moved here
var bar, someValue; // default to 'undefined'
// the function declaration got moved up too
function test(data) {
var goo, i, e; // missing block scope moves these here
if (false) {
goo = 1;
} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // fails with a TypeError since bar is still 'undefined'
someValue = 42; // assignments are not affected by hoisting
bar = function() {};
test();
由于 Javascript
没有块级作用域,这不仅将提升 var
表达式,同时也会使得 if
结构变得不够直观。
在上例中,尽管看上去 if
在对全局变量 goo
进行操作,实际上,由于变量 goo
被提升,所以修改的是局部变量。
如果没有对提升规则有所了解,你可能会认为下面的代码将会抛出 ReferenceError
错误。
// check whether SomeImportantThing has been initialized
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
当然上面的代码是没有错误的,因为在代码在运行前,var
表达式已经被提升到顶部。
var SomeImportantThing;
// other code might initialize SomeImportantThing here, or not
// make sure it's there
if (!SomeImportantThing) {
SomeImportantThing = {};
}
这里要推荐下 @nightire 凡哥的博文 《理解 JavaScript(二)》,里面对提升的讲解非常透彻。
名称解析顺序
当尝试在一个函数作用域内访问一个 foo
变量时,Javascript
将会按照下面的顺序查找:
- 当前作用域内是否有
var foo
的定义。 - 函数形参中是否有
foo
变量。 - 函数自身的名称是否为
foo
。 - 跳到外层定义域,再从第一部开始查找起。
命名空间
一个最常见的问题就是命名冲突,这是因为 Javascript
只有一个全局作用域所带来的。但这个问题可以通过匿名的外部函数解决。
(function() {
// a self contained "namespace"
window.foo = function() {
// an exposed closure
};
})(); // execute the function immediately
上例中的匿名函数被认为是表达式,所以它们会被执行。
( // evaluate the function inside the parentheses
function() {}
) // and return the function object
() // call the result of the evaluation
当然我们也可以用其他方式来调用函数表达式,不同的结构,但是同样的效果。
// A few other styles for directly invoking the
!function(){}()
+function(){}()
(function(){}());
// and so on...
总结
建议大家使用匿名的外部函数来将代码封装到空间内,这样不仅可以解决命名空间的冲突,同时也有利于程序的模块化。
此外,使用全局变量不是一个好习惯,这将带来高成本的维护代价而且容易产生错误。
参考
http://bonsaiden.github.io/JavaScript-Garden/#function.scopes
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。