先打个补丁
要理解JS闭包,你首先要知道作用域链与词法作用域。
作用域链
- 每一个执行上下文的变量环境中都存在一个 outer 指针,用来指向外部的上下文。
- 当 V8 在查找一个变量时,在当前执行上下文中没有找到,就会顺着 outer 所指向的那个执行上下文查找,以此类推,直到找到全局为止
词法作用域
这个函数声明在哪,其函数词法声明作用域就在那。词法环境这个空间也可以看作为一个栈
作用域链与词法作用域链实战
来一段代码加深一下印象
function bar(){
console.log(myName);
}
function foo(){
var myName = 'x'
bar()
}
var myName = 'y'
foo()
输出 y,我们来梳理一下这个过程。
- 运行代码之前,首先要预编译。这个过程中会创建一个全局执行上下文。
myName声明,值从undifined后被统一为you,存放在变量环境中;foo()、bar()声明,值为function(),存放在词法环境中- 执行。要调用
foo(),因此产生一个foo()的执行上下文 bar函数、foo函数都定义在全局作用域中,因此其词法父级都是全局作用域- 所以
bar()输出的myName不是从foo()中得到,而是在其词法父级 全局作用域中得到y
要点在于函数声明的位置,其词法外级是什么。全局作用域的词法父级是
null
闭包
先给总结:
- 一个函数执行完毕后,它的执行上下文会被销毁
- 根据作用域的查找规则,内部函数一定可以访问外部函数中的变量。
- 当一个外部函数中的内部函数被拿到外部函数之外来执行,哪怕外部函数执行完毕了,被内部函数引用的那部分变量依然需要保留,我们把这部分变量的集合称之为闭包
闭包 = 函数 + 它引用的外部变量的集合
从经典的例子入手
来看一段代码
function foo(){
var myName = 'zhang'
var age = 18
function bar(){
console.log(myName)
}
return bar
}
var baz = foo()
baz()
输出为zhang 这段代码又做了什么?
- foo()执行,内部声明了 myName 、 age 、和内部函数 bar
- foo()把 bar 返回了出去,赋值给全局变量 baz
- foo()执行完毕,它的执行上下文被销毁
- 调用 baz(),却输出了 myName 的值
这是怎么回事?这就是上面说的闭包:虽然foo()执行完了,其执行上下文应当被销毁,但 bar 函数引用了其中的一个变量 myName,所以 myName 被保留了下来
闭包怎样形成
- 存在函数嵌套(一个函数里面定义了另一个函数)
- 内部函数引用了外部函数的变量
- 内部函数被带到了外部执行(例如
return出去、push到数组、作为回调)
闭包的三种写法
函数return函数
例如
function createCounter(){
let count = 0
return function(){
count++
return count
}
}
const counter = createCounter()
console.log(counter())
console.log(counter())
console.log(counter())
你看这段代码。分别输出了三次 count,不是直接输出,而是将函数的值赋给了一个变量,再调用这个变量,相当于直接调用这个函数的内部函数
每次调用的 counter()操作的都是同一个 count,闭包将其保留下来了
自然会输出
1
2
3
函数作为参数传递
function fn(){
let msg = 'Hello Closure'
setTimeout(function(){
console.log(msg)
}, 1000)
}
fn()
1 秒后输出 Hello Closure
setTimeout 的回调函数被带到了外部执行,但它还可以访问 fn 内部的 msg
经典的面试题,循环中的闭包
var arr = []
for(var i = 1;i<= 5; i++){
function fn(j){
arr.push(function(){
console.log(j);
})
}
fn(i)
}
for(let n = 0;n< arr.length;n++){
arr[n]()
}
面试官想让我用闭包输出1-5 五个数字。如果直接输出。像这样:
var arr = []
for(var i = 1; i <= 5; i++){
arr.push(function(){
console.log(i)
})
}
for(let n = 0; n < arr.length; n++){
arr[n]()
}
会输出什么?1-5?还是5个5?
是6个6。
- var i是函数作用域,5次循环共用同一个 i
- 循环结束时,i = 6
- 所有的函数引用的都是同一个 i,因此全部输出 6
回到示例的代码,注意看我们采用函数嵌套函数的写法
function fn(j){
arr.push(function(){
console.log(j);
})
这样一来形成了闭包,每次调用都是重新调用内层的 function()函数
function()函数是无名函数,这里不展开讲
当然,你可以用最简单直接的办法,不用古老的 var,for循环内换成let i即可。
注意一个坑:
var bar = add()
for(let i = 0; i < 3; i++){
console.log(bar())
}
这是同一个闭包,count累加,会输出 1,2,3
for(let i = 0; i < 3; i++){
console.log(add()())
}
而这是每次新建闭包,count充值为0,会输出 1,,1,1
那如果我想用 add()()实现累加,怎么办?可以把 count提到外面:
let count = 0
function add(){
return function(){
count++
return count
}
}
for(let i = 0; i < 3; i++){
console.log(add()())
}
闭包的优缺点
闭包的有点
- 定义私有变量,外部无法直接访问闭包内部的变量,只能通过暴露的接口操作
- 防止全局变量污染,变量限定在函数内部,不会污染全局命名空间
- 实现模块化,将相关功能封装在闭包中,只暴露必要接口。
闭包的缺点
内存泄漏:
正常情况下,一个函数执行完毕后,其执行上下文会被销毁,内部变量被回收,但闭包中的变量因为仍然被引用而无法被销毁,一直占用内存
如果闭包被全局变量引用,那这块内存将永远无法释放
总结
一句话记住闭包
一个函数记住了它外面的变量,即使外面函数执行完了,这些变量依旧存在
闭包的三个必要条件
- 外层函数嵌套内存函数
- 内层函数引用外层变量
- 把内层函数拿出去
###注意事项
- 非必要不写闭包,防止爆栈、不必要的内存占用
- 优先使用let,可以避免很多麻烦
- 理解词法作用域是理解闭包的关键,函数的作用域看定义位置,不看调用位置