正如上一章讨论,作用域包含了一系列的“气泡”,每一个都可以作为容器,其中包含了标识符(变量、函数)的定义,这些气泡互相嵌套并且整齐地排列成蜂窝型,排列的结构是在写代码时定义的。
究竟是什么产生了一个新的气泡?只有函数会产生新的气泡吗?JavaScript中其它结构能生成气泡吗?
很对人认为 JavaScript 具有基于函数的作用域,意味着每声明一个函数都会为其自身创建一个气泡,而其他结构不会创建作用域气泡。但事实上并不完全正确!
function foo() {
var a = 2;
function bar() {
var b = 3;
}
var c = 4;
}
在这个代码片段中,foo 的作用域气泡包含了三个标识符:a, bar, c
bar 拥有自己的作用域气泡
同样全局作用域也拥有自己的作用域气泡
a, bar, c 这些标识符都是属于 foo 的作用域气泡,因此无法从 foo 的外部去对它们进行访问。
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及使用(嵌套),这种设计方案非常有用,能充分利用 JavaScript 变量可以根据需要改变值类型的“动态”特性
对函数的传统认知就是先声明一个函数,然后往里面添加代码。
反过来可以带来一些启示:从所写的代码中挑选一个任意的片段,然后用函数把它进行包装,实际上就是把这些代码隐藏了起来。实际的结果就是在整个代码片段得到周围创建了一个作用域气泡,也就是说这段代码中的任何声明都将绑定在整个新创建的包装函数的作用域里,而不是先前所在的作用域
为什么隐藏“变量”和“函数”是一个非常有用的技术。最小授权、最小暴露原则:在软件设计中,应该最小限度地暴露必要地内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计
如果所有变量和函数都在全局作用域中,当然可以在所有的内部嵌套的作用域中去访问到他们,但这样会破坏到前面提到的最小原则,因为可能会暴露过多的变量或函数,而这些变量或函数本应该私有的,正确的代码应该是可以阻止对这些变量或函数进行访问。
function doSomething(a) {
b = a + somethingElse(a * 2);
console.log(b * 3);
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething(2); // 15
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + somethingElse(a * 2);
console.log(b * 3);
}
doSomething(2); // 15
隐藏作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符冲突。两个标识符可能具有相同的名字但是用途不一样,无意间可能会造成命名冲突。冲突会导致变量的值被意外覆盖。
function foo() {
function bar(a) {
i = 3;
console.log(a + i);
}
for(var i = 0; i < 10; i++) {
bar(i * 2); // 无限循环了
}
}
变量冲突的典型例子存在于全局作用域中。当程序中加载了多个第三方库的时候,如果它们没有妥善地将内部私有的函数或变量隐藏起来,就很容易发生冲突。
这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴露在顶级的词法作用域中。
在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。虽然这种技术可以解决一些问题,但是它并不理想,因为会导致一些额外的问题。首先必须要声明一个具名函数 foo,意味着这个名称本身污染了所在作用域。其次,必须显示地通过函数名去调用这个函数才能运行起里面的代码。如果函数可以不需要函数名,且能够自动运行起来那会更理想。
所以,(IIFE)立即执行函数解决了这两个问题,函数被包含在一对括号内部,成为了一个表达式,末尾加上另外一对括号,函数被会当作函数表达式而不是一个标准的函数声明来处理。
function foo() {
var a = 1;
console.log(a);
}
foo();
// IIFE
(function foo() {
var a = 1;
console.log(a);
})();
对于函数表达式最熟悉的场景可能就是回调参数了,比如:
setTimeout(function() {
console.log('123');
}, 1000);
这叫作匿名函数表达式,因为function是没有名称标识符的,函数表达式时可以匿名的,而函数声明则不可以省略函数名——JavaScript的语法这是非法的。
匿名函数表达式写起来简单快捷,很多库和工具也倾向鼓励使用在这种风格的代码,但是有几个缺点。
1、匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难
2、如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee 引用,比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。
3、匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。
行内表达式非常强大且有用——匿名和具名之间的区别并不会对这点有影响,给函数表达式指定一个函数名可以解决以上问题。始终给函数表达式命名是一个最佳实践。
尽管函数作用域是最常见的作用域单元,但是其他类型的作用域单元也是存在的,并且通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀、简洁
除 JavaScript 外的很多编程语言都支持块作用域,因此其他语言的开发者对于相关的思维方式很熟悉,但是对于主要使用 JavaScript 的开发者来说,这个概念会很陌生
for(var i = 0; i < 10; i++) {
console.log(i);
}
在 for 循环的头部定义了变量 i , 通常是因为只想在 for 循环内部的上下文中使用 i
这就是块级作用域的用处,变量的声明应该距离使用的地方越近越好,并最大限度地本地化
块作用域就是对之前的最小授权原则进行扩展的工具,
with不仅是一个难以理解的结构,同时也是块作用域的一个例子(块作用域的一种形式),用 with 从对象中创建出的作用域仅在 with 声明中而非外部作用域有效
with(obj) {
a: 10
}
很少有人主要到 JavaScript ES3 规范中规定了 try/catch 的 catch 分句会创建一个块作用域,其中声明的变量仅在 catch 内部有效,当试图在别处引用,它就会报错
try {
undefined(); // 强行报错
} catch(error) {
console.log(error);
}
注:
当一个作用域下存在两个try/catch用同样的标识符名称声明错误变量时,很多静态检查工具会发出警告,实际上这并不是重复定义 ,因为所有变量都很安全地被限制在块级作用域内部。所有很多人会将标识符名称改为 err1, err2 来避免这个不必要地警告。
到目前为止,我们知道 JavaScript 在暴露块级作用域的功能中有一些奇怪的行为,如果仅仅时这样,那么 JavaScript 开发者多年来也不会将块级作用域当作非常有用的机制来使用了。
幸好 ES6 改变了现状,引入了新的 let 关键字,提供了一种除 var 以外的另一种变量声明方法。
块作用域在 es6 引入了新的关键字 let 之后成为了一个非常有用的机制
let 关键字可以将变量绑定到所在的任意作用域中,为其声明的变量隐式地劫持了所在的块作用域
同样,const 也可以创建块作用域变量
函数是JavaScript中最常见的作用域单元。本质上,声明一个函数内部的变量或函数会在所处的作用域隐藏起来,这是有意为之的良好软件的设计原则。
但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,有可以属于某个代码块。
从 ES3 开始,try/catch 结构在 catch 分句中具有块作用域。
在 ES6 引入了 let 关键字,用来在任意代码块中声明变量,if(…) { let a = 2 } 会声明一个劫持了 if 的块的变量,并且将这个变量添加到块中。