在js中,闭包是一个基础且不可不了解的要点 在此之前,我们需要了解一些前置知识,那就是作用域和作用域链 作用域就是指在js中在程序中定义变量的区域,该位置决定了变量的生命周期,也就是说作用域就是变量和函数可访问的范围,控制着变量和函数的可见性与生命周期 我们都知道,在js引擎中var有着变量提升与预编译这一特性
一、作用域和作用域链
变量提升
console.log(a) //输出undefined 原因:在预编译阶段已经读取到a的存在,但仍未赋值,所以输出undefined
var a=1
在js引擎中执行阶段实际为
var a
console.log(a)
a=1
了解完预编译之后,接着我们再看两段代码,观察其中有多少作用域,与运行后会输出什么
var a =2
function fn(){
var b=2
console.log(a)
}
fn() // 输出结果为2
var a =2
function fn(){
var b=2
}
console.log(b) //报错,无输出结果
观察后我们可以得知,此段代码中有着全局作用域与一段fn函数作用域,但是观察代码后你可能会好奇,为何在全局作用域中用var定义的a,却可以在函数作用域中被访问,而函数作用域中定义的b却不能在全局作用域中被访问呢,这时就要引用出js中作用域链的机制了
1.作用域链的本质其实就是底层的查找机制
- 在函数被执行时,会优先查找当前函数作用域中的变量
- 如果当前作用域查找不到则会逐级查找父级作用域直到全局作用域
//全局作用域
let a =1
let b =2
function fn(){ //外部函数作用域
let a =1
function fn2(){ //内部函数作用域
a=2
console.log(a)
}
g() //调用g
}
f() //调用f
最终输出结果为2
总结: 1.嵌套关系的作用域串联起来形成了作用域链 2.内部函数作用域能访问外部函数作用域,而外部函数作用域无法访问内部函数作用域
二、闭包(closur)
在了解了函数作用域和作用域的相关知识后,我们来看看闭包究竟是怎么一回事 简单来说,就是内层函数访问了外层函数的变量,这时便产生了一个闭包
例如
function outer(){
let a=1
function inner(){
console.log(a)
}
return inner
}
let fun=outer()
fun() //输出1
此时就可以通过闭包实现在外部访问函数outer()内的变量,通过闭包绕过了在作用域链中,外部函数作用域无法访问内部函数作用域的限制
闭包的作用
function outer(){
let a=1 //根据函数作用域链的知识可知,函数outer()外部无法直接访问变量a,此时我们可以使用闭包来访问
function inner(){
console.log(a)
}
return inner //返回函数inner()函数体
}
let fn =outer() //使用fn接受inner()的函数体
fn() //此时通过fn()可实现外部调用outer()内部的a的变量值
了解了闭包是什么,那让我们看看几个实际案例来看看闭包都有哪些实际用处
通过闭包,我们可以实现了解函数的被调用次数
在不用闭包的情况下,我们要了解函数的被调用次数,此时需要例如下代码
let i=0
function fn(){
i++
console.log(i)
}
fn()
但是,计数所用的i是一个全局变量,意味着在代码量较大时,很容易被错误地修改 此时我们可以通过使用闭包使得计数变量为函数内变量,来实现数据的私有
function fn(){
let i=0
function count(){
i++
console.log(i)
}
return count
}
fn()
fn()
fn() //调用三次
let count_num=fn()
count_num() //输出3
闭包的缺陷:根据js中的垃圾回收机制,我们知道,在一个函数被调用执行完毕之后,为了清理空间,函数内部的变量会被回收,然而如以下代码所示
function fn(){
let i=0
function count(){
i++
console.log(i)
}
return count
}
let count_num=fn()
let count_num=fn()在全局作用域中,所以count_num()在代码执行结束前不会被回收机制回收,而count_num指向count(),count()内又调用了变量i,所以i会保存在闭包中而不被回收销毁,从而导致本应被回收的函数局部变量i不被回收,导致内存泄漏