看完这篇,闭包你就懂了。

306 阅读8分钟

理解闭包吗?

这个问题其实在问:

  1. 闭包是什么?
  2. 闭包有什么作用?

闭包是什么?

MDN:闭包是函数和声明该函数的词法环境的组合。

简单讲,闭包就是指有权访问另一个函数作用域中的变量的函数。(《JavaScript高级程序设计》)

从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链。(《JavaScript权威指南》)

按照我的理解就是:闭包= 「函数」和「函数体内可以访问的变量总和」

举一个简单的例子:

(function() {
    var a = 1;
    function add() {
        var b = 2

        var sum = b + a
        console.log(sum); // 3
    }
    add()
})()

add函数本身,以及其内部可访问的变量,即 a = 1,这两个组合在一起就被称为闭包,仅此而已。

闭包的作用

局部变量无法共享和长久的保存,而全局变量可能造成变量污染,所以我们希望有一种机制既可以长久的保存变量又不会造成全局污染。 闭包最大的作用就是隐藏变量,闭包的一大特性就是内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回了之后 基于此特性,JavaScript可以实现私有变量、特权变量、存储变量等。 我们就以私有变量举例,私有变量的实现方法有很多,有靠约定的(变量名前面加短下划线的),有靠Proxy代理的,也有靠Symbol这种新的数据类型的。 但是真正广泛流行的其实是使用闭包。

function Person() {
	var name = 'cxk';
	this.getName = function() {
		return name;
	}
	this.setName = function(value) {
		name = value;
	}
}
const cxk = new Person();	
console.log(cxk.getName);	//cxk
cxk.setName('jntm');
console.log(cxk.getName);	//jntm
console.log(name) //name is not defined

不使用闭包的情况

在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 变量,这使得当我执行下面的代码时,它没有办法分辨现在到底是在对狗计数还是对猫计数,进而导致把猫的数量和狗的数量交错计算的错误情况,除非我们单独设置一个 countDogsNum 和 countCatsNum:

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里面能有一个计算dogs的count变数;而countCats里面也能有一个计算cats的count变量,两者是不会互相干扰的。 为了达到这样的效果,我们就要利用闭包,让变量保留在该函数中而不会被外在环境干扰。 改成闭包的写法会像这样:

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因此也不会相互干扰。

进一步了解和使用闭包

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)
}

这篇文章「JavaScript系列之闭包(Closure)」写的非常好,仔细看完对闭包理解能够更上一层。