函数作用域和块作用域
作用域包含了一些“容器”,其中包含了标识符(变量,函数)的定义,这些容器互相嵌套并整齐排列成蜂窝状,排列的结构是在写代码时定义的。
但是,究竟是什么生成了一个新的容器?只有函数可以吗?JavaScript中的其它结构能生成容器吗?
对于此问题,最常见的答案是JavaScript具有基于函数的作用域,意味着每声明一个函数都会为其自身创建一个容器,而其它结构都不会创建作用域容器,但事实上这并不完全正确。
函数作用域的含义是指:
属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域也可以使用)。
这种作用域的嵌套,本人提出一种理解方式,供参考。
以类和类的成员函数来理解
我们可以将外层的函数看做“类”,内层的函数看做是外层类的成员函数,成员函数可以访问类内部的变量(默认为私有),但是类外部的变量无法访问类内部的变量。
这种函数嵌套函数的方式可以被看做是对强类型语言类与对象功能的模仿。
隐藏内部实现
对函数的传统认知就是先声明一个函数,然后再向里面添加代码,但是反过来想也可以带来一些启示:从所写的代码中挑选出一个任意的片段,然后用函数声明对它进行包装,实际上就是把这些代码隐藏起来了。
实际的结果就是在这个代码片段的周围创建了一个作用域容器,也就是说,这段代码中的任何声明(变量或者函数)都将绑定在这个新创建的包装函数的作用域中,而不是先前所在的作用域中。然后用这个作用域来隐藏它们。
隐藏是从最小特权原则中引申出来的,也叫最小授权或最小暴露原则。这个原则指:在软件设计中,应该最小限度暴露必要内容,而将其他内容隐藏起来,比如某个模块或者对象的API设计。
函数的嵌套的目的也在于:被嵌套的函数可以认为是类的私有函数,避免在类外调用。
规避冲突
隐藏作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突。
个人感觉我们可以联想一下操作系统的文件目录结构,父目录不同的子目录即使命名相同也不会冲突。
变量冲突的一个典型例子存在于全局作用域中,当程序加载了多个第三方库时,如果它们没有妥善的将内部私有变量或者函数隐藏起来,就会容易引发冲突。
全局命名空间:
这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象,这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴露在顶级词法作用域中。
模块管理:
从众多的模块管理器中挑选一个来使用。使用这些工具,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显式导入到另外一个特定的作用域中。
这些工具只是利用作用域的规则强制所有的标识符都不能注入到共享作用域中,而是保持在私有,无冲突的作用域中,这样可以有效规避掉所有意外冲突。
依赖注入:就像引入头文件,比如模块A依赖于模块B,只需要将模块B引入到A的作用域中即可,而不必将A和B都放在全局作用域中。
函数作用域:
如果函数不需要函数名,并且能够自动运行,这会更加理想,js有这种机制!
var a=2;
(function foo(){
var a=3;
console.log(a);//3
})();
console.log(a);//2
如代码所示,包装函数的声明以小括号开始,而不是以function开始,虽然是一个很不显眼的细节,但是是一个实际上非常重要的区别:函数会当做函数表达式而不是一个标准的函数声明来处理
典型!就是末尾需要写分号!
区分函数声明和函数表达式最简单的方法是看function关键字出现在声明中的位置,如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
函数声明和函数表达式之间最重要的区别就是他们的名称标识符将会绑定在何处!
如果foo函数不是函数表达式而是一个函数声明,它被绑定在所在的作用域中,可以通过foo()来调用它。
而上图所示代码foo被绑定在函数表达式的自身函数中而不是所在作用域中。
换句话说,(function foo(){...})作为函数表达式,意味着foo只能在...所代表的位置中被访问,外部作用域则不行。foo变量名被隐藏在自身中意味着不会非必要地污染外部作用域。
匿名和具名
对于函数表达式,最典型的的场景可能就是回调参数了。
setTimeout( function(){
console.log("wait");
},1000);
这叫做匿名函数表达式,因为上图中的函数没有名称标识符。函数表达式可以匿名,函数声明不可以省略函数名。
匿名函数表达式书写起来简单快捷,但是也有缺点,缺点如下:
1.匿名函数在追踪栈中不显示有意义的函数名,使得调试困难。
2.如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用。比如在递归中。另一个函数需要引用自身的例子,就是在事件触发之后事件监听器需要解绑自身。
3.匿名函数降低了代码的可读性/可理解性很重要
给函数表达式制定一个名字就可以有效解决这个问题!
setTimeout( function timeoutHandler(){
console.log("wait");
},1000);
参考下图:
var a=2;
(function foo(){
var a=3;
console.log(a);//3
})();
console.log(a);//2
函数被包括在一对括号内部,形成了一个表达式,通过在末尾加上另一对括号可以立即执行这个函数,这种模式的术语叫做:
IIFE:立即执行函数表达式(immediately Invoked function expression)
函数名对于IIFE当然不是必须的,IIFE最常见的用法是使用一个匿名函数表达式。
IIFE的另一个非常普遍的用法是把他们当做函数调用并传递参数
var a=2;
(function foo(c){
var a=3+c;
console.log(a);//
})(2);
这个模式的另外一个场景是解决undefined标识符的默认值被错误覆盖导致的异常。
将一个参数命名为undefined,但是在对应的位置不传入任何值,这样就保证在代码块中undefined标识符的值真的是undefined。
undefined=true;
(function IIFE(undefined){
var a;
if(a===undefined){
console.log("safe");
}
})();
console.log(undefined);
此处的表达非常奇怪,个人的理解如下:
处于某种原因,undefined的默认值被错误覆盖了,比如上述代码中被错误覆盖成了true;
为了保证函数块内的undefined的值正确,因此使用图中的操作。
我们分析一下:
在函数块内,定义了a但是a未初始化,a是undefined;
在函数块内,函数的形参名是undefined,但是undefined没有传入,所以这个形参的值是undefined
所以,在函数块内,undefined就是undefined而不是true;
但是,我尝试写这段代码,发现undefined不能被赋值成true;
效果如图:虽然被赋值为true,但值依然是undefined!(Mac电脑,谷歌浏览器版本号:77.0.3865.90)
块作用域
尽管函数作用域是最常见的作用域单元,当然也是现行大多数js中最普遍的设计方法,但是其他类型的作用域单元也是存在的,并且通过其他类型的作用域单元可以实现维护起来更优秀,简洁的代码。
看个例子:
for(var i=0;i<5;i++){
console.log(i);//0,1,2,3,4
}
console.log(i);//5
if(i===6){
var j=3;
console.log(j);//3
}else{
j=3;
}
console.log(j);//3
我们在for循环头部定义了变量i,但是并不期望它在循环外使用,我们在if结构中声明了一个新的变量j,但是并不期望它在else结构中也能被访问到,因此这两种方式,都没有实现块作用域。
使用try/catch实现块作用域
try{
undefined();
}
catch(err){
console.log(err);//undefined is not a function
}
console.log(err);//Uncaught ReferenceError: err is not defined
使用let实现块作用域
ES6引入了新的 let关键字,提供了一种新的声明方式;
for(let i=0;i<5;i++){
console.log(i);//0,1,2,3,4
}
console.log(i);//ReferenceError
let i=1;
if(i===6){
let j=3;
console.log(j);//3
}
console.log(j);//j is not defined
let关键字可以将变量绑定到所在的任意作用域中,通常是{...}内部。换句话说,let为其声明的变量隐式劫持了所在的块作用域。
只要声明是有效的,在声明中的任意位置都可以使用{...}来为let创建一个用于绑定的块。
提升:提升指声明会被视为存在于其所出现的作用域的整个范围内。
但是使用let进行声明不会在块作用域中进行提升
{
console.log(a);//Reference error
let a=2;
}
垃圾收集
另一个块作用域非常有用的原因和闭包及回收内存垃圾的回收机制有关。
我们考虑以下代码:
function process(data){
//do something
}
var someReallyBigData={...};
process(someReallyBigData);
var btn =document.getElementById("my_button");
btn.addEventListener("click",function click(evt){
console.log("clicked");
})
click函数的点击回调不需要someReallyBigData变量,理论上来说,当process执行后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但由于click函数形成了一个覆盖整个作用域的闭包,js引擎极有可能依然保存着这个结构。
块作用域可以打消这种顾虑!
function process(data){
//do something
}
///在这个块中定义的内容完事之后可以销毁
{
let someReallyBigData={...};
process(someReallyBigData);
}
var btn =document.getElementById("my_button");
btn.addEventListener("click",function click(evt){
console.log("clicked");
})
const
除了let之外,ES6引入了const,同样可以用来创建块作用域变量,但其值是固定的,之后任何试图修改值的操作都会引起错误。
var foo=true;
if(foo){
var a=2;
const b=3;
a=3;
b=4;//error
}
console.log(a);//3
console.log(b);//referenceError;