深入理解javascript闭包

闭包在javascript中是一个非常重要但又难以掌握的概念。已经学习和使用javascript一年半之久,还是完全不理解闭包是什么。今天开始认真理一下。

当内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的闭包,内部函数所在的作用域不会被释放,因为闭包需要它们。即:闭包使得函数可以继续访问定义时的词法作用域。(在函数内定义了函数:嵌套函数。并且外部可以调用内部函数。)

在学习js闭包之前,首先需要了解一个概念,词法作用域

一、 词法作用域

作用域分为词法作用域(大多数编程语言所采用的)和动态作用域(Bash脚本,Perl)。

1. 词法阶段

大部分标准语言编译器的第一个工作阶段,叫做词法化(也叫单词化)。词法化的过程会对源代码的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。

2. 词法作用域气泡

简单的说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和快作用域写在哪里决定的,因此词法分析器在处理代码时会保持作用域不变。
在这里插入图片描述

二、作用域闭包

在javascript中,闭包无处不在。闭包是基于词法作用域书写代码时所产生的自然结果。闭包的使用和创建在代码中随处可见,我们缺少的是一双识别闭包的眼睛和一颗想要拥抱闭包的心。你可能会蓦然发现,你的代码中到处都是闭包。

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

下面这段代码清晰的展示了闭包:

示例1:将内部函数作为结果return出来
function foo() {
    let a = 2;

    function bar() {
        console.log(a);
    }
    return bar;
}
let baz = foo();
baz(); // 2 --->闭包产生的效果

在上述代码中我们把foo()这个函数的执行结果,也就是bar这个函数直接赋值给baz, 然后调用baz() ,实际上只是通过不同的标识符引用调用了内部的函数bar()。在这个例子中,bar()可以在自己定义的词法作用域以外的地方执行。
正常情况下,执行完foo(),通常我们认为foo()的整个内部作用域都会被销毁。因为引擎有垃圾回收器,会自动释放不再使用的内存空间。虽然看上去foo()的内容不会再被使用。
但是,在这里,由于闭包,事实上内部作用域依然存在。因为bar()本身还在使用这个内部作用域。

因为bar()声明位置的原因,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()以后在之后的任何时间都可以使用。
示例2:直接将函数传递到所在的词法作用域外
function foo() {
	var a = 2;
	function baz() {
		console.log( a ); // 2
	}
	bar( baz );
}
function bar(fn) {
	fn(); //这就是闭包!在这里baz()已经被传递到了定义时所在的词法作用域以外。
}
示例3:将函数赋值给全局变量,将内部函数传递到所在的词法作用域外。
var fn;
function foo() {
	var a = 2;
	function baz() {
		console.log( a );
	}
	fn = baz; // 将baz 分配给全局变量
}

function bar() {
	fn(); //这就是闭包!
}

foo();
bar(); // 2
综上,无论使用何种方法,只要将内部函数传递到所在的词法作用域外,它都会保持对原始定义作用域的引用,无论在何处执行都会使用这个闭包。
四、循环和闭包
for (var i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, 1000 );
}

上述代码每隔1s输出一个6,一共输出5个6。
我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i 的副本。但是 根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的, 但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。

五、模块和闭包
六、无处不在的闭包

1.for 循环

function wait(message) {
	setTimeout( function timer() {
		console.log( message );
	}, 1000 );
}
wait( "Hello, closure!" );

2.jQuery

function setupBot(name, selector) {
	$( selector ).click( function activator() {
		console.log( "Activating: " + name );           //闭包
	} );
}
setupBot( "Closure Bot 1", "#bot_1" );
setupBot( "Closure Bot 2", "#bot_2" );
本质上,无论何时何地,如果将函数(访问各自的词法作用域)当成一个第一级的值类型,到处传递,那就会有闭包在这些函数中的应用。因为作为值类型时,它拥有和普通变量一样的作用域。
事实上,只要使用了定时器事件监听器Ajax请求跨窗口通信Web Worker或者其他任何的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包。
七、使用闭包的注意事项

a) 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。

b) 闭包会在父函数外部改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

八、总结

闭包在js中其实就是函数的嵌套,由于内层函数的用到外层函数的参数,从而达到调用局部变量的效果,并且其内存空间不会随函数结束而被释放,但也因此会造成内存的压力,使网站运行效率下降。

参考:《你不知道的JavaScript(上卷)》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值