第一部分 作用域和闭包 第三章 函数作用域和块作用域

第三章 函数作用域和块作用域

book

函数中的作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用),这种设计方案可以根据需要动态的改变值。

隐藏内部实现

函数的传统认知就是声明一个函数,然后想函数体内添加代码。倘若我们从函数体内挑选出任意代码,然后用心的函数声明对其进行封装,那么我们就成功的把这部分代码隐藏了起来。实际上我们是在这段代码周边创建了新的作用域气泡,这段代码被包裹在这个新的作用域气泡内,而不是之前的作用域了,所以对于之前的作用域来说,这段代码被隐藏了起来。

隐藏作用域有很多好处,可以规避相同命名带来的冲突, 也非常符合最小特权或者最小暴露原则。在软件设计中,应该最小限度的暴露必要内容来控制对函数和变量的访问权限。例如:

function foo(){
  function bar(a){
    i = 3
    console.log(i+1)
  }
  for (var i = 0; i<10; i++){
    bar(i)
  }
}

foo()

上述例子中会出现无限循环,因为for里面的i与bar里面的i实际上在同一个作用域里面,因此bar里面的i会不断把for里面的i改写为3,导致无限循环。解决的方法是将bar里面的i改写为var i = 3,这样两个i就在不同的作用域里面了,或者也可以把i变成j。

变量冲突是很容易发生的,尤其是当你使用了大量第三方库的时候很容易会出现白娘冲突问题。可以通过全局命名空间来解决,即在全剧作用域中声明一个名字足够独特的变量,通常是一个对象。这的对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象的属性,而不是将自己的标识符暴露在顶级的词法作用域中。

另外还可以通过模块管理来规避冲突。使用模块管理工具,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将酷的标识符显式的导入到另一个特定的作用域中。他只是利用作用域的规则强制所有标识符都不能注入到共享作用域中,而是保持在私有,无冲突的作用域中。这样也可以规避冲突。

函数的作用域

本节主要介绍了函数表达式(包装函数),匿名函数和自调用。函数表达式的出现是为了防止函数定义(函数名)污染全局作用域,他也可以把函数本身隐藏起来。匿名函数多半使用在回调函数里面,所以他的函数名并不是必要的。而自调用则可以让函数不用通过显式的调用函数名来运行。自调用函数大多是为包装函数服务的。

函数表达式(function expression)非常好理解,与函数声明不同,函数表达式包裹函数的声明以“(function ”开头而不再仅仅式“function”开头,这个区别看上去很小但是却有本质的区别,用()包裹函数后,函数会被当作表达式而不是在是一个函数声明了。函数声明和函数表达式之间最重要的区别是他们的名称标识符将会绑定在何处。函数表达式只能在所代表的位置中被访问,外部的作用域访问不了。

匿名函数顾名思义就是没有函数名的函数,常见于setTime等回调函数中使用。在回调函数中,函数名存在的意义不大,因此可以匿名,这样做方便书写是业界提倡的,但是也会有一点不变,比如调试相对困难,引用的时候会比较麻烦,阅读性相对比较差,所以当遇到上述三种情况的时候尽量还是给一个有意义的名字比较好,这个是情况而定。

自调用函数或者叫做立即执行函数表达式。他的用法就是在函数表达式的末尾加“()”来立即执行这个函数。专业术语叫做IIFE(Immediately Invoked Function Expression)他最常见的用法就是在匿名函数表达式中使用。

IIFE的进阶用法可以参考下面的两个例子,第一个把自调用函数当作函数调用并传递参数进去。

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

第二个倒置代码的运行顺序,将需要运行的函数放在第二位,在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
}

函数表达式def定义在片段的第二部分,然后当作参数被传递进IIFE函数第一的第一部分中。最后,参数def被调用,并将window传入当作global参数的值。

块作用域

函数作用域是最常见的作用域单元,但是其他类型的作用域也是存在的。 块作用的出现是为了使变量的声明距离使用的地方越近越好,同时也可以避免变量名冲突导致作用域污染,并最大限度的本地化。

ES6之后,引入了let关键字,let关键字可以讲变量绑定到所在的任意作用域中(通常是{..}内部)。换句话说,let为其声明的变量隐式的劫持了所在的块作用域。通常来讲,显式的代码优于隐式或一些精巧但不清晰的代码。显式的块作用域风格非常容易书写,并且和其他语言中块作用域的工作原理一致。显式的作用域通过用{}来包裹即可。

另一个块作用域非常有用的原因和闭包及回收内存垃圾的回收机制相关。 例如

function process(data){
    //somthing
}

var someBigData={...};
process(someBigData);

var btn = document.getElementById("my button");
btn.addEventListener("click", function click(e){
    console.log("button clicked")
})

click函数的点击回调并不需要someBigData变量。理论上这意味着当process(..)执行后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于click函数形成了一个覆盖整个作用域的闭包,JavaScript引擎极有可能依然保存着这个结构(取决于具体实现)。块作用域可以打消这种顾虑 改成

function process(data){
    //somthing
}
{
    let someBigData={...};
    process(someBigData);
}

var btn = document.getElementById("my button");
btn.addEventListener("click", function click(e){
    console.log("button clicked")
})

为变量显式声明块作用域,并对变量进行本地绑定是非常有用的工具,可以把它添加到你的代码工具箱中了

还有一个比较经典的例子,面试会时常出现,那就是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)
}

const也是ES6新加入的,同样也可以创建块作用域变量,但是他的值是固定的,任何的修改都会引起错误。

Search

    Table of Contents