JavaScript学习笔记(十一) 闭包

什么是闭包?我们先来看看《JavaScript 权威指南》中的定义:

函数对象可以通过作用域链关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性称为闭包

哈哈哈看完是不是一脸懵呢?没关系,下面我们从最简单的作用域、作用域链开始,一步步探索究竟什么是闭包

1、作用域

(1)函数作用域

什么是作用域?一个变量的作用域就是源代码中定义这个变量的区域,在 JavaScript 中采用的是函数作用域

也就是说,变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的

function scope() {
    if (true) {
        var i = 0
        for (var j = 1; j <= 5; j++) {
            i = i + j
        }
        console.log(j)
    }
    console.log(i)
    ~function() { // 立即执行函数
        console.log(i)
        console.log(j)
    }()
}

scope()

/*
 * 执行结果:
 * 6
 * 15
 * 15
 * 6
**/

(2)声明提前

JavaScript 采用函数作用域,这意味着在函数内声明的所有变量在该函数体内都是可见的

这导致了变量在声明前就已可用,在 JavaScript 中这种现象称为声明提前,但要注意的是赋值不会提前

function hoisting() {
    console.log(i) // 声明提前,但赋值并不会提前
    var i = 'hello'
    console.log(i)
}

hoisting()

/*
 * 执行结果:
 * undefined
 * hello
**/

2、作用域链

(1)声明上下文对象

我们知道,在 JavaScript 中全局变量是全局对象的属性,但是很多人不知道局部变量也是某个对象的属性

这个对象叫做声明上下文对象,它是与函数调用相关的一个对象,作为一种内部实现,我们无法直接引用这个对象

(2)作用域链

每段 JavaScript 代码(全局代码或函数)都有一个与之关联的作用域链

所谓的作用域链其实就是一个对象列表,这组对象定义这段代码作用域中的变量,称为变量对象

一般来说,变量对象包括定义全局变量的全局对象和定义局部变量的声明上下文对象

在全局代码中(就是不包含在函数定义中的代码),作用域链上只有一个对象,就是全局对象

对于没有包含嵌套的函数,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象

对于一个包含嵌套的函数,它的作用域链上至少存在三个对象(想一想这三个对象分别是什么)

(3)变量解析

当 JavaScript 需要查找一个变量 x 的值时,它会从作用域链上的第一个对象开始查找

如果这个对象上有名为 x 的属性,则直接使用该属性的值作为变量 x 的值

如果这个对象没有名为 x 的属性,就会查找作用域链上的下一个对象,直至到达作用域链的最后

如果作用域链上没有任何一个对象存在名为 x 的属性,那么抛出一个引用异常

(4)函数的作用域链

当定义一个函数时,就会保存一个作用域链;当调用这个函数时,就会创建一个新的对象用于保存局部变量

然后将这个对象添加到保存的作用域链上,并且创建一个新的作用域链表示函数调用的作用域

当函数返回的时候,就会从保存的作用域链上删除这个对象,但是这并不意味着对象马上就被当作垃圾回收

因为按照 JavaScript 的垃圾回收机制,只有当对象没有被引用的时候,才会被 JavaScript 回收

3、闭包

(1)闭包的理解

如果一个函数内部包含嵌套函数(但是没有返回嵌套函数),那么外部函数返回后,嵌套函数也被回收

闭包的核心在于一个函数包含嵌套函数,并且返回这个嵌套函数,这时就有一个外部的引用指向这个嵌套的函数

这个嵌套函数就不会被当作垃圾回收,它所对应的作用域链也被保存下来

var global_scope = 'global'
function outer_function(outer_params) {
    var outer_scope = 'outer'
    console.log(outer_scope)
    // console.log(inner_scope) -> inner_scope is not defined
    var inner_function = function(inner_params) {
        var inner_scope = 'inner'
        console.log(outer_scope)
        console.log(inner_scope)
    }
    return inner_function
}
outer_function('outer')('inner')

/*
 * 执行结果:
 * outer
 * outer
 * inner
**/

在调用 outer_function 的时候,作用域链如下:

  1. outer_function { outer_params: 'outer', outer_scope: 'outer', inner_function: function }
  2. window { global_scope: 'global' }

