什么是闭包?
闭包的概念:
《JavaScript》权威指南: 函数对象可以通过作用域链相互关联起来,函数体内部的变量可以保存在函数作用域内,这种特性称为“闭包”。
通俗的说:所谓闭包,就是一个函数,这个函数能够访问其他函数作用域中的变量。
或者说:闭包,有权访问另一个函数作用域中的变量的函数;一般情况就是在一个函数中包含另一个函数(被包含的函数就是闭包)。
函数的作用域是独立的、封闭的,外部的执行环境是访问不了的,但是闭包具有这个能力和权限。
那闭包是怎样的一个表现形式呢?
第一,闭包是一个函数,而且存在于另一个函数当中
第二,闭包是可以访问到父级函数的变量,且该变量不会销毁
function person () {
var name = '小樱'
function cat() { // 这个是一个内部的函数,是一个闭包
console.log(name)
}
return cat
}
var per = person() // per 的值就是return后的结果,即cat函数
per(); // 结果: 小樱,per()就相当于cat()
per(); // 小樱
per(); // 小樱
闭包的原理
闭包的实现原理,其实是利用了作用域链的特性,我们都知道作用域链就是在当前执行环境下访问某个变量时,如果不存在就一直向外层寻找,最终寻找到最外层也就是全局作用域,这样就形成了一个链条。
例如:
var age = 18
function cat () {
age++
console.log(age) // cat函数内输出了age,该作用域没有,则向外层寻找,找到了,输出19
}
cat() // 19
这个时候如果继续调用,结果就会一直增加,也就是变量age的值一直在递增
cat() // 20
cat() // 21
cat() // 22
如果程序还有其他函数,也需要用到age的值,则会受到影响,而且全局变量还容易被人修改,比较不安全,这就是全局变量容易污染的原因,所以我们必须解决变量污染问题,那就是把变量封装到函数内部,让它成为局部变量。
如下:
// f2 加括号表示返回的是函数值,不加括号返回的是函数体
// 示例一
function f1() {
var age = 18
function f2() { // 是一个内部函数,是一个闭包
age++
console.log('年龄-->', age)
}
return f2();
}
f1() // 19
f1() // 19
// 示例二
function f1() {
var age = 18
function f2() { // 是一个内部函数,是一个闭包
age++
console.log('年龄-->', age)
}
return f2;
}
var a = f1()
a() // 19
a() // 20
示例一:内部函数 f2() 在执行前,从外部函数返回。
示例二:f1( ) 执行后,将其返回值(也就是内部的 f2() 函数)赋值给变量a, 并调用a(),实际上只是通过不同的标示符引用调用了内部的函数f2。
f1() 函数执行后,正常情况下f1() 的整个内部作用域被销毁,占用的内存被回收。但是现在的f1() 的内部作用域f2() 还在使用,所以不会对其进行回收。f2() 依然持有对改作用域的引用,这个引用叫做闭包。这个函数在定义的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。
常见的闭包
function foo(a) {
setTimeout(function timer() {
console.log(a)
}, 1000)
}
foo(2)
foo执行1000ms后,它的内部作用域不会消失,timer 函数依然保有foo 作用域的引用。timer函数就是一个闭包。
定时器,事件监听器,Ajax请求,跨窗口通信,Web Workers 或者其他异步或同步任务中,只要使用回调函数,实际上就是闭包。
循环和闭包
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
上面的这段代码,预期是每隔一秒,分别输出0,1,2,3,4,但实际上依次输出的是5,5,5,5,5。首先解释一下5从哪里来的,这个循环的终止条件是 i 不在 < 5,条件首次成立时 i 的值是5,因此,输出显示的是循环结束时 i 的最终值。
延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的都是setTimeout(..., 0) ,所有的回调函数依然是在循环结束后才执行。因此每次都输出一个5来。
我们预期的是每个迭代在运行时都会给自己“捕获”一个 i 的的副本。但实际上,根据作用域的原理,尽管循环中的五个函数都是在各自迭代中分别定义的,但是他们都封闭在一个共享的全局作用域中,因此实际上只有一个 i。即所有函数共享一个 i 的引用。
改成下面这样,就可以按照我们期望的方式进行工作了。这样修改之后,在每次迭代内使用 IIFE (立即执行函数)会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代内部都会含有一个具有正确值的变量可以访问。
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j)
}, j * 1000)
})(i)
}
当然,使用ES6 块级作用域的 let 替换 var 也可以达到我们的目的。
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
闭包的优缺点
优点:
- 隐藏变量,避免全局污染
- 可以读取函数内部的变量
使用不当,优点就变成了缺点: - 导致变量不会被垃圾回收机制回收,造成内存消耗
- 不恰当的使用闭包可能会造成内存泄漏的问题。
为什么使用闭包时变量不会被垃圾回收机制销毁呢?
JS垃圾回收机制:
JS规定在一个函数作用域内,程序执行完以后变量就会被销毁,这样可以节省内存;使用闭包时,按照作用域链的特点,闭包(函数)外面的变量不会被销毁,因为函数会一直被调用,所有一直存在,如果闭包使用过多会造成内存泄漏。
理解闭包
理解闭包首先要了解嵌套函数的词法作用域规则,如下代码
// 示例一
var str = 'Hello World' // 全局变量
var a = function () {
var str = 'Hello 你好' // 局部变量
function f() {
return str
}
return f()
}
console.log('示例1 ---->', a()) // 打印结果: Hello 你好
a () 函数声明了一个局部变量,并定义了一个新的函数f(),函数f()返回了这个变量的值,最后将函数f()的执行结果返回。
变量提升
变量提升即将变量声明提升到它所在作用域的最开始的地方。
只有 var 可以将变量进行提升,const 跟 let 不会
下面举个例子:
变量提升(全局作用域)
// 示例1
console.log(a) // undefined
var a = 8
// 示例1 等价于
var a;
console.log(a) // undefined
a = 8
// 示例2
var a = 8;
function fn() {
console.log('1--->', a); // undefined
var a = 9;
console.log('2--->', a); // 9
}
fn()
// 示例2 等价于
var a = 8
function fn() {
var a;
console.log('1---->', a) // undefined
a = 9;
console.log('2---->', a) // 9
}