js:闭包

73 阅读4分钟

当函数可以记住并访问所在的词法作用域,就产生了闭包。即函数是在当前词法作用域之外执行的。

什么是闭包?

在简单点说就是函数内部定义的函数,被返回了出去并在外部调用使用了。

开门先举个栗子:

function foo(){
var a=2
function bar (){
console.log(a)
}
return bar 
}
var baz=foo()
baz()     //2

函数bar 的词法作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当做一个值进行传递。

在foo()执行之后,返回的内部bar()赋值给变量baz并调用baz(),实际上只是通过了不同的标识符引用调用了内部函数bar()。

bar()在上面的例子中是在自己定义的词法作用域之外的地放执行的。

在foo()执行后,通常会期待foo()的整个作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。但由于bar()声明的位置,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后的引用。

bar()依然持有对该作用域的引用,而这个引用就叫作闭包。

再举个栗子:

for (var i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i)
},i*1000)
}

预期的结果为每隔一秒依次打印1,2,3,4,5。但输出结果是每秒一次的频率输出五次6。

6是哪里来的?

这个循环的终止条件是i不再<=5。条件首次成立时i的值6。因为js是单线程工作的,延迟函数的回调会在循环结束时才执行,因此输出显示的是循环结束时i的最终值。 循环中的五个函数是在各个迭代中分别定义的,但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。

那应该怎么更改这段代码实现预期的效果。

for (var i=1;i<=5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
},j*1000)
})(i)
}
// 1,2,3,4,5

可以使用使用立即执行函数将i传递进去,使延迟函数的回调可以将新的作用域封闭在每个迭代的内部,每次循环都会具有正确值的变量供我们访问。

立即执行函数在每次迭代时都创建了一个新的作用域,所以每次迭代我们都需要一个块作用域。

所以还可以使用let声明变量,来实现代码的预期效果。

for (let i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i)
},i*1000)
}

应用场景:

单例模式

单例模式是一种常见的涉及模式,它保证了一个类只有一个实例。实现方法一般是先判断实例是否存在,如果存在就直接返回,否则就创建了在返回。

function  a(){
this.data='yao'
}
a.getContent=(function(){
var b
return function(){
if(b){
return b
}else{
b= new a()
return b
}
}
})()

var fa=a.getContent()
var fb=a.getContent()
console.log(fa===fb)
console.log(fa.data)

模拟私有属性

JavaScript中的方法和属性均可以访问这样会造成安全隐患,内部的属性任何开发者都可以随意修改。虽然JavaScript语言层面不支持私有属性的创建,但是我们可以用闭包的手段模拟出私有属性。

// 模拟私有属性
    function getFunc() {
        var name = 'yao'
        var age = 22
        return function () {
            return {
                getName: function () {
                    return name
                },
                getAge: function () {
                    return age
                }
            }
        }
    }
    var obj = getFunc()()
    console.log(obj.name, obj.getAge(), obj.getName())

闭包的问题:

闭包的过渡使用会导致内存的占用无法释放的,造成内存泄漏。

举个栗子:

function foo(){
var a=2
function bar (){
console.log(a)
}
return bar 
}
var baz =foo()
baz()

JavaScript内部的垃圾回收机制用的是引用计数收集:当内存中的一个变量被引用一次,计数就加一。垃圾回收机制会以固定的时间轮询这些变量,将计数为0的变量标记为失效变量并清除释放内存。

上面的代码中,理论上来说,foo函数作用域隔绝了外部环境,所有的变量引用都是在函数内部完成的。foo运行完成以后,内部的变量就该被销毁,内存被回收。然而闭包导致了全局作用域始终存在一个baz的变量,在引用这foo内部的bar函数,这就意味着foo内部定义的bar函数的引用数始终为1,垃圾运行机制就无法把它销毁,更糟糕的是,bar有可能还要使用到父作用域foo中的变量信息,那它们自然也不能被销毁。

JS 引擎无法判断你什么时候还会调用闭包函数,只能一直让这些数据占用着内存。