浅析javascript中的闭包

作用域闭包

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

词法作用域:定义在词法阶段的作用域,在写代码时将变量和块作用域写在哪里来决定。(还有动态作用域)

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

上面的例子中bar()被封闭在了foo()内部。函数bar()具有一个涵盖foo()作用域的闭包(事实上它涵盖了它能访问的所有作用域,例如全局作用域)。

再看一个例子如下:

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

函数bar()的词法作用域能够访问foo()内部作用域。函数bar()本身被当做一个值类型进行传递。

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

bar()可以被正常执行,但是它是在自己定义的词法作用域之外的地方被执行的。

在foo()执行之后,通常会期待foo()的整个内部作用域被销毁,但是闭包可以阻止这个。

在上面的例子中,foo()的内部作用域依然存在,所以没有被回收,谁在使用?是bar()本身在使用。

由于bar()函数在foo()函数内部声明,它能够使得该作用域一直存活。bar()函数一直持有对于该作用域的引用,这个引用就叫做闭包。

变量baz被实际调用(就是内部函数bar()),它可以访问在它定义时的词法作用域,所以它可以访问变量a。

这个函数在定义时的词法作用域之外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。

看另一个例子:

function foo(){
    var a = 2;
    function baz(){
        console.log(a);
    }
    bar(baz);
}
function bar(fn){
    fn();//这就是一个闭包
}

把内部函数baz传递给bar(),当调用这个内部函数(fn)时,可以访问当foo()的内部作用域,就可以访问到a。

无论通过什么手段将内部的函数传递到所在的词法作用域之外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

闭包的部分应用分析

function wait(message){
    setTimeout(function timer(){
        console.log(message);
    },1000);
}
wait('hello');

在上面的例子中,名为timer的内部函数作为传参传递给了setTimeout(..),timer函数持有wait函数内部作用域的闭包,它可以对message变量进行引用。

wait函数在执行1000ms后,它的内部作用域并不会消失,timer函数依然持有wait函数内部作用域的闭包。

setTimeout函数持有对一个参数的引用,引擎会调用这个参数,该例子中就是timer函数,而词法作用域在这个过程中会保持完整。

另一个例子:

function setupBot(name,selector){
    $(selector).click(function activator(){
        console.log(name);
    })
} 
setupBot('Bot1','#bot_1');
setupBot('Bot2','#bot_2');

这个例子是在jQuery中的应用。这实际上也是闭包。

在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调,就相当于使用了闭包。

IIFE是不是闭包?

//to do …

循环和闭包

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

每秒一次的频率输出五次6。

延迟函数的回调会在循环结束的时候才执行。循环结束的条件时I=6,执行回调的时候已经是i的值为6了,所有会输出五次6。

我们对于这段代码的预期应该是输出1~5,但为什么会出现这样的结果?

因为在预期中,我们试图假设循环中的每个迭代在运行的时候都会给自己获取一个i的副本。但是,根据作用域的工作原理,实际情况是尽管循环中的五个函数是分别在各自的迭代中定义的,但是他们都被封闭在一个共享的全局作用于下,实际上只有一个i。因此这五个函数共用同一个i的引用。

如何实现预期的效果?

我们需要给循环中的每一个迭代增加一个闭包作用域,让他们各自互不影响。

尝试一下IIFE:

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

这样结果还是输出五次6。

为什么不行?

因为我们新增的这个立即执行函数它的作用域是空的。我们在获取对i的引用时,还是会到外部的作用域取值。

进行如下的修改:

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

这样输出结果就是预期的1,2,3,4,5。

还有一种写法和上面的结果一样:

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

不定义变量j,直接将j作为立即执行函数的传参,而此时我们将i作为参数传进去,和上面的代码结果一致。

此时IIFE会为循环的每一个迭代都创建一个新的作用域,将延迟函数的回调封闭在每一个迭代中。

模块

function CoolModules(){
    var something = "cool";
    var another = [1,2,3];
    function doSomething(){
        console.log(something);
    }
    function doAnother(){
        console.log(another.join('!'));
    }
    return {
        doSomething:doSomething,
        doAnother:doAnother
    }
}
var foo = CoolModules();
foo.doSomething();//cool
foo.doAnother();//1!2!3

//to do …

两个特征:

  1. 为创建内部作用域而调用了一个包装函数
  2. 包装函数的返回值必须至少包括一个对内部函数的引用
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值