因此在打印 outer_scope 的时候,首先查找 outer_function 对象,里面能够找到 outer_scope 属性,直接返回

如果要打印 inner_scope,查找 outer_function 对象和 window 对象都没有找到 outer_scope 属性,抛出异常


在调用 inner_function 的时候,作用域链如下:

  1. inner_function { inner_params: 'inner', inner_scope: 'inner'}
  2. outer_function { outer_params: 'outer', outer_scope: 'outer', inner_function: function }
  3. window { global_scope: 'global', outer_function: function }

在打印 outer_scope 时,首先查找 inner_function 对象,没有找到,然后查找 outer_function 对象,能够找到

在打印 inner_scope 时,首先查找 inner_function 对象,能够找到

(2)闭包的应用

  • 定义私有属性
var counter = (function() { // 立即执行函数,返回一个对象
    var value = 0 // 私有属性,无法直接访问
    var changeBy = function(val) { value += val } // 私有方法,无法直接访问
    // 以下多个嵌套函数共享一个作用域链
    return {
        getValue: function() { return value },
        increase: function() { changeBy(+1) },
        decrease: function() { changeBy(-1) }
    }
})()

console.log(counter.getValue())
counter.increase()
console.log(counter.getValue())
counter.decrease()
console.log(counter.getValue())

/*
 * 执行结果:
 * 0
 * 1
 * 0
**/
  • 缓存处理结果
function memory(f) {
    // 缓存处理结果
    var cache = {}
    return function() {
        // 将传入的参数作为键
        var key = arguments.length + Array.prototype.join.call(arguments, ',')
        if (key in cache) { // 如果值在缓存,直接读取返回
            return cache[key]
        } else { // 否则执行计算,并把结果放到缓存
            return cache[key] = f.apply(this, arguments)
        }
    }
}

var factorial = function(n) { return (n <= 1) ? 1 : n * factorial(n - 1) }

var factorialWithMemory = memory(factorial)

(3)使用闭包的注意事项

  • 外部变量的值是否改变
function test() {
    var array = []
    for(var count = 0; count < 5; count++) {
        array[count] = function() { console.log(count) }
    }
    return array
}

var result = test()
result[0]()
result[1]()
result[2]()
result[3]()
result[4]()

/*
 * 执行结果:
 * 5
 * 5
 * 5
 * 5
 * 5
**/

/*
 * 结果分析:
 * 在调用数组中的函数时,由于需要打印变量 count 的值,所以沿着这些函数的作用域链往上查找
 * 最终在外层函数 test 的变量对象中找到,但是此时局部变量 count 的值已经变成 5
**/

解决办法

function test() {
    var array = []
    for(var count = 0; count < 5; count++) {
        array[count] = function(value) { // 立即执行函数
            return function() { console.log(value) }
        }(count)
    }
    return array
}

var result = test()
result[0]()
result[1]()
result[2]()
result[3]()
result[4]()

/*
 * 执行结果:
 * 0
 * 1
 * 2
 * 3
 * 4
**/

/*
 * 结果分析:
 * 同样道理,由于需要打印变量 value 的值,所以在调用数组中的函数时需要往上查找作用域链
 * 但是此时在上层函数对应的变量对象中即可找到 value,并且 value 的值等于当时传入的 count 的值
**/
  • this 的指向是否符合预期
var test = {
    value: 0,
    getValue: function() {
        return function() { console.log(this.value) }
    }
}

var result = test.getValue()
result()

/*
 * 执行结果:
 * undefined
**/

/*
 * 结果分析:
 * 在调用闭包函数时,是在全局作用域的环境下执行的,因此 this 指向全局对象
**/

解决方法

var test = {
    value: 0,
    getValue: function() {
        return function() { console.log(this.value) }.bind(this)
    }
}

var result = test.getValue()
result()

/*
 * 执行结果:
 * 0
**/

/*
 * 结果分析:
 * 使用 bind 函数将 this 的值绑定为 test 对象
**/

【 阅读更多 JavaScript 系列文章,请看 JavaScript学习笔记 】

你可能感兴趣的