之前对闭包的概念和理解都不是很清晰,所以写下这一篇博客来加深自己对闭包的理解,如果有什么不对的地方欢迎指正!
作用域和作用域链
说到闭包就得提一下 JavaScript 的作用域和作用域链,JavaScript 只有全局作用域和函数作用域,没有块级作用域,不过 ES6 中出现了 let,补上了 JavaScript 没有块级作用域的短板。
// 全局作用域(定义在全局的变量,函数内部可以访问的到)
var global = 'global'
function f () {
console.log(global)
}
f() // => global
// 函数作用域(定义在函数内部的变量,函数外部访问不到)
function f2 () {
var inner = 'inner'
}
console.log(inner) // => Uncaught ReferenceError: inner is not defined
// 如果是用 var 声明的变量是没有块级作用域的,但是用 let 声明的变量是块级作用域(这里暂时不讨论let)
{
var foo = 'bar'
let foo2 = 'bar2' // 顺便举个 let 的栗子
}
console.log(foo) // => bar
console.log(foo2) // => Uncaught ReferenceError: foo2 is not defined
总结:如果在当前作用域中找不到想要的变量,则通过作用域链向在父作用域中继续查找,直到找到第一个同名的变量为止,若果找不到,抛出 ReferenceError 错误,这就是 JavaScript 中作用域链的概念。那么问题来了,子作用域可以根据作用域链访问父作用域中的变量,那如果父作用域想访问子作用域中的变量呢?这时候就需要通过闭包来实现。
什么是闭包?
闭包就是能够读取其他函数内部变量的函数,由于在 JavaScript 语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成 “定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
在 JavaScript 高级程序设计(第3版)中是这样描述的:
闭包是指有权访问另一个函数作用域中的变量的函数。
闭包有什么作用?
- 可以在函数的外部访问到函数内部的局部变量。
- 让这些变量始终保存在内存中,不会随着函数的结束而自动销毁。
一些关于闭包的栗子?
例1:即使函数已经运行结束,导致创建变量的环境销毁,也依然会存在,直到访问变量的那个函数被销毁。
function fn () {
var count = 0
return function () {
return ++count
}
}
// 函数已经运行结束,创建变量的环境销毁
var fns = fn()
// 依然能够访问到函数内部的 count 变量,始终活跃在内存中
console.log(fns()) // => 1
console.log(fns()) // => 2
// 直到访问变量的那个函数被销毁,即释放对闭包的引用
fns = null
console.log(fns()) // => Uncaught TypeError: fns is not a function
// 重新创建一个,就又可以访问到了
fns = fn()
console.log(fns()) // => 1
例2:闭包只能取得包含函数中任何变量的最后一个值。因为闭包所保存的是整个变量对象,而不是某个特殊的变量。
var arr = []
for (var i = 0; i < 3; i++) {
arr[i] = function () {
console.log(i)
}
}
console.log(arr[0]()) // => 3
console.log(arr[1]()) // => 3
console.log(arr[2]()) // => 3
/*
解释:arr 数组中有三个函数,每个函数都是打印变量 i 的值,
但是 function 的作用域中没有变量 i,i 为 undefined,
则解析引擎会寻找父级作用域,发现父级作用域中有 i,且 for 循环绑定事件结束后,
i 的值为 3,所以每个函数打印的都是 3,这是作用域的问题。
*/
我们把每次的 i 都保存到一个变量中,匿名闭包就可以实现想要的效果,改写后就能够正常输出0,1,2。
var arr = []
for (var i = 0; i < 3; i++) {
arr[i] = (function (j) {
return function () {
console.log(j)
}
})(i)
}
/*
解释:这样就使用了闭包,这里面的闭包指的是 function () { console.log(j) }
第二个 function 里面打印的 j 是第一个 function 的参数,
通过 (i) 执行了这里面的第一个函数,同时 i 的值被保存到 j 中,
这样子每个点击事件中都有一个局部变量 j,j 保存的是相应的传入的 i 的值。
*/
例3:老生常谈的 for 循环中定时器的问题
console.log('start')
for(var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, 0)
}
console.log('end')
// => start
// => end
// => 3
// => 3
// => 3
/*
注意:setTimeout 的第二个参数不管是 1000 还是 0 都是一样的,
这是因为 JavaScript 是单线程的,异步任务总是在同步任务执行完之后才执行,就算设置成 0,
也要等到 for 循环执行完之后才执行 setTimeout。
*/
使用闭包稍作修改之后就能够正常输出啦。
console.log('start')
for(var i = 0; i < 3; i++) {
(function (i) {
setTimeout(function () {
console.log(i)
}, 0)
})(i)
/*
// 或者这样写
setTimeout((function (j) {
return function () {
console.log(j)
}
})(i), 0)
*/
}
console.log('end')
// => start
// => end
// => 0
// => 1
// => 2
闭包中 this 的指向问题
var name = "The Window"
var object = {
name: "My Object",
getNameFunc: function () {
return function () {
return this.name
}
}
}
console.log(object.getNameFunc()()) // => The Window
在上面这段代码中,object.getNameFunc()() 实际上是在全局作用域中调用了匿名函数,this指向了 Window。这里要理解函数名与函数功能是分割开的,不要认为函数在哪里,其内部的this 就指向哪里。Window 才是匿名函数功能执行的环境。如果想使 this 指向外部函数的执行环境,可以这样改写:
var name = "The Window"
var object = {
name: "My Object",
getNameFunc: function () {
var that = this
return function () {
return that.name
}
}
}
console.log(object.getNameFunc()()) // => My Object
总结
闭包的优点:
- 能够在减少全局变量污染的情况下,长久保存局部变量。
- 减少了全局变量的使用,增强了网页的安全性。
闭包的缺点:
- 常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露。
- 如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。