《你所不知道的JavaScript》读书笔记(一):作用域和闭包(下)

267 阅读6分钟

前言

这篇文章我们接着前面《你所不知道的JavaScript》读书笔记(一):作用域和闭包(上)来继续聊作用域和闭包的话题。

3. 函数作用域和块作用域

在前面的内容里,我们知道每个函数都有自己的作用域,而一个.js文件也拥有自己的全局作用域。那么问题来了,究竟是什么生成了一个新的作用域?只有函数能生成作用域吗?对于这些问题,书中的回答是这样的

JavaScript具有基于函数的作用域,意味着没生命一个函数都会为其自身创建一个气泡(作用域),而其他结构都不会创建作用于气泡。但事实上这并不完全正确。 那么答案究竟如何呢?我们往下看。

3.1 两种理解

对于函数作用域,我们有两种理解:

  1. 函数作用域是指,属于这个函数的全部变量都可以在整个函数范围内使用及复用(事实上在嵌套的作用域中也可以使用):这种理解就是我们通常对作用于的理解。函数内部的任何变量都可以在函数内部任意使用,但是在函数的外部无法看到函数内部的变量。这样就以一个函数就给他周围画了一个圈,里面的东西是属于它的,外面无法使用也无法看到。但是函数却可以使用外面的东西。而这个圈在函数创建的时候就已经被同时创建出来了。举个栗子:
function foo(a){
    var b=2
    // 一些代码
    function bar() {
        // ...
    }
    // 更多的代码
    var c=3
}

在这段代码中,foo(...)的作用于包含了标识符a,b,c和bar。在函数内部的任何地方都可以对这些变量进行访问和修改,但是在更外层的全局作用域则无法访问到函数作用域内部的变量。对于foo(...)和bar(...)这两个函数而言,bar(..)可以访问foo(..)中的变量,反过来则不行。因为bar(..)又是一个新的作用域,在这个函数里面的东西都是属于它的,外部的环境——函数foo()——是无法看到的里面的变量的。

  1. 函数的另一个理解是从所写的代码中挑选一个任意的片段,然后用函数声明对他进行包装,结果是该代码片段周围创建了作用域气泡:一段代码的本质就是变量和变量运算。在一堆变量和变量运算中,我们抽取一段特定的片段,对他用函数进行包装。包装的结果就是隐藏内部实现,只暴露希望外面可以看到的内容(接口)。换句话说,这就是一个访问权限的问题。那么这样做有什么好处呢?
    • 最小特权原则
    • 规避冲突

3.2 匿名函数和具名函数

函数作为“一等公民”除了可以直接声明之外,还可以作为表达式存在在一段代码当中。区分函数声明和函数表达式的方法如下:

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

// 函数声明
function foo() {
}

// 函数表达式
var foo = function(){
}

(function (){
})()

在上面的例子中,函数表达式function(){}没有名称标识符,叫做匿名函数表达式。在JavaScript的语法中,函数表达式是可以匿名的,而函数声明则不可以省略函数名。

3.3 立即执行函数

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

在这段代码中,由于函数foo()被包含在一堆括号()的内部,因此变成了一个函数表达式,通过在末尾加上另外一个()可以立即执行这个函数。(function foo(){})()这样的写法就使得函数foo变成了立即执行函数,即在在函数定义之后立即执行它。因此,上述代码打印到控制台的是3,2.

3.4 块作用域

一个{...}中包含的代码就是一个代码块,这个代码块所创建的作用域叫做块作用域。在JS中有几种方式可以创建块作用域:

  • let/const
  • try...catch
  • with 这三种方式中,前两种比较常用。

4. 作用域和闭包

我们首先看一下书上给闭包下的定义:

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 这个定义有两个关键点:

  • 记住并访问词法作用域
  • 在当前此法作用域之外执行 从这两个点可以看出来,闭包发生在函数嵌套的时候,由于作用域链的原因,内层函数可以访问到外层函数的作用域。当内层函数作为返回值被返回的时候,内层函数就不一定在当前词法作用域内执行了。而被记住的词法作用域中的变量被保留下来。这就是闭包。语言的力量总是苍白,我们来看一个闭包的例子:
function foo(){
    var a = 2
    function bar(){
        console.log(a)
    }
    
    return bar
}

var baz = foo()
baz()             // 执行的结果为:2

上述代码就是一个必报的例子,我们来看一下其中的关键点:

  1. 函数嵌套:函数foo(..)里面嵌套了函数bar(...)
  2. 内层函数使用外层函数的变量:内层函数bar(...)使用了外层函数作用域中的变量a
  3. 内层函数作为外层函数的返回值
  4. 执行结果为外部空间也能得到外层函数的值:显然如果函数foo(...)中的变量a是无法被全局作用域访问到的,但是通过闭包,我们在全局作用域打印了内部变量a的值。 闭包在JS代码中随处可见,本质上无论何时何地,如果将(访问他们各自词法作用域的的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
    最后,我们以一个思考问题来结束这篇文章。请看代码:
for(var i=1; i<=5; i++){
    setTimeout( function (){
        console.log(i)
    }, i*1000)
}

请问上述代码的执行结果是什么?(下篇文章揭晓答案...)