JavaScript系列之闭包(Closure)

栏目: JavaScript · 发布时间: 4年前

内容简介:相信很多初学者在学习JavaScript 的时候,一直对闭包(closure) 有所疑惑。因为从字面上来看,完全看不出它所代表的东西。那么今天,我想通过这篇文章,尽量用简单易懂的话来与各位介绍「闭包」到底是什么。在具体介绍闭包之前,为了更好的理解本文要介绍的内容,建议先去阅读前面的文章首先,先来看看MDN 对闭包的定义:

相信很多初学者在学习JavaScript 的时候,一直对闭包(closure) 有所疑惑。因为从字面上来看,完全看不出它所代表的东西。那么今天,我想通过这篇文章,尽量用简单易懂的话来与各位介绍「闭包」到底是什么。

在具体介绍闭包之前,为了更好的理解本文要介绍的内容,建议先去阅读前面的文章 《JavaScript系列之变量对象》《JavaScript系列之作用域和作用域链》 ,因为它们相互之间都是有关联的。

闭包是什么?

首先,先来看看MDN 对闭包的定义:

闭包是指那些能够访问自由变量的函数。

那什么是自由变量呢?

自由变量是一个既不是函数的形参,也不是函数的局部变量的变量。

由此,我们可以看出闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量

好,如果上面三行就看得懂的话那么就不用再往下看了,Congratulations!

...... 不过如果你是初学者的话,我想应该不会,如果仅用三言两语就把闭包讲通,那还能称为Javascript 语言的一个难点吗?

先来举个例子:

var n = 1;

function f1() {
    console.log(n);  // 1
}

f1() 
复制代码

f1 函数可以访问变量 n ,但是 n 既不是 f1 函数的形参,也不是 f1 函数的局部变量,所以这种情况下的 n 就是自由变量。其实上面代码中就存在闭包了,即函数 f1 + f1 函数访问的自由变量 n 就构成了一个 闭包

上面代码中,函数 f1 可以读取全局自由变量 n 。但是,函数外部无法读取函数内部声明的变量:

function f1() {
    var n = 1;
}

console.log(n)  // Uncaught ReferenceError: n is not defined
复制代码

如果有时需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过改变形式才能实现。那就是在函数的内部,再定义一个函数。

function f1() {
  var n = 1;
  function f2() {
    console.log(n); // 1
  }
  return f2;
}

var a = f1();
a();
复制代码

上面代码中,函数 f2 就在函数 f1 内部,这时 f1 内部的所有局部变量,对 f2 都是可见的。既然 f2 可以读取 f1 的局部变量,那么只要把 f2 作为返回值,我们就可以在 f1 外部读取它的内部变量了。

所以闭包是一个可以从另一个函数的作用域访问变量的函数。这是通过在函数内创建函数来实现的。当然,外部函数无法访问内部范围。

在我们深入研究闭包之前,有必要先从不使用闭包的情况切入,了解为什么要用闭包。

不使用闭包的情况

在JavaScript 中,全局变量的错用可能会使得我们的代码出现不可预期的错误。

假设我们现在要做一个计数的函数,一开始我们想要先写一个给狗的计数函数:

// 狗的计数函数
var count = 0

function countDogs () {
  count += 1
  console.log(count + ' dog(s)')
}

countDogs()    // 1 dog(s)
countDogs()    // 2 dog(s)
countDogs()    // 3 dog(s)
复制代码

接着继续写代码的其他部分,当写到后面时,我发现我也需要写猫的计数函数,于是我又开始写了猫的计数函数:

// 狗的计数函数
var count = 0

function countDogs () {
  count += 1
  console.log(count + ' dog(s)')
}


// 中间的其它代码...

// 猫的计数函数
var count = 0

function countCats () {
  count += 1
  console.log(count + ' cat(s)')
}

countCats()    // 1 cat(s)
countCats()    // 2 cat(s)
countCats()    // 3 cat(s)
复制代码

