精读《你不知道的JavaScript》上卷-I-第3章函数作用域和块作用域

208 阅读5分钟

I-第3章 函数作用域和块作用域

开发中,函数写的太多了。怎样才是个好的函数?我们常讲封装。封装函数,封装常用的方法,通过这章内容可以学习到为什么要封装,封装的好处和一些封装的方法。

函数作用域是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(在嵌套的作用域中也可以使用)。 这种设计方案能充分利用 JavaScript 变量可以根据需要改变值类型的“动态”特性。

利用函数作用域隐藏内部实现

正确的代码应该考虑到如何选择作用域来包含变量和函数。遵循最小授权原则。


最小授权或最小暴露原则:在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 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(..) 内部具体实现的“私有”内容。 给予外部作用域对 bdoSomethingElse(..) 的“访问权限”不仅没有必要,而且可能是“危险”的, 因为它们可能被有意或无意地以非预期的方式使用,从而导致超出了doSomething(..) 的适用条件。 更“合理”的设计会将这些私有的具体内容隐藏在 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();

这里用var i = 3;j = 3;都可以解决这个问题,但使用作用域来“隐藏”内部声明是唯一的最佳选择。

解决方案
  1. 全局命名空间

    在jQuery时代,引用各种第三方库尤为明显。如果组件没有很好的将内部私有函数和变量隐藏起来会引起很多问题。

    最佳实践:声明一个名字足够独特的变量,通常是一个对象。所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴漏在顶级的词法作用域中。

    示例

    var MyReallyCoolLibrary = {
        awesome: "stuff",
        doSomething: function() {
            // ...
        },
        doAnotherThing: function() {
            // ...
        }
    };
    
  2. 模块管理

    从众多模块管理器中挑选一个来使用。使用这些工具,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显式地导入到另外一个特定的作用域中。

函数作用域优化

函数作用域需要显示声明函数名和调用该函数

采用包装函数来解决,将函数声明转成表达式

var a = 2;
(function foo(){ // <-- 添加这一行
    var a = 3;
    console.log( a ); // 3
    })(); // <-- 以及这一行
console.log( a ); // 2

如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

匿名和具名函数

匿名函数表达式
setTimeout( function() {
    console.log("I waited 1 second!");
}, 1000 );

这叫作匿名函数表达式,因为 function().. 没有名称标识符。函数表达式可以是匿名的, 而函数声明则不可以省略函数名——在 JavaScript 的语法中这是非法的。

匿名函数缺点:

  1. 匿名函数在栈追踪中不会显示函数名,调试很困难。
  2. 如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用,比如在递归中。 另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。
  3. 匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。

行内函数表达式非常强大且有用——匿名和具名之间的区别并不会对这点有任何影响。 给函数表达式指定一个函数名可以有效解决以上问题。 始终给函数表达式命名是一个最佳实践

setTimeout( function timeoutHandler() { // <-- 快看,我有名字了!
    console.log( "I waited 1 second!" );
}, 1000 );
立即执行函数表达式(IIFE Immediately Invoked Function Expression)

将函数包含在一对 ( ) 括号内部,因此成为了一个表达式,通过在末尾加上另外一个 ( ) 可以立即执行这个函数。 比如 (function foo(){ .. })(), 另一种形式(function(){ .. }())。两种形式在功能上是一致的。

IIFE进阶用法,把它们当作函数调用并传递参数进去。

var a = 2;
(function IIFE( global ) {
    var a = 3;
    console.log( a ); // 3
    console.log( global.a ); // 2
})( window );
console.log( a ); // 2

将 window 对象的引用传递进去,但将参数命名为 global,因此在代码风格上对全局对象的引用变得比引用一个没有“全局”字样的变量更加清晰。 这对于改进代码风格是非常有帮助的。

IIFE 还有一种变化的用途是倒置代码的运行顺序,将需要运行的函数放在第二位,在 IIFE 执行之后当作参数传递进去。 这种模式在 UMD(Universal Module Definition)项目中被广泛使用。


var a = 2;
(function IIFE( def ) {
    def( window );
})(function def( global ) {
    var a = 3;
    console.log( a ); // 3
    console.log( global.a ); // 2
});

块作用域

{}, for, if, with, try/catch, let, const都是声明块级作用域。

if (foo) {
    { // <-- 显式的块
        let bar = foo * 2;
        bar = something( bar ); console.log( bar );
    }
}
console.log( bar ); // ReferenceError

本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。

↓↓↓↓↓↓↓↓↓复习一下之前的内容吧↓↓↓↓↓↓↓↓↓

第2章-词法作用域

第1章-作用域是什么