第三章
函数中的作用域
众所周知,JS中的函数可以当做一个作用域,在该作用域里面定义着标识符(变量)以及函数,函数作用域之间又可以嵌套。函数作用域的含义是指,属于这个函数的全部标识符都可以在整个函数的范围内使用以及复用(包括在嵌套的作用域中),这种设计能充分利用JS的动态特性(静态类型语言在编译时便已确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型)
隐藏内部实现
把代码片段用一个函数包裹起来(创建新的函数作用域),可以隐藏其在原来该函数位置的访问。好处是其遵循了最小暴露原则:在软件设计中,应该最小限度地暴露不必要的内容,而将其他内容隐藏起来。尽量时某些私有的变量和函数不可被访问到,比如:
//bad
function doSomething(a){
b = a + doSomethingElse( a * 2 );
console.log( b * 2 );
}
function doSomethingElse(a){
return a - 1;
}
var b;
doSomething(2); //15
//good
function doSomething(a){
function doSomethingElse(a){
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );
console.log( b * 2 );
}
doSomething(2); //15
规避冲突
全局命名空间
把第三方库在全局作用域中声明一个特殊的名字变量,然后把其属性以及方法作为其对象的成员,这个对象就叫做命名空间
模块管理
通过依赖管理器将库的标识符显式地导入到特定的作用域中(第五章详讲)
函数作用域
平时在全局变量声明一个函数时,会导致产生出一个标识符,而且该函数需要显式调用才能执行。如果不需要函数名(不需要再次调用),并且需要自动执行的话,可以用函数立即执行的方式:
(function foo(){
var a = 3;
console.log(a);
})();
console.log(a) //2
解释:包装的函数以(function…开始而不是以function…开始,如果不是以funciton开始的函数,都会被当做是函数表达式,而不是一个标准的函数声明
区分表达式和声明最简单的就是看函数在代码中的位置,如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个表达式
函数声明和表达式最重要的区别是他们的标识符(函数名)会绑定在哪个作用域中。函数声明的函数可以在其所在位置的作用域直接调用,而(function…这种的函数表达式,其只能在其自己的函数作用与中调用(也就是(function{…(这里调用)})
匿名和具名
函数表达式可以匿名,例如:
setTimeOut(function(){...},1000);
(function(){...})();
而函数声明不可以匿名,只能具名,例如:
function foo(){
//...
}
不推荐用匿名函数:
- 在栈追踪中不会显示出有意义的函数名,使得调试困难
- 调用自己时只能使用arguments.callee,以及事件监听器需要解绑自身时
- 匿名函数降低了代码的可读性、可理解性
所以,始终给函数表达式命名是一个最佳实践
立即执行的函数表达式
像(function(){})()
的就是立即执行函数表达式(IIFE)
另一种写法:(function(){...}())
两者无区别,凭个人喜好
另外,IIFE还可以传递参数
var a = 2;
(function(global){
console.log(global.a);
})(window);
块作用域
一般来讲在if或者for里面用var声明的变量,实际上其都会绑定在所在位置的函数或者全局中,而不是块作用域里面。例如:
for(var i = 0; i < 10; i++){
//...
}
console.log(i); //10————可以在代码块外访问到i
JS有以下几个可以实现真正的块作用域
with
with不仅可以改变词法作用域,他本身也是相当于一个有效的块作用域,在with中声明的标识符仅在with中有效
try/catch
catch块中声明的变量仅在catch中有效
let
在代码块中用let声明的标识符可以仅在该块作用域中有效
且let声明的变量不会提升(第四章讲),在声明之前访问会导致ReferenceError错误
显式地写出代码块是一个好习惯:
if(foo){
{
let bar = foo*2;
bar = something(bar);
console.log(bar);
}
}
//这样的写法可以方便重构、移动,而不会对if造成影响
除此之外,块作用域有用的原因和闭包以及回收内存垃圾的回收机制有关(第五章详解闭包机制)
例子如下:
function process(data){
//...
}
var someReallyBigData = {..};
process( someReallyBigData );
var btn = document.getElementById('button');
btn.addEventListener("click",function click(evt){
console.log('点击了按钮');
},false);
如上例子,由于执行完process函数后,click函数的点击回调并不需要someReallyBigData变量了,理论上会回收这些占用大量空间的数据结构(someReallyBigData),但是由于click函数形成了一个覆盖整个作用域的闭包,所以JS引擎极有可能依然保存着这个结构
而显示使用块结构可以让JS引擎知道可以回收而没必要保存someReallyBigData了:
function process(data){
//...
}
//在这个代码块结束后就可以回收了!
{
let someReallyBigData = {..};
process( someReallyBigData );
}
var btn = document.getElementById('button');
btn.addEventListener("click",function click(evt){
console.log('点击了按钮');
},false);
使用let还可以使for循环中的每个迭代都重新绑定(第五章详细讲)
const
和let一样,可以创建块级作用域变量