JS中有一个非常重要但又难以完全掌握的概念,那就是闭包。很多JS程序员自以为已经掌握了闭包,但实质上是一知半解,就像“JS中万物皆为对象”这个常见的错误说法一样,很多前端开发者到现在还固执己见。对于闭包,笔者现在也不敢说是完全掌握,但是希望通过自己对闭包的理解,让更多人对闭包的概念有一个更深刻的认识。

理解闭包的重要性,我想用一本书中的话来描述:“对于那些有一点JavaScript使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生”。

一、概念

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

2、下面一段代码,清晰地展示了闭包:

function foo() {

var a = 2;

function bar() {

console.log( a );

}

return bar;

}

var baz = foo();

baz(); // 2,这就是闭包的效果。

函数bar()的词法作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当作一个值类型进行传递。在这个例子中,我们将bar所引用的函数对象本身当作返回值。

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

bar()显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。

在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用 来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。

而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar()本身在使用。

拜bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以 供bar()在之后任何时间进行引用。

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

因此,在几微秒之后变量baz被实际调用(调用内部函数bar),不出意料它可以访问定义时的词法 作用域,因此它也可以如预期般访问变量a。 这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。

当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。

function foo() {

var a = 2;

function baz() {

console.log( a ); // 2

}

bar( baz );

}

function bar(fn) {

fn(); // 看,这就是闭包!

}

传递函数当然也可以是间接的。

var fn;

function foo() {

var a = 2;

function baz() {

console.log( a );

}

fn = baz; // 将baz分配给全局变量

}

function bar() {

fn(); // 看,这就是闭包!

}

foo();

bar(); // 2

3、深入理解

闭包绝不仅仅是一个好玩的玩具。你已经写过的代码中一定到处都是闭包的身影。现在让我们来 搞懂这个事实。

function wait(message) {

setTimeout( function timer() {

console.log( message ); }, 1000 );

}

wait( "Hello, closure!" );

将一个内部函数(名为timer)传递给setTimeout(..)。timer具有涵盖wait(..)作用域的闭包,因此 还保有对变量message的引用。

wait(..)执行1000毫秒后,它的内部作用域并不会消失,timer函数依然保有wait(..)作用域的闭 包。 深入到引擎的内部原理中,内置的工具函数setTimeout(..)持有对一个参数的引用,这个参数也许 叫作fn或者func,或者其他类似的名字。引擎会调用这个函数,在例子中就是内部的timer函数,而 词法作用域在这个过程中保持完整。 这就是闭包。

或者,如果你很熟悉jQuery(或者其他能说明这个问题的JavaScript框架),可以思考下面的代码:

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 Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

二.闭包的应用(或表现)

只知道闭包是什么还不够,要知道它的使用场景或说表现方式:

1、  要说明闭包,for循环是最常见的例子。

for (var i=1; i<=5; i++) {

(function(j) {

setTimeout(

function timer() {

console.log( j );

}, j*1000 ); })( i );

}

for (var i=1; i<=5; i++) {

let j = i; // 是的,闭包的块作用域!

setTimeout( function timer() {

console.log( j ); }, j*1000 );

}

for循环头部的let声明还会有一个特殊的行 为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使 用上一个迭代结束时的值来初始化这个变量。

块作用域和闭包联手便可天下无敌。

2、  模块

还有其他的代码模式利用闭包的强大威力,但从表面上看,它们似乎与回调无关。下面一起来研究 其中最强大的一个:模块。

