作用域和闭包
什么是作用域
在理解作用域前,我们先来简单了解一下 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