前言
这篇文章是有关 JS 闭包概念以及优缺点和经典使用场景
1. 闭包是什么?
- 官方说法:闭包就是指有权访问另一个函数作用域中的变量的函数。
- MDN 说法:闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
- 其他理解:本质就是上级作用域内变量的生命周期,因为被下级作用域内引用,而没有被释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。
function makeFunc() {
var name = "Mozilla"
function displayName() {
alert(name)
}
return displayName
}
var myFunc = makeFunc()
myFunc() // Mozilla
var myFunc = makeFunc()中的makeFunc()创建了相应的函数执行上下文,在该上下文中声明了一个局部变量name 并且声明了一个函数displayname,最后返回displayname函数的指针(或引用),即把function displayName(){alert(name);} 返回给了myFunc变量
var myFunc = makeFunc()执行结束后,相应的函数执行上下文就从栈中弹出,一般情况下,其变量也会随之销毁 ,但是myFunc()调用了myFunc,即执行了function displayName(){alert(name);},这里的name引用着makeFunc里的变量name, 所以其变量并不会随着销毁,相当于封装了一个私有变量.
这就是闭包。
2. 闭包的执行顺序
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
第 1-8 行。我们在全局执行上下文中创建了一个新的变量createCounter,并赋值了一个的函数定义。
第9行。我们在全局执行上下文中声明了一个名为increment的新变量。
第9行。我们需要调用createCounter函数并将其返回值赋给increment变量。
第 1-8行。调用函数,创建新的本地执行上下文。
第2行。在本地执行上下文中,声明一个名为counter的新变量并赋值为 0;
第 3-6行。声明一个名为myFunction的新变量,变量在本地执行上下文中声明,变量的内容是为第4行和第5行所定义。
第7行。返回myFunction变量的内容,删除本地执行上下文。变量myFunction 和counter不再存在。此时控制权回到了调用上下文。
第9行。在调用上下文(全局执行上下文)中,createCounter返回的值赋给了increment,变量increment现在包含一个函数定义内容为createCounter返回的函数。它不再标记为myFunction````,但它的定义是相同的。在全局上下文中,它是的标记为labeledincrement```。
第10行。声明一个新变量 c1。
继续第10行。查找increment变量,它是一个函数并调用它。它包含前面返回的函数定义,如第4-5行所定义的。
创建一个新的执行上下文。没有参数,开始执行函数。
第4行。counter=counter + 1。在本地执行上下文中查找counter变量。我们只是创建了那个上下文,从来没有声明任何局部变量。让我们看看全局执行上下文。这里也没有counter变量。Javascript会将其计算为counter = undefined + 1,声明一个标记为counter的新局部变量,并将其赋值为number 1,因为undefined被当作值为 0。
第5行。我们变量counter的值 1,我们销毁本地执行上下文和counter变量。
回到第10行。返回值1被赋给c1。
第11行。重复步骤10-14,c2也被赋值为1。
第12行。重复步骤10-14,c3也被赋值为1。
第13行。我们打印变量c1 c2和c3的内容。
详细请看链接我从来不理解JavaScript闭包,直到有人这样向我解释它
2. 闭包的优缺点?
优点:私有化数据,在私有化数据的基础上保持数据
缺点:可能会导致内存泄漏,内部的变量不会被自动回收掉(不正当的闭包会导致内存泄漏,这时面试官可能会问到关于垃圾回收机制)
3.闭包的作用
- 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
- 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化
4.闭包的经典应用场景
防抖,节流 …
vue :响应式原理
- return 回一个函数
var n = 10
function fn(){
var n =20
function f() {
n++;
console.log(n)
}
return f
}
var x = fn()
x() // 21
- 函数作为参数
var a = '千堆雪'
function foo(){
var a = 'foo'
function fo(){
console.log(a)
}
return fo
}
function f(p){
var a = 'f'
p()
}
f(foo())
/* 输出
* foo
/
使用 return fo 返回回来,fo() 就是闭包,f(foo()) 执行的参数就是函数 fo,因为 fo() 中的 a 的上级作用域就是函数foo(),所以输出就是foo
- IIFE(自执行函数)
var n = '千堆雪';
(function p(){
console.log(n)
})()
/* 输出
* 千堆雪
/
同样也是产生了闭包p(),存在 window下的引用 n。
- 循环赋值
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}
因为存在闭包的原因上面能依次输出1~10,闭包形成了10个互不干扰的私有作用域。将外层的自执行函数去掉后就不存在外部作用域的引用了,输出的结果就是连续的 10。为什么会连续输出10,因为 JS 是单线程的遇到异步的代码不会先执行(会入栈),等到同步的代码执行完 i++ 到 10时,异步代码才开始执行此时的 i=10 输出的都是 10。
- 使用回调函数就是在使用闭包
window.name = '千堆雪'
setTimeout(function timeHandler(){
console.log(window.name);
}, 100)
经典面试题
- for 循环和闭包(号称必刷题)
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]()
/* 输出
3
3
3
/
这里的 i 是全局下的 i,共用一个作用域,当函数被执行的时候这时的 i=3,导致输出的结构都是3。
使用闭包改善上面的写法达到预期效果,写法1:自执行函数和闭包
var data = [];
for (var i = 0; i < 3; i++) {
(function(j){
setTimeout( data[j] = function () {
console.log(j);
}, 0)
})(i)
}
data[0]();
data[1]();
data[2]()
写法2:使用 let
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]()
let 具有块级作用域,形成的3个私有作用域都是私有的互不干扰。
闭包和作用域
简单来说,作用域 指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限。
javascript 中大部分情况下,只有两种作用域类型:
- 全局作用域:全局作用域为程序的最外层作用域,一直存在。
- 函数作用域:函数作用域只有函数被定义时才会创建,包含在父级函数作用域 / 全局作用域内。
当函数被执行时,总是先从函数内部找寻局部变量,如果找不到相应的变量,则会向创建函数的上级作用域寻找,直到找到全局作用域为止,这个过程被称为作用域链。
闭包的本质是利用了作用域的机制,来达到外部作用域访问内部作用域的目的。