函数作用域和块作用域

148 阅读4分钟

函数声明和函数表达式的辨别,可以通过一个小技巧来一眼分辨:看function关键字出现在声明中的位置,注意,不仅仅是一行代码,而是整个声明中的位置,如果function是声明中的第一个词,那就是函数声明,否则就是函数表达式。

function foo(){}; //函数声明

var foo = function(){}; //函数表达式

(function foo(){}); //函数表达式

(function(){ //匿名函数表达式

});

函数声明和函数表达式的区别是它们的名称标识符将会绑定在何处。举个例子:

var a = 10;
function foo(a){
var b = a * 2;
return b;
}
console.log(foo); //正确打印foo函数
foo();

(function fn(a){
var c = a + 10;
console.log(fn); //正确打印fn函数
return c;
})();
console.log(fn); //ReferenceError: fn is not defined

上例中,假设代码所处作用域为全局作用域,foo函数的访问作用域是全局作用域,fn函数的访问作用域被绑定在函数表达式自身的函数中而非所在的全局作用域。此时,fn变量被隐藏在自身作用域中就意味着不会非必要的污染外部作用域。

在前文的函数表达式举例中,我还列出了匿名函数表达式,这种函数表达式的常用之地是回调函数,它是没有名称标识符的。函数表达式可以省略函数名,但函数声明则不可以省略函数名,否则会报错。

匿名函数表达式的应用非常常见,很多工具或库都有用到,但其也存在几个缺点:

匿名函数在栈中不显示有意义函数名,调试困难;
由于没有函数名,所以如需调用自身,比如递归或者事件触发后事件监听器需要解绑自身等,就不太好办了,除非使用arguments.callee,但这个已非官方推荐实践,将被彻底废弃;
同样由于没有函数名,导致代码可读性差,毕竟有个好的描述性名称,胜过额外添加注释。
正是由于以上三个缺点,所以比较推荐为匿名函数表达式加了名称标识符,这个操作不会对代码实现有任何影响,还能一举解决上面三个缺点,何乐不为:

setTimeout(function foo(){
console.log("哈哈,我有名称了.")
}, 1000)
//1秒后打印:
//哈哈,我有名称了.

块作用域
在ES5及之前版本中,js中的块作用域形同于无,实在要说的话,也只有try-catch中的catch部分定义的变量所在作用域是catch块中的,其他的都只是样子像,而本质上都不是块作用域,例如

for(var i=0; i<10; i++){
console.log(i);
}
console.log('外部:'+i);
// 外部:10

上例外部作用域可以访问到i的值为10。

但在ES6版本开始,有了let和const,终于可以明目张胆的定义块级作用域了,想必用惯了其他语言块级作用域的同学,心里的别扭终于可以舒口气了吧。

let关键字可以将变量绑定到所在的任意作用域中,通常是{...}内部,也就是说,let关键字为其声明的变量隐式的定义了所在的块级作用域。

let关键字发挥作用的典型在于for循环。

for(let i=0; i<10; i++){
console.log(i);
}
console.log(i); //ReferenceError:i is not defined

你看,在外部作用域访问变量标识符i时,就直接报未定义的语法错误。

事实上,for循环头部的let不仅将i绑定到for循环的块中,在每次循环开始时,还将其重新绑定到新的循环迭代中去,确保使用上一个循环迭代结束时的值重新赋值。

至于const也是可以创建块作用域中,不同于let的是,其值是固定的常量,任何对其值的修改都会引起错误。

总结一下
js中的作用域,主要有函数作用域和块级作用域,当然还有全局作用域。

函数作用域的使用,可以隐藏代码实现,减少变量暴露,避免命名冲突,符合软件设计的最小特权原则。关于函数作用,还讲了函数声明与函数表达式的辨别方法和区别。在函数表达式中,还分出了命名函数表达式和匿名函数表达式。

块级作用域的实现,有赖于ES6的版本进步,提供let和const关键字,可以实现同其他语言相同的由{...}包裹起来的块级作用域。比较典型的就是let版的for循环和var版的for循环,感兴趣的可以自行了解。