JavaScript 中 闭包 的详解

闭包是怎样

在 JavaScript 中,闭包是一个令人很难弄懂的定义。ECMAScript
中给闭包的概念是:闭包,指的是词法表示包括不被总括的变量的函数,也就是说,函数可以利用函数之外定义的变量。

是不是看完这一个定义感觉越是懵逼了?别急,我们来分析一下。

初稿作者:林鑫,作者博客:https://github.com/lin-xin/blog

  • 闭包是一个函数
  • 闭包能够行使在它外面定义的变量
  • 闭包存在定义该变量的功能域中

看似有些清晰了,可是使用在它外面定义的变量是何许看头,我们先来探望变量功用域。

变量成效域

变量可分为全局变量和一些变量。全局变量的功能域就是全局性的,在 js
的其他地方都得以采用全局变量。在函数中采纳 var
关键字注解变量,这时的变量即是局部变量,它的功效域只在阐明该变量的函数内,在函数外面是造访不到该变量的。

var func = function(){
    var a = 'linxin';
    console.log(a);         // linxin
}
func();
console.log(a);             // Uncaught ReferenceError: a is not defined

成效域相对相比简单,我们不多讲,来看看跟闭包关系相比大的变量生存周期。

变量生存周期

全局变量,生命周期是永远的。局部变量,当定义该变量的函数调用截至时,该变量就会被垃圾回收机制回收而销毁。又一次调用该函数时又会再一次定义了一个新变量。

var func = function(){
    var a = 'linxin';
    console.log(a);
}
func();

a 为部分变量,在 func 调用完之后,a 就会被销毁了。

var func = function(){
    var a = 'linxin';
    var func1 = function(){
        a += ' a';
        console.log(a);
    }
    return func1;
}
var func2 = func();
func2();                    // linxin a
func2();                    // linxin a a
func2();                    // linxin a a a

可以看来,在第一次调用完 func2 之后,func 中的变量 a 变成 ‘linxin
a’,而没有被销毁。因为这时 func1 形成了一个闭包,导致了 a
的生命周期延续了。

这下子闭包就相比较明朗了。

  • 闭包是一个函数,比如上边的 func1 函数
  • 闭包使用其他函数定义的变量,使其不被销毁。比如下面 func1 调用了变量
    a
  • 闭包存在定义该变量的听从域中,变量 a 存在 func 的效果域中,那么
    func1 也终将存在这些效能域中。

今昔得以说,满意这多少个标准化的就是闭包了。

上边我们通过一个大概而又经典的例子来更是熟知闭包。

for (var i = 0; i < 4; i++) {
    setTimeout(function () {
        console.log(i)
    }, 0)
}

咱俩可能会简单的以为控制台会打印出 0 1 2 3,可事实却打印出了 4 4 4
4,这又是为何呢?咱们发现,set提姆eout
函数时异步的,等到函数执行时,for循环已经完结了,此时的 i 的值为 4,所以
function() { console.log(i) } 去找变量 i,只可以得到 4。

俺们回忆上一个事例中,闭包使 a
变量的值被保存起来了,那么这里大家也足以用闭包把 0 1 2 3 保存起来。

for (var i = 0; i < 4; i++) {
    (function (i) {
        setTimeout(function () {
            console.log(i)
        }, 0)
    })(i)
}

当 i=0 时,把 0 作为参数传进匿名函数中,此时 function(i){}
此匿名函数中的 i 的值为 0,等到 set提姆eout 执行时顺着外层去找
i,这时就能得到 0。如此循环往复,就能得到想要的 0 1 2 3。

内存管理

在闭包中调用局部变量,会造成这个片段变量无法顿时被灭绝,相当于全局变量一样会直接占有着内存。假使急需回收那个变量占用的内存,可以手动将变量设置为null。

然则在动用闭包的历程中,相比容易形成 JavaScript 对象和 DOM
对象的循环引用,就有可能导致内存泄露。那是因为浏览器的排泄物回收机制中,假若六个对象期间形成了巡回引用,那么它们都没法儿被回收。

function func() {
    var test = document.getElementById('test');
    test.onclick = function () {
        console.log('hello world');
    }
}

在上头例子中,func 函数中用匿名函数创造了一个闭包。变量 test 是
JavaScript 对象,引用了 id 为 test 的 DOM 对象,DOM 对象的 onclick
属性又引述了闭包,而闭包又足以调用 test
,因此形成了循环引用,导致五个目的都心有余而力不足被回收。要解决那些题目,只需要把循环引用中的变量设为
null 即可。

function func() {
    var test = document.getElementById('test');
    test.onclick = function () {
        console.log('hello world');
    }
    test = null;
}

一经在 func
函数中不利用匿名函数创设闭包,而是通过引用一个表面函数,也不会产出循环引用的题目。

function func() {
    var test = document.getElementById('test');
    test.onclick = funcTest;
}
function funcTest(){
    console.log('hello world');
}

更多随笔:lin-xin/blog