因为都是文字,有些不好理解,所以尽量将文字都以图文的形式画出来,便于理解。
作用域包含了一系列的气泡。每一个都是一个容器,包含了标识符的定义。 这些气泡相互嵌套,排列成蜂窝型,排列结构在写代码时定义。
下面考虑以下几个问题:
- 是什么生成了一个新气泡?
- 只有函数会生成新气泡吗?
- JavaScript 中的其他结构能生成作用域气泡吗?
带着这几个问题往下看。
函数中的作用域
对于上面问题的常见答案是,
- JavaScript 具有基于函数的作用域。
- 每声明一个函数,就生成一个作用域气泡。
- 其他结构不会生成作用域气泡
但是,这并不完全正确。
先来看一个代码片段。
function foo(a) { var b = 2; function bar() { } var c = 3; }
这些标识符代码的变量和函数都属于所处作用域的气泡,所以在外部是无法访问的。
尝试在全局作用域访问 foo 内部的标识符,会报错:
function foo(a) { var b = 2; function bar() { } var c = 3; } bar(); // Uncaught ReferenceError: bar is not defined console.log(a); // Uncaught ReferenceError: a is not defined
函数作用域含义:属于这个函数的全部变量都可以在整个函数的范围内使用及复用(在嵌套的作用域中也可以使用)。
隐藏内部实现
函数的认知
可简单参考下图:
为什么要隐藏函数:从最小特权原则中引申出来。也叫最小授权或最小暴露原则。
最小特权原则:在软件设计中,应最小限度的暴露必要内容,其他内容“隐藏”起来。比如某个模块或对象的API接口。
这个原则可以延伸到如何选择作用域来包含函数和变量。
// 在全局作用域中声明 b 和 doSomethingElse function doSomething(a) { b = a + doSomethingElse(a * 2); console.log(b * 3); } function doSomethingElse(a) { return a - 1; } var b; doSomething(2); // 15
上面代码中,b 和 doSomethingElse 应是 doSomething 内部私有访问的。 给予外部作用域对他们的访问权限,不仅没必要,而且有可能会被无意覆盖。
“合理”的设计应该这样:
// 将 b 和 doSomethingElse 隐藏在 doSomething 内部 function doSomething(a) { function doSomethingElse(a) { return a - 1; } var b; b = a + doSomethingElse(a * 2); console.log(b * 3); } doSomething(2); // 15
规避冲突
隐藏变量和函数的好处:规避冲突。(避免同名的标识符被覆盖)
先来一段代码:
function foo() { function bar(a) { i = 3; // 无意将 i 重写成 3,3 永远小于 10 console.log( a + i); } for (var i = 0; i < 10; i++) { // 此处会死循环 bar(i * 2); } } foo();
解决上面死循环的方法有两种:
- bar 中用 var 声明 i,达到遮蔽效果
- bar 中的变量换个名字
但是软件设计可能要求使用同样的标识符名称,所以在这种情况下,使用作用域来“隐藏”内部声明是唯一的最佳选择。
1. 全局命名空间
变量冲突的典型例子出现在全局作用域。
当加载多个第三方库的时候,如果没有隐藏内部私有的变量和函数,就会出现冲突。
通常做法是在全局作用域声明一个独特变量(通常是对象),这个变量叫做命名空间,所有变量都是它的属性。
例如:
var myLibrary = { name: 'library', doSomething: function() {}, doSomethingElse: function() {} }
2. 模块管理
从众多模块管理器中挑一个来使用:
函数作用域
虽然用函数将任意代码片段包装起来,可以将内部变量和函数隐藏起来,但是并不理想(会导致额外问题)。
var a = 2; function foo() { var a = 3; console.log(a); } foo(); console.log(a);
以上代码的问题:
- 必须显示声明具名函数 foo(污染了所在作用域)
- 必须显示调用 foo() 才能运行其中代码
如果函数不需要名称且可以自动运行,就好了。
var a = 2; (function foo() { // 以 (function 这种形式开头声明,函数会被当前函数表达式来处理,而不是函数声明 var a = 3; console.log(a); })(); console.log(a);
第一个片段中,foo 被绑定在所在的作用域中;第二个片段,foo 被绑定在函数表达式自身的函数中,而不是所在的作用域中,不会污染外部作用域。
匿名和具名
函数表达式最熟悉的就是回调函数:
setTimeout(function() { // 其中 function() {} 叫做匿名函数表达式 console.log(1); }, 1000);
匿名函数表达式使用起来简单快捷,很多库和工具都倾向鼓励使用这种风格的代码。
但是也有缺点:
给函数表达式指定一个名字,可以有效解决以上问题:
setTimeout(function timeHandler() { // <-- 指定名字 timeHandler console.log(1); }, 1000);
立即执行函数表达式
var a = 2; (function foo() { var a = 3; console.log(a); })(); console.log(a);
还有另外一种形式:
var a = 2; (function foo() { var a = 3; console.log(a); }()); // <-- 将最后的括号移入最前面的括号中 console.log(a);
立即调用函数的进阶用法:
var a = 2; (function foo(global) { var a = 3; console.log(a); // 3 console.log(global.a); // 2 })(window); // <-- 将立即调用函数当做函数调用,并传递参数进去 console.log(a); // 2
块作用域
for (var i = 0; i < 10; i++) { // i 被绑定在全局作用域中 console.log(i); }
表面上看,JavaScript 没有块作用域。
with
with 是块作用域的一个例子(一种形式)。用 with 从对象中创建出的作用域仅在 with 声明中而非外部作用域中有效。
try/catch
ES3 中规定,try/catch 的 catch 分句会创建一个块级作用域。
try { throw 'error'; } catch(err) { console.log(err); // error 正常执行 } console.log(err); // err is not defined
let
let 关键字可以将变量绑定到所在的任意作用域(通常是 {} 中)。
let 为其声明的变量隐式地藏在了所在的块作用域中。
var foo = true; if (foo) { let bar = 1; console.log(bar); // 1 } console.log(bar); // bar is not defined
var foo = true; if (foo) { { // <-- 显示的块 let bar = 1; console.log(bar); // 1 } } console.log(bar); // bar is not defined
const
关于 let 和 const, 可以参考阮一峰老师的 《ECMAScript 6 入门》
有理解的不对的地方,烦请指出,谢谢!!
注:以上所有的文字、代码都是本人一个字一个字敲上去的,图片也是一张一张画出来的,转载请注明出处,谢谢!