乍看之下好像没啥问题,当我执行 countDogs()countCats() ,都会让 count 增加,然而问题在于当我在不注意的情况下把 count 这个变量建立在了全局作用域底下时,不论是执行 countDogs() 或是 countCats() 时,都是用到了全局的 count 变量,这使得当我执行下面的代码时,它没有办法分辨现在到底是在对狗计数还是对猫计数,进而导致把猫的数量和狗的数量交错计算的错误情况:

countCats()    // 1 cat(s)
countCats()    // 2 cat(s)
countCats()    // 3 cat(s)

countDogs()    // 4 dog(s),我希望是 1 dog(s)
countDogs()    // 5 dog(s),我希望是 2 dog(s)

countCats()    // 6 cat(s),我希望是 4 cat(s)
复制代码

闭包让函数有私有变量

从上面的例子我们知道,如果错误的使用全局变量,很容易会出现一些莫名其妙的bug ,这时候我们就可以利用闭包(closure)的写法,让函数有自己私有变量,简单来说就是 countDogs 里面能有一个计算 dogscount 变数;而 countCats 里面也能有一个计算 catscount 变量,两者是不会互相干扰的。

为了达到这样的效果,我们就要利用闭包,让变量保留在该函数中而不会被外在环境干扰。

改成闭包的写法会像这样:

function dogHouse () {
  var count = 0
  function countDogs () {
    count += 1
    console.log(count + ' dogs')
  }
  return countDogs
}

const countDogs = dogHouse()
countDogs()    // "1 dogs"
countDogs()    // "2 dogs"
countDogs()    // "3 dogs"
复制代码

这样我们就将专门计算狗的变量 count 闭包在 dogHouse 这个函数中,在 dogHouse 这个函数中里面的 countDogs() 才是我们真正执行计数的函数,而在 dogHouse 这个函数中存在 count 这个变量,由于JavaScript变量会被缩限在函数的执行上下文中,因此这个 count 的值只有在 dogHouse 里面才能被取用,在 dogHouse 函数外是取用不到这个值的。

接着因为我们要能够执行在 dogHouse 中真正核心 countDogs() 这个函数,因此我们会在最后把这个函数给return出来,好让我们可以在外面去调用到 dogHouse 里面的这个 countDogs() 函数。

最后当我们在使用闭包时,我们先把存在 dogHouse 里面的 countDogs 拿出来用,并一样命名为 countDogs (这里变量名称可以自己取),因此当我执行全局中的 countDogs 时,实际上会执行的是 dogHouse 里面的 countDogs 函数。

上面这是闭包的基本写法: 一个函数里面包了另一个函数,同时会 return 里面的函数让我们可以在外面使用到它

我们可以把我们最一开始的代码都改成使用闭包的写法:

function dogHouse () {
  var count = 0
  function countDogs () {
    count += 1
    console.log(count + ' dogs')
  }
  return countDogs
}

function catHouse () {
  var count = 0
  function countCats () {
    count += 1
    console.log(count + ' cats')
  }
  return countCats
}

const countDogs = dogHouse()
const countCats = catHouse()

countDogs()    // "1 dogs"
countDogs()    // "2 dogs"
countDogs()    // "3 dogs"

countCats()    // "1 cats"
countCats()    // "2 cats"

countDogs()    // "4 dogs"
复制代码

当我们正确地使用闭包时,虽然一样都是使用 count 来计数,但是是在不同执行环境内的 count 因此也不会相互干扰。

进一步了解和使用闭包

另外,甚至在运用的是同一个 dogHouse 时,变量间也都是独立的执行环境不会干扰,比如:

function dogHouse () {
  var count = 0
  function countDogs () {
    count += 1
    console.log(count + ' dogs')
  }
  return countDogs
}

// 虽然都是使用 dogHouse ,但是各是不同的执行环境
// 因此彼此的变量不会互相干扰

var countGolden = dogHouse()
var countPug = dogHouse()
var countPuppy = dogHouse()

countGolden()     // 1 dogs
countGolden()     // 2 dogs

countPug()        // 1 dogs
countPuppy()      // 1 dogs

countGolden()     // 3 dogs
countPug()        // 2 dogs
复制代码

