作用域和闭包

什么是作用域

在理解作用域前,我们先来简单了解一下 JavaScript 引擎运行机制是怎么样的?

举个例子:

var bar = 1

JavaScript 引擎在执行上面那句代码前,须要对它进行编译。编译时,在当前作用域查找是否有 bar 这个变量,如果没有就声明一个新的变量 bar,否则就忽略。运行时,在当前作用域查找是否有 bar 这个变量,如果能够找到就对它赋值 1,否则就抛出异常。

再举个例子:

var b = 1

function foo (a) {
    console.log(a + b)
}

foo(2)

JavaScript 引擎在执行上面那段代码过程如下:

编译时,声明 foo 函数和 foo 函数的形参 a 和 b 变量。
执行时,首先在 foo 当前函数作用域查找变量 a 和 b,b 未在当前作用域找到,便跑到上一层作用域去查找,抵达顶层(全局作用域)后停止查找。

因此作用域是用于确定在何处以及如何查找变量,即确定当前执行代码能够触及变量的边界。

词法作用域

JavaScript 采用的是词法作用域,换句话说就是在写代码时将变量和函数写在哪里来决定的。

举个例子:

var name = 'huitoutunao'

function bar () {
    console.log(name)
}

function bar2 () {
    var name = 'huitoutunao2'
    bar()
}

bar2() // 打印结果:huitoutunao

进一步得出结论:无论函数在哪里被调用,也无论如何被调用,它的词法作用域都只由函数声明时所处的位置决定。

闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

举个例子:

function foo () {
    var a = 2

    function bar () {
        console.log(a)
    }

    return bar
}

var baz = foo()

baz() // 2

分析:

首先 bar 函数可以访问 foo 函数内部作用域,然后将 bar 函数对象本身当作返回值,接着在 foo 函数执行后,其返回值赋值给变量 baz 并调用 baz(),其实就是执行函数 bar,在这里函数 bar 在自己定义的词法作用域之外执行。
按理 foo 函数执行完了后,它的作用域都会被销毁,可是这个例子没有,foo 函数的内部作用域依然存在,因为函数 bar 本身在使用。
因此 bar 函数依然持有对 foo 函数作用域的引用,而这个引用就叫作闭包。

经典例子:

var foo = []

for (var i = 0; i < 3; i++) {
    foo[i] = function () {
        console.log(i)
    }
}

foo[0]()
foo[1]()
foo[2]()

// 输出结果都是:3

分析:

这段代码先执行的 for 循环,当 i >= 3 时才终止循环,然后 foo[0](),foo[1](),foo[2]() 函数才执行,所以这里输出结果都是 3。进一步拆解,for 循环的 i 变量是定义在全局作用域中的,循环中的 3 个函数表达式是在各个迭代中分别定义的,它们也是在全局作用域中,因此每个函数里 console.log(i) 访问的 i 是全局作用域的。

拆解代码片段:

var i = 0

for (i < 3; i++) {} // 循环结束再执行下面函数

// 迭代第1次定义,这时全局变量 i = 1
foo[0] = function () {
    console.log(i)
}

// 迭代第2次定义,这时全局变量 i = 2
foo[1] = function () {
    console.log(i)
}

// 迭代第3次定义,这时全局变量 i = 3
foo[2] = function () {
    console.log(i)
}

解决:

为了达到我们的预期结果,可以使用闭包作用域的方式,把每次迭代的变量 i 保存起来,以便在后面执行时访问。这里使用 IIFE 立即执行函数表达式来创建作用域。

var foo = []

// IIFE
for (var i = 0; i < 3; i++) {
    foo[i] = (function (j) {
        return function () {
            console.log(j)
        }
    })(i)
}

foo[0]()
foo[1]()
foo[2]()

// 输出结果:
// 0
// 1
// 2

// ES6
for (let i = 0; i < 3; i++) {
    foo[i] = function () {
        console.log(i)
    }
}

foo[0]()
foo[1]()
foo[2]()

// 输出结果:
// 0
// 1
// 2