对JS中的闭包还没有了解过的可看 MDN 闭包, 在面试的时候闭包也是经常被提及的Topic
一、闭包的概念
简单的说,闭包就是有权访问其他函数作用域内部变量的函数
闭包 = 函数 + 函数能够访问的自由变量
闭包的三个特点:
- 函数嵌套函数
- 内部的函数可以引用外部函数的参数或变量
- 参数和变量不会被垃圾回收机制回收,因为内部还在引用
引用其他博主对于闭包的解释,很精辟:
通俗地讲就是别人家有某个东西,你想拿到但是因为权限不够(不打死你才怪),但是你可以跟家里的孩子套近乎,通过他拿到!
这个家就是局部作用域,外部无法访问内部变量,孩子是返回对象,对家里的东西有访问权限,借助返回对象间接访问内部变量
子函数在外被调用,子函数所在的父函数的作用域不会被释放
闭包的作用域链包含着它自己的作用域,以及父函数的作用域和全局作用域。
function func(){
var a = 1, b = 2
function sum(){
return a + b
}
return sum
}
二、闭包的优势
我们为什么要使用闭包呢?因为闭包有如下好处:
- 变量可以长期驻扎在内存之中,实现结果的缓存
(闭包不会释放外部的引用,所以内部的值得以保留,比如可以利用这个实现累加器) - 避免全局变量的污染,有私有成员变量
(全局变量:可重用,但是易被污染;私有变量:不可重用,仅函数内使用,不会被污染,而使用闭包既可以实现重用变量,还不会造成其被污染) - 匿名执行函数
(有些函数只需要执行一次,内部变量无需维护,可以使用闭包)
三、闭包的特性
1. 闭包的应用:
- 模块化的代码定义,模仿块级作用域
- 在循环中找到索引
- 设计私有的方法和变量,实现封装
- 保存外部函数的变量
2. 使用闭包的步骤:
- 用外层函数包裹住要保护的变量和内层函数
- 外层函数将内层函数返回到外部
- 掉用外层函数,获取内层函数的对象,保存在外部的变量中——形成了闭包
function module(){
var arr = []
function add(val){
if(typeof(val) === 'Number'){
arr.push(val)
}
}
function get(index){
if(index < arr.length){
return arr[index]
}else{
return null
}
}
return{
add: add,
get: get
}
}
var m = module()
m.add(1)
m.add(2)
m.add('xxx')
console.log(m.get(2))
简单来说,创建闭包就是在一个函数内部创建另一个函数
3. 闭包的变量特点
在javascript中,如果一个对象不再被引用,那么这个对象就会被垃圾回收机制回收;
如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。
闭包只会取得包含函数中任何变量的最后一个值,这是因为闭包所保存的是整个变量对象,而不是某个特殊的变量
function test(){
var arr = []
for(var i = 0; i < 10; i++){
arr[i] = function(){
return i
}
}
for(var j = 0; j < 10; j++){
console.log(arr[j]()) // 输出的是变量最后一个值
}
}
test() // 连续打印10个10
若是想输出0,1,2 … 9,可将上面的 var i
改为 let i
4. 闭包中的this对象
匿名函数的执行环境具有全局性,因此其this对象通常指向window
var name = "The window"
var obj = {
name: "My Object"
getName: function(){
return function(){
return this.name
}
}
}
console.log(obj.getName()) // The window
Tips: 函数与函数的功能(函数值)是分割开的,并不是函数在哪里,内部的this就指向哪里
5. call & apply
call
和apply
都可以改变this的指向,但是调用方式有所不同,call
传递参数,apply
以数组的形式传递与arguments是一对好基友,建议使用
四、闭包的缺点
闭包在带来优势的同时,也毫不例外的会有一些问题
1. 常驻内存,更多的内存使用量
Solution: 在闭包不使用时,要及时释放,将引用内层函数对象的变量赋值为null
// 用外层函数包裹要保护的变量和内存函数
function outer() {
var i = 1
// 外层函数返回内层函数到外部
return function(){
console.log(i++)
}
// 调用外层函数获得内层函数对象
var getNum = outer()
getNum() // 1
getNum() // 2
i = 1
getNum() // 3
getNum() // 4
}
//
通常,函数的作用域及其所有的变量都会在函数执行结束之后被销毁。但是,在创建了一个闭包只有,这个函数的作用域就会一直保存到闭包不存在为止。
释放闭包的引用:
function makeAdder(x){
return function(y){
return x+y
}
}
var addFive = makeAdder(5)
var addTen = makeAdder(10)
console.log(addFive(2)) // 7
console.log(addTen(2)) // 12
//释放对闭包的引用
addFive = null
addTen = null
//addFive 和 addTen 都是闭包。它们共享相同的函数定义,但是保存了不同的环境。
2. 在IE下使用不当会引发内存泄露
当一个变量为DOM节点获取或宿主对象,他的一个属性引用了一个内部函数,而内部函数又在引用外部变量,会造成互相引用,从而引起内存泄漏。
闭包会引用包含函数的整个变量对象,如果闭包的作用域链中保存着一个
HTML
元素,那么就以为着该元素无法被销毁,所以我们有必要在对这个元素操作完之后进行主动销毁
function assignHandler(){
var element = document.getElementById('someElement')
var id = element.id
element.onclick = function(){
alert(id)
}
element = null
}
总之,如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。
附:关于闭包的常见的面试题 参考
function func(n, o){
console.log(o)
return {
func: function(m){
return func(m,n)
}
}
}
var a = func(0) // ?
a.func(1) // ?
a.func(2) // ?
a.func(3) // ?
var b = func(0).func(1).func(2).func(3) // ?
var c = func(0).func(1) // ?
c.func(2) // ?
c.func(3) // ?
结果:
undefined
0
0
0
undefined,0,1,2
undefined,0
1
1