将参数代入闭包中

但是这么做的话你可能觉得还不够清楚,因为都是叫做 dogs ,这时候我们一样可以把外面的变量通过函数的参数代入闭包中,像是下面这样,返回的结果就清楚多了:

// 通过函数的参数将值代入闭包中
function dogHouse (name) {
  var count = 0
  function countDogs () {
    count += 1
    console.log(count + ' ' + name)
  }
  return countDogs
}

// 同样是使用 dogHouse 但是使用不同的参数
var countGolden = dogHouse('Golden')
var countPug = dogHouse('Pug')
var countPuppy = dogHouse('Puppy')

// 结果看起来更清楚了
countGolden()     // 1 Golden
countGolden()     // 2 Golden

countPug()        // 1 Pug
countPuppy()      // 1 Puppy

countGolden()     // 3 Golden
countPug()        // 2 Pug
复制代码

为了进一步简化代码,我们可以在闭包中直接return一个函数出来,我们就可以不必为里面的函数命名了,而是用匿名函数的方式直接把它返回出来。

因此写法可以简化成这样:

function dogHouse () {
  var count = 0
  // 把原本 countDogs 函数改成匿名函数直接放进来
  return function () {
    count += 1
    console.log(count + ' dogs')
  }
}

function catHouse () {
  var count = 0
  // 把原本 countCats 函数改成匿名函数直接放进来
  return function () {
    count += 1
    console.log(count + ' cats')
  }
}
复制代码

然后我们刚刚有提到,可以透过函数参数的方式把值代入闭包当中,因此实际上我们只需要一个counter ,在不同的时间点给它参数区分就好。这样子不管你是要记录哪一种动物都很方便了,而且代码也相当简洁:

function createCounter (name) {
  var count = 0
  return function () {
    count++
    console.log(count + ' ' + name)
  }
}

const dogCounter = createCounter('dogs')
const catCounter = createCounter('cats')
const pigCounter = createCounter('pigs')

dogCounter()     // 1 dogs
dogCounter()     // 2 dogs
catCounter()     // 1 cats
catCounter()     // 2 cats
pigCounter()     // 1 pigs
dogCounter()     // 3 dogs
catCounter()     // 3 cats
复制代码

闭包的实际应用

我们要实现这样的一个需求:点击某个按钮,提示点击的是"第n个"按钮,此处我们先不用事件代理:

.....
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<script type="text/javascript">
   var buttons = document.getElementsByTagName('button')
    for (var i = 0; i < buttons.length; i++) {
      buttons[i].onclick = function () {
        console.log('第' + (i + 1) + '个')
      }
    }
</script>  
复制代码

这时候可能会预期点选不同的按钮时,会根据每个button 点击顺序的不同而得到不同的结果。但是实际执行后,你会发现返回的结果都是“第四个”。这是因为 i 是全局变量,执行到点击事件时,此时 i 的值为3。

如果要强制返回预期的结果,那该如何修改呢?最简单的是用 let 声明 i

for (let i = 0; i < buttons.length; i++) {
    buttons[i].onclick = function () {
        console.log('第' + (i + 1) + '个')
    }
}
复制代码

简单来说,通过 let 可以帮我们把所定义的变量缩限在块级作用域中,也就是变量的作用域只有在 { } 内,来避免 i 这个变量跑到全局变量被重复覆盖。

另外我们可以通过闭包的方式来修改:

for (var i = 0; i < buttons.length; i++) {
    (function (j) {
        buttons[j].onclick = function () {
          console.log('第' + (j + 1) + '个')
        }
    })(i)
}
复制代码

希望看完这篇文章后,你能对于闭包有更清楚的认识。

如果觉得文章对你有些许帮助,欢迎在 我的GitHub博客 点赞和关注,感激不尽!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Design systems

Design systems

Not all design systems are equally effective. Some can generate coherent user experiences, others produce confusing patchwork designs. Some inspire teams to contribute to them, others are neglected. S......一起来看看 《Design systems》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

在线进制转换器
在线进制转换器

各进制数互转换器

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码