企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
## 1.3 函数作用域与块作用域 ### 1.3.1 函数作用域 ~~~ function foo(a) { var b = 2; // 一些代码 function bar() { // ... } // 更多的代码 var c = 3; } ~~~ 在这个代码片段中,foo(..) 的作用域气泡中包含了标识符a、b、c 和bar。无论标识符声明出现在作用域中的何处,这个标识符所代表的变量或函数都将附属于所处作用域的气泡。 **函数作用域**的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。 ### 1.3.2 隐藏内部实现 **最小授权或最小暴露原则**,指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API 设计。 ~~~ 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; // 修改for 循环所属作用域中的i console.log( a + i ); } for (var i=0; i<10; i++) { bar( i * 2 ); // 糟糕,无限循环了! } } foo(); ~~~ **1. 全局命名空间** 变量冲突的一个典型例子存在于全局作用域中。当程序中加载了多个第三方库时,内部私有的函数或变量如果没有妥善隐藏起来,就会很容易引发冲突。 这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的**命名空间**,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴漏在顶级的词法作用域中。 ~~~ var MyReallyCoolLibrary = { awesome: "stuff", doSomething: function() { // ... }, doAnotherThing: function() { // ... } }; ~~~ **2. 模块管理** 另外一种避免冲突的办法和现代的模块机制很接近,就是从众多模块管理器中挑选一个来使用。使用这些工具,任何库都无需将标识符加入到全局作用域中,而是**通过依赖管理器的机制将库的标识符显式地导入到另外一个特定的作用域中**。 这些工具只是利用作用域的规则强制所有标识符都不能注入到共享作用域中,而是保持在私有、无冲突的作用域中,这样可以有效规避掉所有的意外冲突。 ### 1.3.3 函数作用域 在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。 但这样必须声明一个函数(污染了作用域),并且需要显示地调用才能运行函数里的代码。 **区分函数声明和表达式最简单的方法是看function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。** **1. 匿名和具名** ~~~ setTimeout( function() { console.log("I waited 1 second!"); }, 1000 ); ~~~ 匿名函数的缺点: * 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。 * 如果没有函数名,当函数需要引用自身时只能使用已经过期的`arguments.callee` 引用,比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。 * 匿名函数省略了对于代码可读性/ 可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。 **行内函数表达式**非常强大且有用——匿名和具名之间的区别并不会对这点有任何影响。给函数表达式指定一个函数名可以有效解决以上问题。始终给函数表达式命名是一个**最佳实践**: ~~~ setTimeout( function timeoutHandler() { // <-- 快看,我有名字了! console.log( "I waited 1 second!" ); }, 1000 ); ~~~ **2. 立即执行函数表达式** ~~~ var a = 2; (function foo() { var a = 3; console.log( a ); // 3 })(); console.log( a ); // 2 ~~~ 第一个( ) 将函数变成表达式,第二个( ) 执行了这个函数。*第二个( )也能写在第一个( )末尾里* 这种模式称为:**IIFE**,代表**立即执行函数表达式**(Immediately Invoked Function Expression),第一个( )常用一个匿名函数表达式。 #### IIFE进阶用法:把IIFE当作函数调用并传递参数进去。 * 传递外部作用域的变量 ~~~ var a = 2; (function IIFE( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2 })( window ); console.log( a ); // 2 ~~~ * 解决undefined 标识符的默认值被错误覆盖导致的异常 将一个参数命名为undefined,但是在对应的位置不传入任何值,这样就可以保证在代码块中undefined 标识符的值真的是undefined。 ~~~ undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做! (function IIFE( undefined ) { var a; if (a === undefined) { console.log( "Undefined is safe here!" ); } })(); ~~~ * 倒置代码的运行顺序 将需要运行的函数放在第二位,在IIFE执行之后当作参数传递进去。 ~~~ var a = 2; (function IIFE( def ) { def( window ); })(function def( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2 }); ~~~ 函数表达式def 定义在片段的第二部分,然后当作参数(这个参数也叫作def)被传递进IIFE 函数定义的第一部分中。最后,参数def(也就是传递进去的函数)被调用,并将window 传入当作global 参数的值。 ### 1.3.4 块作用域 **块作用域**,指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{ .. } 内部)。 ~~~ for (var i=0; i<10; i++) { console.log( i ); } ~~~ 在for 循环的头部直接定义了变量i,通常是因为只想在for 循环内部的上下文中使用i,而忽略了i 会被绑定在外部作用域(函数或全局)中的事实。 变量的声明应该距离使用的地方越近越好,并最大限度地本地化。 ~~~ var foo = true; if (foo) { var bar = foo * 2; bar = something( bar ); console.log( bar ); } ~~~ bar 变量仅在if 声明的上下文中使用,因此如果能将它声明在if 块内部中会是一个很有意义的事情。但是,当使用var 声明变量时,它写在哪里都是一样的,因为它们最终都会属于外部作用域。这段代码是为了风格更易读而伪装出的形式上的块作用域,如果使用这种形式,要确保没在作用域其他地方意外地使用bar 只能依靠自觉性。 块作用域是一个用来对之前的**最小授权原则**进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。 **表面上看JavaScript 并没有块作用域的相关功能。** **1. with** `with关键字`是块级作用域的一个形式,用with 从对象中创建出的作用域仅在with 声明中而非外部作用域中有效。 **2. try/catch** JavaScript 的ES3 规范中规定try/catch 的catch 分句会创建一个块作用域,其中声明的变量仅在catch 内部有效。 ~~~ try { undefined(); // 执行一个非法操作来强制制造一个异常 } catch (err) { console.log( err ); // 能够正常执行! } console.log( err ); // ReferenceError: err not found ~~~ **3. let** `let 关键字`可以将变量绑定到所在的任意作用域中(通常是{ .. } 内部)。换句话说,`let`为其声明的变量隐式地附加了所在的块作用域。 ~~~ var foo = true; if (foo) { let bar = foo * 2; bar = something( bar ); console.log( bar ); } console.log( bar ); // ReferenceError ~~~ 使用`let `进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不“存在”。 ~~~ { console.log( bar ); // ReferenceError! let bar = 2; } ~~~ * 垃圾收集 考虑以下代码: ~~~ function process(data) { // 在这里做点有趣的事情 } var someReallyBigData = { .. }; process( someReallyBigData ); var btn = document.getElementById( "my_button" ); btn.addEventListener( "click", function click(evt) { console.log("button clicked"); }, /*capturingPhase=*/false ); ~~~ click 函数的点击回调并不需要someReallyBigData 变量。理论上这意味着当process(..) 执行后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于click 函数形成了一个覆盖整个作用域的闭包,JavaScript 引擎极有可能依然保存着这个结构(取决于具体实现)。 块作用域可以打消这种顾虑,可以让引擎清楚地知道没有必要继续保存someReallyBigData 了: ~~~ function process(data) { // 在这里做点有趣的事情 } // 在这个块中定义的内容可以销毁了! { //<-- 显示声明作用域块,清晰,易读 let someReallyBigData = { .. }; process( someReallyBigData ); } var btn = document.getElementById( "my_button" ); btn.addEventListener( "click", function click(evt){ console.log("button clicked"); }, /*capturingPhase=*/false ); ~~~ * let循环 ~~~ for (let i=0; i<10; i++) { console.log( i ); } console.log( i ); // ReferenceError ~~~ for 循环头部的let 不仅将i 绑定到了for 循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。 下面通过另一种方式来说明每次迭代时进行重新绑定的行为: ~~~ { let j; for (j=0; j<10; j++) { let i = j; // 每个迭代重新绑定! console.log( i ); } } ~~~ **当代码中存在对于函数作用域中var 声明的隐式依赖时,就会有很多隐藏的陷阱,如果用let 来替代var 则需要在代码重构的过程中付出额外的精力。** 考虑以下代码: ~~~ var foo = true, baz = 10; if (foo) { var bar = 3; if (baz > bar) { console.log( baz ); } // ... } ~~~ 这段代码可以简单地被重构成下面的同等形式: ~~~ var foo = true, baz = 10; if (foo) { var bar = 3; // ... } if (baz > bar) { console.log( baz ); } ~~~ 但是在使用块级作用域的变量时需要注意以下变化: ~~~ var foo = true, baz = 10; if (foo) { let bar = 3; if (baz > bar) { // <-- 移动代码时不要忘了bar! console.log( baz ); } } ~~~ ### 1.3.4 const 除了let 以外,ES6 还引入了const,同样可以用来创建块作用域变量,但其值是固定的(常量)。之后任何试图修改值的操作都会引起错误。 ~~~ var foo = true; if (foo) { var a = 2; const b = 3; // 包含在if 中的块作用域常量 a = 3; // 正常! b = 4; // 错误! } console.log( a ); // 3 console.log( b ); // ReferenceError! ~~~