三、函数作用域+隐藏内部实现

作用域包含了一系列的“气泡”,每一个都可以作为容器,其中包含了标识符(变量、函数)的定义。

1.函数中的作用域

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

function(){
    var b=2;
    //一些代码
    function bar(){
    //..}
    //更多代码
    var c=3;
}
bar();//失败
console.log(a,b,c);//三个全都失败

这个代码片段中,foo(..)的作用域气泡中包含了标识符a、b、c和bar。
bar(..)有自己的作用域气泡。全局作用域也有自己的作用域气泡,它只包含了一个标识符foo。
无论标识符声明出现在作用域中的何处,这个标识符所代表的变量或函数都将附属于所处作用域的气泡。 标识符a、b、c和bar都附属于foo(..)的作用域气泡,因此无法从foo(..)的外部对它们进行访问。也就是说,这些标识符无法从全局作用域中进行访问,因此会导致ReferenceError错误。但是,这些标识符(a、b、c和bar)在foo(..)内部可以被访问。

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(..)内部具体实现的“私有”内容。可能被有意或无意的以非预期的方式使用,从而导致超出了doSomething(..)的适用条件。更合理的设计会将这些私有的具体内容隐藏到doSomething(..)内部,如:

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

现在,b和doSomethingElse(..)都无法从外部被访问,而只能被doSomething(..)所控制。
功能性和最终效果都没有受到影响,但是设计上将具体内容私有化了,设计良好的软件都会依次进行实现。

2.1 规避冲突
“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突,两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命令的冲突。冲突会导致变量的值被意外覆盖。
例如:

function foo(){
    function bar(a){
        i=3;//修改for循环所属作用域中的i
        console.log(a+i);
    }
    for (var i=0;i<10;i++){
        bar(i*2);//无限循环了
    }
}
foo();

bar(..)内部的赋值表达式 i=3 ,意外的覆盖了声明在foo(..)内部的for循环中的i ,i 被固定设置为3,导致无限循环。
bar(..)内部的赋值操作需要声明一个本地变量来使用,采用任何名字都可以,var i=3;就可以满足要求(同时会为i 声明一个前面提到过的“遮蔽变量”)。另一种方法是采用一个完全不同的标识符名称,比如 “var j=3;”。但是在软件设计中可能自然而然地要求使用同样的标识符名称,因此这种情况下使用作用域来“隐藏”内部声明是唯一的 最佳选择。

2.2.1全局命名空间
变量冲突的一个典型列子存在于全局作用域中。当程序中加载了多个第三方库时,如果它们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发冲突。

扫描二维码关注公众号,回复: 3051379 查看本文章

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

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

这些工具管理器并没有能够违反词法作用域规则的功能,只是利用作用域的规则强制多有的标识符都不能注入到共享作用域中,而且保持私有、无冲突的作用域中,这样可以有效规避意外冲突。
第五章将会介绍模块模式的详细内容。

3.函数作用域

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。
例如:

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

这种技术可以解决一些问题,但是并不理想。首先必须声明一个具名函数foo(),意味着foo这个名称本身“污染”了所在作用域(这个例子中是全局作用域)。其次,必须显式的通过函数名(foo())调用这个函数才能运行其中代码。

如果不需要函数名(或者至少函数名可以不污染所在作用域),并且能够自动运行,接下来,JS提供了能够同时解决这两个问题的方案。
如下:

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

包装函数的声明以(function…开始,函数会被当作函数表达式而不是一个标准的函数声明来处理。

区分函数声明和表达式最简单的方法是:看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明 中的位置)。如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

函数声明和函数表达式之间最重要的区别是:
它们的标识符将会绑定在何处。
比较前面两段代码。第一个片段中foo被绑定在所在作用域中,可以直接通过foo()来调用它。第二个片段中,(function foo(){..})作为函数表达式意味着foo只能在..所代表的位置中被访问,外部作用域则不行。foo变量名被隐藏在自身中意味着不会非必要的污染外部作用域。

猜你喜欢

转载自blog.csdn.net/leng_nuan/article/details/82350743
今日推荐