潜入理解闭包

550 阅读5分钟

作用域链

  • 当一个函数被调用的时候,都会创建一个执行环境以及相应的作用域链
  • 使用arguments和其他命名参数初始化这个函数的活动对象;位于作用域链的第一位
  • 外部函数的活动对象位于作用域链第二位···最后一位是全局执行环境(全局变量对象)
    • 全局变量对象始终存在
    • 局部活动对象仅在函数执行时存在
  • 一般情况下,函数执行后,本地活动对象被销毁;内存中仅保存全局作用域
function compare (a, b) {
    if (a < b){
        return -1
    }else if (a > b) {
        return 1
    }
    return 0
}
var result = compare(5, 10)

函数执行时的作用域链

上图即展示了,compare()函数执行时的作用域链:

  • compare()内部活动变量位于作用域链第一位
  • 外部变量对象,位于第二位;此处外部活动对象即为全局变量对象
  • compare()函数执行完成,内部活动对象即被销毁,全局依然保存

但是,闭包不同于普通函数。

闭包

闭包的作用域链

当一个函数返回另外一个函数(闭包)时,返回的这个函数会将包含它的函数的活动对象加入其作用域链。当然,我们先达成共识:每个函数都有其作用域链

那么闭包的作用域链就有(从内到外):闭包本身的活动变量->闭包包含函数(返回闭包的那个函数)的活动变量->全局变量对象

function createComparisonFunction(propertyName) {
    return function (obj1, obj2) {
        var val1 = obj1[propertyName]
        var val2 = obj2[propertyName]
        if (val1 > val2) {
            return -1
        }else if (val1 < val2) {
            return 1
        }
        return 0
    }
}
// 创建函数
var compareNames= createComparisonFunction('name')
// 调用函数
var result = compareName({name: 'aa'},{name: 'nn'})
// 解除对匿名函数的引用,释放内存
compareNames = null

通过下图我们可以看出

  • 每个函数调用时都产生一个作用域链
  • compareNames()调用时,其作用域链会包括(由内到外):闭包自己的活动变量->闭包的包含函数的活动变量->全局变量对象
  • 闭包包含函数createComparisonFunction()(外部函数|返回闭包的那个函数)调用完成之后,其执行环境的作用域链会被销毁,但其活动对象不会被销毁,因为···
  • 因为闭包的作用域链还在,仍然在引用这个活动对象,那么闭包的作用域链何时销毁
  • compareNames = null时,解除对匿名函数的应用,此时闭包的执行环境的作用域链被销毁

使用闭包时的作用域链

闭包可能带来的问题

变量的问题

在之前的阅读中,我们知道闭包的作用域链会保存其包含函数的活动对象,那么可以这么说:闭包保存着其包含函数的变量的最终值。那么问题来了:

// 一道常见的面试题
function countFns () {
    var fns = []
    for (var i = 0; i < 10; i++) {
        fns[i] = function () {
            console.log(i)
        }
    }
    return fns
}
// 得到函数数组
var theFns = countFns()
theFns[0]() // 我们的期望输出是0,但结果却是10

我们再来体会一下上面那句话,闭包的作用域链包括其包含函数的活动对象,保存着其包含函数变量的最终值。因为,在本题中,闭包中的i最后为10,所以theFns中所有函数输出都是10。那么如何解决这个问题呢?let?yes,let是其中一种

解决方法一
// 使用let局部变量
function countFns () {
    let fns = []
    for (let i = 0; i < 10; i++) {
    // 其实在这里声明了一个隐藏作用域 let i = i(循环的i)
        fns[i] = function () {
            console.log(i)
        }
    }
    return fns
}
// 得到函数数组
let theFns = countFns()
theFns[0]() // 0,这回就对了
theFns[1]() // 1,这回就对了
theFns[2]() // 2,这回就对了

乍一看觉得不应该和var声明一样嘛?!其实不然,事实上会在循环体(大括号)内再有个隐藏作用域let i = i,所以函数数组中每个函数都有一个不同的i。所以输出自然会不同。详情参考方方老师这篇文章:我用了两个月的时间才理解 let

解决方法二

解决方法二其实告诉了我们一点:ES5中如何创建局部变量

先来整理思路,ES5中使用局部变量要用到匿名立即执行函数,我们要得到的数组的每个元素都是一个函数,可以用闭包。确认这两点,可以coding了

function countFns () {
    var fns = []
    for (var i = 0; i < 10; i++) {
        fns[i] = (function (num) {
            return function () {
                console.log(num)
            }
        })(i)
    }
    return fns
}

为什么匿名立即执行函数可以创建局部变量:在匿名函数中定义的任何变量,都会在执行结束后立即销毁。

this对象的问题

非严格模式下,全局变量this指的是Window对象(浏览器环境下)。而在闭包使用this则指向当前匿名函数中this

下面看一个栗子:在这个例子中,obj.sayName()返回一个函数,此函数的执行对象不是obj而是Window:我们可以这么理解var temp = obj.sayName(),然后再执行temp()函数。那么闭包中的this很明显就是全局作用域中的this,所以输出全局变量name

var name = 'The Window'
var obj = {
    name: 'The obj',
    sayName () {
        return function () {
            console.log(this.name)
        }
    }
}

obj.sayName()() // The Window

argumentsthis存在同样的问题,每个函数调用时都会自动取得两个特殊变量:this和arguments。内部函数搜索这两个值时,只会搜索到其内部活动对象即止。


本文中示例引用自:Javascript高级程序设计