function CoolModule() {

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 = CoolModule();

foo.doSomething(); // cool

foo.doAnother(); // 1 ! 2 ! 3

这个模式在JavaScript中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露,这里展 示的是其变体。

模块模式需要具备两个必要条件。

1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。

2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以 访问或者修改私有的状态。

一个具有函数属性的对象本身并不是真正的模块。从方便观察的角度看,一个从函数调用所返回 的,只有数据属性而没有闭包函数的对象并不是真正的模块。

三、总结

闭包就好像从JavaScript中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够 到达那里。但实际上它只是一个标准,显然就是关于如何在函数作为值按需传递的词法环境中书 写代码的。

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

如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循环中。但 同时闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。

模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回值必须至 少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

现在我们会发现代码中到处都有闭包存在,并且我们能够识别闭包然后用它来做一些有用的事!

深入JS——理解闭包可以看作是某种意义上的重生的更多相关文章

  1. js 理解闭包

    学习Javascript闭包(Closure) 引用: 阮一峰 http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures. ...

  2. 我从没理解js的闭包,直到他人向我这么解释。。。

    前段时间根据执行上下文写过一次闭包,但是写的简陋些.昨天在twitter上看到这篇文章,感觉背包的比喻挺恰当的.所以就翻译了. 这篇文章有些啰嗦,但是讲解很细,希望还是耐心看完.也欢迎指出错误. 原地 ...

  3. 带你一分钟理解闭包--js面向对象编程

    上一篇<简单粗暴地理解js原型链--js面向对象编程>没想到能攒到这么多赞,实属意外.分享是个好事情,尤其是分享自己的学习感悟.所以网上关于原型链.闭包.作用域等文章多如牛毛,很多文章写得 ...

  4. 深入理解JS的闭包

    闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域 ...

  5. 关于js中闭包的理解

    1.以前很不理解js中闭包的概念及使用,下面来看一下 function foo() { var a = 123; var b = 456; return function () { return a; ...

  6. js深入理解"闭包"

    一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量 ...

  7. 前端基本知识(三):JS的闭包理解

    JS闭包的理解 一.变量的作用域 二.如何从外部读取局部变量 三.什么是闭包 四.深入理解闭包 五.闭包的用途 六.使用闭包注意情况 七.JavaScript的垃圾回收机制 八.一些思考题 一.变量作 ...

  8. 前端基本知识(三):JS的闭包理解(第一个思考题有错误,已修改)

    JS闭包的理解 一.变量的作用域 二.如何从外部读取局部变量 三.什么是闭包 四.深入理解闭包 五.闭包的用途 六.使用闭包注意情况 七.JavaScript的垃圾回收机制 八.一些思考题 一.变量作 ...

  9. 对js中闭包,作用域,原型的理解

    前几天,和朋友聊天,聊到一些js的基础的时候,有一种‘好像知道,好像又不不知道怎么讲的感觉’...于是捡起书,自己理一理,欢迎拍砖. 闭包 理解闭包首先要理解,js垃圾回收机制,也就是当一个函数被执行 ...

  10. 理解运用JS的闭包、高阶函数、柯里化

    JS的闭包,是一个谈论得比较多的话题了,不过细细想来,有些人还是理不清闭包的概念定义以及相关的特性. 这里就整理一些,做个总结. 一.闭包 1. 闭包的概念 闭包与执行上下文.环境.作用域息息相关 执 ...

随机推荐

  1. 事实胜于雄辩,苹果MacOs能不能玩儿机器/深度(ml/dl)学习(Python3.10/Tensorflow2)

    坊间有传MacOs系统不适合机器(ml)学习和深度(dl)学习,这是板上钉钉的刻板印象,就好像有人说女生不适合编程一样的离谱.现而今,无论是Pytorch框架的MPS模式,还是最新的Tensorflo ...

  2. ros-python学习样例笔记

    1.通信基本原理介绍 待写 2.三种通信方式的程序样例(python版) 2.1 topic 通信方式(非自定义和自定义) 2.1.1 创建工作空间和topic功能包 在ubuntu中打开命令行,输入 ...

  3. MySQL匿名空用户名处理

    问题描述:公司漏扫发现数据库内出现空用户名及密码,需要对这些用户进行整改 1.首先出现了疑问,这些空的用户名是怎么出现的,而且不附带密码. 2.可以手动这样创建这样的用户名和密码形式么. 3.如果能这 ...

  4. 使用CodeArts发布OBS,函数工作流刷新CDN缓存

    摘要:上次通过OBS和CDN部署来Hexo网站,但是每次我们不可能都自己编译然后在上传到OBS,不然太麻烦了,所以我们需要构建流水线,通过PUSH Markdown来发布文章. 本文分享自华为云社区& ...

  5. MySQL(三)数据目录

    目录 Mysql的主要目录结构 1 数据库文件的存放路径 /var/lib/mysql/ 2 相关命令目录 /usr/bin/mysql /usr/sbin/mysql 3 配置文件目录 /usr/s ...

  6. Rust中的Copy和Clone

    1.Copy和Clone Rust中的Copy和Clonetrait都允许创建类型实例的副本.它们都提供了一种复制类型实例的方法,但它们之间存在一些重要的区别.了解这些区别有助更好地使用这两个特征. ...

  7. Caused by: java.net.BindException: Address already in use: JVM_Bind(ActiveMq已经启动)

    1.本地启动项目开启两个启动类出错. Error creating bean with name 'brokerService' defined in class path resource [com ...

  8. ERROR: libfdk_aac not found和ERROR: libmp3lame &gt;= 3.98.3 not dound

    ERROR: libfdk_aac not found和ERROR: libmp3lame >= 3.98.3 not dound 编译ffmepg时出现这两个错误或者有时候需要x264编码的时 ...

  9. Wine运行问题 希沃-汉字卡无法显示

    在wine环境下运行希沃白板5,汉字卡无法正常显示: 这个模块是我之前参与开发的模块,过了5年还有点印象. 大概原理是获取字体库中字符的笔画路径GraphicsPath,根据里面的路径点集PathPo ...

  10. 2020-10-02:golang如何写一个插件?

    福哥答案2020-10-02:#福大大架构师每日一题#简单回答:buildmode=plugin plugin.Openp.Lookup [中级回答:](https://www.zhihu.com/q ...