1.什么函数中的作用域
先说结论:属于这个函数的全部变量都可以在整个函数的范围内使用及复用(这不是废话?)。
小示例
function foo(a) {
var b = 2;
var c = 3;
function bar(){
console.log(a, b, c);
}
bar();
}
在foo
的作用域中标识符a、b、c、bar
,不能在外部访问直接访问它们,相当于foo
把这些标识符"隐藏"起来了。
我们一般是声明一个函数,然后往里面写代码。但是反过来想一下我们把一段代码放到一个函数里面给它“包起来”,这不就把这段代码给“隐藏”起来了!芜湖!开始有点意思了。
其实这种技术是非常有用的,灵感来自最小特权原则,也叫最小授权或最小暴露原则。在软件设计中,应该最小限度的暴露必要的内容,避免都暴露在全局作用域造成混乱。
这就又引出一个问题:在所有内部嵌套作用域中访问到它们(就是前面提到的作用域链,引擎可以从最里面一层一层寻找到全局标识符)。这样就破坏了最小特权原则,暴露过多的变量或函数,而这些变量本来应该是私有的,正确的代码应该是需要阻止对这些标识符的访问的。
思考如下代码
function foo(a) {
b = a + bar(a * 2);
console.log(b * 3);
}
function bar(a) {
return a - 1;
}
var b;
foo(2); // 15
上述代码中,变量b
和函数bar
应该是foo
内部具体实现的“私有”内容。给予外部作用域对b
和bar
的“访问权限”不仅没有必要,而且可能是危险的,因为它们可能被以非预期的方式使用。更"合理"的设计会将这些私有的内容隐藏在foo
内部。
function foo(a) {
function bar(a) {
return a - 1;
}
var b = a + bar(a * 2);
console.log(b * 3);
}
foo(2); // 15
“隐藏”作用域中的变量和函数还会带来另一个好处:避免同名标识符之间的冲突。
function foo() {
function bar(a) {
i = 3;
console.log(a + i);
}
for(var i = 0; i < 10; i++) {
bar(i * 2); // 这里会造成无限循环!
}
}
分析一下:bar
内部的表达式i = 3
意外的覆盖了声明在foo
内部for
循环中的i
。这里例子中会导致无限循环,因为i
被固定设置为了3
永远无法满足小于10
这个条件。
这时候就需要bar
函数声明一个本地的变量如var i = 3;
或者干脆换一个变量名称 var j = 3;
,因此就和上面呼应上了,使用作用域来“隐藏”内部声明式唯一的选择。
到这里可能还会“迷糊”,接最开始说的:属于这个函数的全部变量都可以在整个函数的范围内使用及复用
。在上述"栗子"中bar
中的i
并没有声明在bar
函数中,执行bar
函数就会将i
赋值为3
,所以无法将其“隐藏”在函数内部,这样就造成了变量的冲突和覆盖。
2.函数作用域
上面已经知道了,在任意代码片段外部包上一个函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。虽然这种技术可以解决一些问题,但是还是会有一些问题。
首先,必须声明一个具名函数foo
,意味着foo
这个名称本身就“污染”了所在作用域。
其次,必须显式地通过函数名foo()
调用这个函数才能运行其中的代码。
还好JavaScript提供了同时可以解决上述两个问题的方案。
var a = 2;
(function foo(){
var a = 3;
console.log(a); // 3
})()
console.log(a) // 2
下面来分析一下
首先,包装函数的声明以(function… 而不仅是以function…开始。尽管看上去这并不是一个很显眼的细节,但实际上却是非常重要的区别。这时的函数会被当做函数表达式而不是一个标准的函数声明来处理。
区分函数声明和表达式的方式是看function关键字出现在声明中的位置,如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
2.1具名函数和匿名函数
就是字面的意思:具名函数
就是有名字的函数,匿名函数
就是没有具体名字的函数
setTimeout(function(){
console.log("Hello World!");
}, 1000);
因为function()...
没有名称标识符。函数表达式可以是匿名的,而函数声明则不可以省略函数名,匿名函数书写起来简单快捷,但是它也有几个缺点。
1.匿名函数在栈追踪中不会显示出有意义的函数名,使得调试困难。
2.如果没有函数名,当函数需要引用自身时只能使用过期
的arguments.callee
引用,比如在递归中。还有一种是在事件触发后事件监听器需要解绑自身,没有明确的标识符就无法明确解除绑定。
3.匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。
arguments.callee
是啥意思呢?
function foo(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
arguments.callee
引用的是当前正在执行的函数,是JavaScript的一个特性。就是自己调用自己。
所以给函数一个名分(函数名)可以有效解决上述问题。
2.2立即执行函数
var a = 2;
(function foo() {
var a = 3;
console.log(a); // 3
})();
console.log(a); // 2
由于一个函数被包含在一对括号内部,因此这对上述说过的函数表达式,通过在表达式末尾添加()
可以立即执行这个函数
它还有另一个表现形式(function (){ ... }())
,仔细观察可以发现,这种形式将执行括号()
放在了包装括号里面,两种形式都是可以随便选一种即可。
这个模式另一种应用场景式为了解决undefined
标识符的默认值被错误覆盖导致异常(这并不常见),将一个参数命名为undefined
,但是在对应位置不传入任何值,这样就保证在代码块中的undefined
标识符的值是真的undefined
undefined = true; // 绝对不能这么做
(function(undefined){
var a;
if(a === undefined) {
console.log('这里的undefined是没问题的!');
}
})()
console.log(undefined);
小结
函数是JavaScript中常见的作用域单元。本质上,声明一个在函数内部的变量或函数会在所处的作用域隐藏起来
。这样做避免了标识符之间相互混淆。