You Don't Know JS: Scope & Closures (第3章: 函数 vs 块作用域)
第二章,作用域由一系列的bubbles组成。每一个都代表了一个container或bucket,装着被声明的identifiers(variables, functions)。这些bubbles相互嵌套。这种嵌套是在开发阶段写完的。
什么制造了一个新的bubble? 只是函数吗?其他的JS结构可以创建bubbles of scope吗?
Function Vs. Block Scope
Scope From Functions
探索函数作用域和它的暗示implications.
函数作用域内的所有变量都属于这个函数,并能够反复在这个函数内使用(甚至在嵌套的作用域中)。
这种设计方法非常有用,可以充分利用动态的自然javascript变量, 在需要时携带不同类型的值。
另一方面,如果不小心,变量跨出一个作用域将导致一些意外的陷阱。
Hiding in Plain Scope
软件界的设计原则: 最少的特权。最少的暴露。比如为一个对象/模块的API, 你应当只暴露那些必要的代码。
这个原则扩展到:哪个作用域包括变量和函数的选择!
因此,变量/函数不应该都在全局作用域中。这违背了原则!
避免碰撞
无限循环了
function foo() { function bar(a) { i = 3; // changing the `i` in the enclosing scope's for-loop console.log( a + i ); } for (var i=0; i<10; i++) { bar( i * 2 ); // oops, infinite loop ahead! } } foo();
Global "Namespaces"
全局作用域可能发生变量碰撞。比如多个库被引用进你的程序。导致一些函数/变量的名字相同。
可以使用‘namespace’,声明一个对象。在这个对象内使用库的方法:
var MyReallyCoolLibrary = { awesome: "stuff", doSomething: function() { // ... }, doAnotherThing: function() { // ... } };
Module 管理
避免变量碰撞的另一个方法是更现代的module方法。使用相关的managers。
Functions As Scopes
函数作为作用域 。
使用(声明的函数)();可以立即执行。这个方法用于解决2个问题:
- 1. 只想把函数当作用域,并立即执行代码。
- 2. 函数没有名字。通过(函数)();就无需名字了,⚠️这个函数只需要执行一次,才能这么写。
理解:
注意用括号()包裹了函数,因此这个函数不再是一个标准的声明declaration。而是当做函数表达式对待functio-expression。
⚠️,区别declaration和expression,最简单的方法在于function这个词是否位于statment的第一个位置。是的话就是一个函数声明,否则就是函数表达式。
关键区别是 函数的名字作为一个identifier绑定在哪里?
var a = 2; function foo() { //绑定在这个enclosing scope, 因此可以直接使用foo()调用这个函数 var a = 3; console.log( a ); // 3 } foo(); (function bar(){ // 名字bar没有绑定在enclosing scope,而是绑定在自己的函数内。 var a = 3; // 标志符bar只会在{..}的块作用域中出现。 console.log( a ); // 3 })();
第4章hoisting的一个案例解释了表达式的identifier绑定在内部。
上面的立即执行函数等同于👇:
(function() {
var bar = ...self...
})();
Anonymous Vs. Named
setTimeout( function() {..}, 1000)
异步函数表达式:函数没有名字,并不再enclosing scope中。
又一个区别:
- 函数表达式可以异步
- 函数声明不能缺少名字。
不过没有名字的函数表达式不容易维护。比如不能在stack traces中显示,让debug变困难。
所以使用Inline function expressions是完美的。就是加上名字。
Invoking Function Expressions Immediately
因此(function foo(){ .. })()
第一个括号用于写出一个函数表达式,第二个括号执行它。
过去社区统一使用IIFE
第二个括号可以传递参数:
var a = 2; undefined (function IIFE(global){ var a = 3; console.log(a); //3 console.log(global.a); //得到全局变量a的值: 2 })( window );
也把一个函数表达式作为参数:
var a = 2; (function IIFE( def ){ def( window ); })(function def( global ){ var a = 3; console.log( a ); // 3 console.log( global.a ); // 2 });
//好理解,就是写起来太啰嗦
Blocks AS Scopes
函数是作用域中应用最广泛最普通的单位。还有其他作用域单位,甚至更好,更清楚来维护代码。
块作用域(伪):
Block scope是一个工具,用来扩展早期“最少暴露原则”,从在函数中隐藏信息,到在块中隐藏信息。
这种写法if,for,把变量放入block scope,是一个对开发者的提醒,不用在其他地方使用这个i变量了。
因为,早期JS,没有块作用域的概念。(这就很不方便)
for(var i = 0; i < 10; i++) { .. } //这个for循环参数i的作用域是全局/函数作用域。而非块作用域。 for(let i = 0; i < 10; i++) { .. } //i是块作用域。
var foo = true; if (foo) { var bar = foo * 2; console.log( bar ); } //变量bar是全局作用域, 如果用let声明bar,则bar是块作用域。
注意:在ES6以前,JS没有真正的块作用域。后来加上了let声明方式,配合{ .. },才有块作用域,
try/catch,早期javascript的一个例外,事实上的块作用域!
try { undefined(); // illegal operation to force an exception! } catch (err) { console.log( err ); // works! } console.log( err );
ReferenceError: `err` not found , 变量err在全局作用域不存在!
更多信息看附加B。catch非常有用!
let关键字(ES6)
尽量使用明显的块作用域:
if (true) {
{
let bar = 2;
console.log(bar);
}
}
内部嵌套的{}花括号可以不写。但明确的写出来,视觉效果更好,代码更容易维护!!
用let声明的变量不会hoisting!
Garbage Collection垃圾收集
block-scope很有用的另一个原因, 是关于闭包和垃圾收集,来恢复内存reclaim memory.
function process(data) {
// do something interesting
}
//在👇的块作用域let声明的变量,在块作用域结束后,清除它的内存!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
...略...
如果后续操作和前面的process()无关的话,
就没必要保存someReallyBigData变量在内存内,
通过块作用域可以限定它的使用范围,
在它用完后,自动释放它的内存。
let Loops
上文已经提到在循环中使用let, 就是使用块作用域。不过为了看起来更清除,方便今后的重构代码,
可以明确写上块{ .. }
{ let j; for (j=0; j<10; j++) { let i = j; // re-bound for each iteration! console.log( i ); } }
Const
也是块作用域定义的变量,但值不能改变。可以改变type
{ const a = '123'; Number(a); console.log(typeof a); // string }
Reviews
从ES3的try/catch,到ES6的let/const,block-scope。
开发者应灵活使用var , let, const声明变量。