函数作用域和块作用域

2017/11/25 posted in  JavaScript

一个作用域就犹如一个气泡,气泡可以层层嵌套,也可以如蜂窝一样整齐的排列。

一、函数中的作用域

在Javascript中每声明一个函数都会为其自身创建一个气泡。

function foo(a) { 
    var b = 2;
    // 一些代码
    function bar() { // ...
    }
    // 更多的代码 
    var c = 3;
}

在这片代码中,foo(…)的作用域气泡中包含了a、b、c和bar,无论标识符声明出现在作用域中的何处,这个标识符所代表的变量或者函数都附属于所处作用域的气泡。

全局作用域也有自己的气泡,它只包含了一个标识符:foo。

函数作用域的含义:属于这个函数的全部变量都可以在整个函数范围内使用及复用(嵌套的作用域中也可以使用)

二、隐藏内部实现

在软件设计中,有个原则叫最小授权或最小暴露原则,就是应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来。

在Javascript中可以通过函数来实现这一原则,在所写的代码中挑选出任意片段,然后用函数声明对它进行包装,实际上就是在这篇代码的周围创建了一个作用域气泡,这段代码中的任何声明(变量或函数)都将绑定在这个新创建的包装函数的作用域中。

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(…)的“访问权限”不仅没必要,而且可能是“危险”的。

function doSomething(a) { 
    function doSomethingElse(a) {
        return a - 1; 
    }
    var b = a + doSomethingElse( a * 2 );
  console.log( b * 3 );
}
doSomething( 2 ); // 15

现在变量b和函数doSomethingElse(…)都无法从外部被访问了。

规避冲突
“隐藏”作用域中的变量和函数可以避免同名标识符之间的冲突

function foo() { 
    function bar(a) {
        i = 3;
      console.log( a + i );
  }

    for (var i=0; i<10; i++) {
        bar( i * 2 );
    } 
}
foo();

上面的代码中,因为bar(...)内容的赋值表达式是 i = 3,在bar(…)作用域中找不到 i 变量的声明,顺着作用域链就会找到foo(…)作用域里的变量 i ,这样就导致了无限循环。

只要bar(…)内部的赋值操作改成声明一个本地变量来使用,如 var i = 3; 就可以满足这个需求(遮蔽变量)

1.全局命名空间

当程序中加载了多个第三库时,如果它们没有妥善的将内部私有的函数或变量隐藏起来,就会很容易发生冲突。

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

var MyReallyCoolLibrary = { 
    awesome: "stuff", 
    doSomething: function() {
        // ... 
    },
  doAnotherThing: function() {
      // ...
    } 
};

2.模块管理

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

三、函数作用域

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,但这并不理想。

var a = 2;
function foo() { 
    var a = 3; 
    console.log( a ); // 3
} 
foo(); 
console.log( a ); // 2

因为首先必须声明一个具名函数 foo(),这意味着 foo 这个名称本身“污染”了所在的作用域。其次,必须显示地通过函数名(foo())调用这个函数才能运行其中的代码。

var a=2;
(function foo(){ 
    var a = 3;
    console.log( a ); // 3 
})();  
console.log( a ); // 2

只要 function 是声明中的第一个词,那么就是一个标准函数声明,否则就是一个函数表达式。(function foo(){ …. }) 作为函数表达式意味着 foo 只能在 … 所代表的位置中被访问。

1、匿名和具名

setTimeout( function() {
    console.log("I waited 1 second!");
}, 1000 );

匿名函数表达式,因为function() …. 没有名称标识符。函数表达式可以是匿名的,而函数声明不可以省略函数名。匿名函数用的最多的地方就是作为回调参数。

匿名函数表达式书写起来很方便,但却有几个缺点:

  1. 在栈追踪中不会显示出有意义的函数名,使得调试很困难。
  2. 没有函数名,在函数需要引用自身时只能使用已经过期的 arguments.callee 引用
  3. 匿名函数省略了对于代码可读性/可理解性很重要的函数名,一个描述性的名称可以让代码不言自明。

2、立即执行函数表达式

术语:IIFE(Immediately Invoked Function Expression)

var a = 2;
(function foo() { 
    var a = 3;
  console.log(a); // 3
})();
console.log(a); // 2

由于函数被包含在一对( )括号内部,因此成为了一个表达式,通过在末尾加上另外一个 ( ) 可以立即执行这个函数。

进阶用法:

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

可以从外部作用域传递任何你需要的东西,并将变量命名为任何你觉得合适的名字。

undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做! 
(function IIFE( undefined ) {
    var a;
    if (a === undefined) {
        console.log( "Undefined is safe here!" );
    }
})();

上面的代码可以解决 undefined 标识符的默认值被错误覆盖导致异常。将第一个参数命名为undefined ,但对应的位置不传入任何值。

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

倒置代码的运行顺序,将需要运行的函数放在第二位,在 IIFE 执行之后当做参数传递进去。

四、块作用域

for (var i = 0; i<10; i++) { 
    console.log( i );
}

上面这段代码中,通常我们只想在 for 循环内部的上下文中使用 i ,但其实 i 会被绑定在外部作用域(函数或全局)中。

var foo = true;
if (foo) { 
    var bar= foo * 2;
    bar = something( bar ); 
    console.log( bar );
}

这段代码中,尽管我们是在 if 声明的上下文中声明了 bar 变量。但是,当使用 var 声明变量时,它写在哪里都是一样的,因为它们最终都会属于外部作用域。

块级作用域的用处就是使变量的声明距离使用的地方越近越好,并最大限度地本地化。但可惜,表面上看 Javascript 并没有块级作用域。除非,深入研究,下面几种情况都会创建出块级作用域。

  1. with
  2. try/catch
  3. let(ES6引入的关键字)
  4. const (ES6引入的关键字,其值为常量)