在之前的介绍中,我们已经知道 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...