JavaScript闭包只学这篇就够了
闭包不是魔法
这篇文章使用一些简单的代码例子来解释JavaScript闭包的概念,即使新手也可以轻松参透闭包的含义。
其实只要理解了核心概念,闭包并不是那么的难于理解。但是,网上充斥了太多学术性的文章,对于新手来说,看完这些文章可能会更加一头雾水。
这篇文章面向的是使用主流开发语言的程序员,如果你能读懂下面这段代码,恭喜你,你可以开始JavaScript闭包的学习之旅了。
function sayHello(name) {
var text = 'Hello' + name;
var say = function() {
console.log(text);
}
say();
}
sayHello('Joe');
我相信你一定看懂了,那我们就开始吧!
闭包的一个例子
举例之前,我们先用两句话概括一下:
- 闭包是支持
一类函数
特性的一种方式(如果你还不知道什么是一类函数,请自行百度);它是一个表达式,这个表达式可以在其作用域(当它被初次定义时)内引用变量,或者被赋值给一个变量,或者被当做一个变量传递给某个函数,甚至被当作一个函数的执行结果被返回出去。 - 闭包也可以看作是某个函数被调用时分配的栈帧,而且当这个函数返回结果之后它也不会被回收(就好像它被分配给了堆,而不是栈)
下面的例子返回了对一个方法的引用:
function sayHello2(name){
var text= 'Hello' + name; //局部变量
var say=function(){
console.log(text);
}
return say;
}
var say2=sayHello2('Bob');
say2();//logs='Hello Bob'
我想大多数JavaScript程序员都能理解上面代码中一个函数的引用是如何被赋值给一个变量(say2
)的。如果你不清楚的话,最好在继续了解闭包之前弄清楚。使用C语言的程序员或许会认为这个函数是指向另一个函数的指针,并且变量say
和say2
也同样是指向函数的指针。
然而C语言中指向函数的指针和JavaScript中对一个函数的引用有很大的不同。在JavaScript中,你可以把引用函数的变量当作同时拥有两个指针:一个指向函数,另一个隐形地指向闭包。
上面的代码中生成了一个闭包是因为匿名函数function(){console.log(text);}
被定义在了另外一个函数sayHello2()
中。在JavaScript中,如果你在一个函数中定义了另外一个函数,那么你就创建了一个闭包。
在C语言或者其他流行的开发语言当中,函数返回之后,所有局部变量都不能再被访问,因为栈帧已经被销毁了。
在JavaScript中,如果在一个函数中定义了另外一个函数,即使从被调用的函数中返回,局部变量依然能够被访问到。正如上面例子中我们在得到sayHello()
的返回值之后又调用了say2()
一样。需要注意到,我们调用的代码中引用了函数sayHello2()
中的局部变量text
。
function(){console.log(text);} //say2.toString()的输出结果;
观察say2.toString()
的输出结果,我们会发现代码指向变量text
。这个匿名函数能够引用值为Hello Bob
的变量text
是因为sayHello2()
的局部变量被保留在了闭包中。
在JavaScript中神奇的地方在于引用一个函数的同时会有一个秘密的引用指向在这个函数内部创建的闭包,类似于委托一个方法指针加一个隐藏的对象引用。
更多例子
当你读到很多关于闭包的文章时,总会感觉一头雾水,但是当你看到一些应用的例子时,你就能清晰的理解闭包是如何工作的了。下面是我推荐的一些例子,希望大家能够认真研究直到真正清楚闭包是如何工作的。如果在你没有完全理解的情况下就开始使用闭包,你很快就会成为很多奇怪bug的创造者。
下面这个例子展示了局部变量不是被复制,而是被保留在了引用当中。这是当外部函数存在的情况下将栈帧保存在内存中的方法之一。
function say667(){
//处于闭包中的局部变量
var num=42;
var say=function(){console.log(num);}
num++;
return say;
}
var sayNumber=say667();
sayNumber();//logs 43
下面例子中的三个全局函数有对同一个闭包的共同引用,因为他们都在setupSomeGlobals()
中被定义。
var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
//处于闭包中的局部变量
var num = 42;
// 用全局变量存储对函数的引用
gLogNumber = function() { console.log(num); }
gIncreaseNumber = function() { num++; }
gSetNumber = function(x) { num = x; }
}
setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5
var oldLog = gLogNumber;
setupSomeGlobals();
gLogNumber(); // 42
oldLog() // 5
当这三个函数被创建时,它们能够共享对同一个闭包的访问-即对setupSomeGlobals()
中的局部变量的访问。
需要注意到在上述例子中,如果你再次调用setupSomeGlobals()
,会创建一个新的闭包。gLogNumber()
、gSetNumber()
和gLogNumber()
会被带有新闭包的函数重写(在JavaScript中,当在一个函数中定义另外一个函数时,重新调用外部函数会导致内部函数被重新创建)。
下面这个例子对很多人来说都难以理解,所以你更需要真正理解它。在循环中定义函数时要格外小心:闭包中的局部变量或许不会和你的预想的一样。
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + i;
result.push( function() {console.log(item + ' ' + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1,2,3]);
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList() //logs "item2 undefined" 3次
注意到result.push( function() {console.log(item + ' ' + list[i])}
向result
数组中插入了三次对匿名函数的引用。如果你对匿名函数不太熟悉,可以想象成下面的代码:
pointer=function(){console.log(item+''+list[i])};
result.push(pointer);
需要注意到,当你运行上面的例子时,item2 undefined
被打印了三次!这是因为像前一个例子中提到的,buildList
的局部变量只有一个闭包。当在fnlist[j]()
中调用匿名函数时,它们用的都是同一个闭包,而且在这个闭包中使用了i
和item
的当前值(i
的值为3因为循环已经结束,item
的值为item2
)。因为我们从0开始计数所以item
的值为item2
,而i++
会使i
的值变为3
。
下面这个例子展示了闭包在退出之前包含了外部函数中定义的任何局部变量。注意到变量alice
其实是在匿名函数之后定义的。匿名函数先定义,但是当它被调用时它能够访问alice
,因为alice
和匿名函数处于同一作用域(JavaScript会进行变量提升)。sayAlice()()
只是直接调用了sayAlice()
返回的函数引用-但结果却和之前一样,只不过没有临时变量而已。
function sayAlice() {
var say = function() { console.log(alice); }
var alice = 'Hello Alice';
return say;
}
sayAlice()();// logs "Hello Alice"
注意到变量say
也在闭包中,能够被任何在sayAlice()
中定义的函数访问,或者在内部函数中被递归调用。
最后一个例子展现了每次调用都为局部变量创建一个独立闭包。不是每个函数定义都会有一个闭包,而是每次函数调用产生一个闭包。
function newClosure(someNum, someRef) {
var num = someNum;
var anArray = [1,2,3];
var ref = someRef;
return function(x) {
num += x;
anArray.push(num);
console.log('num: ' + num +
'; anArray: ' + anArray.toString() +
'; ref.someVar: ' + ref.someVar + ';');
}
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
总结
如果你对于闭包的概念依然不清晰,那么最好的方式就是运行一下上面的例子,看看会发生什么。读懂一篇长篇大论要比理解一个例子难的多。我对与闭包和栈帧的解释在技术上并不完全正确-而是为了帮助理解而简化了。如果这些基本点都掌握之后,你就可以朝着更细微之处进发了。
最后总结几点:
- 当你在一个函数中定义另外一个函数时,你就使用了闭包。
- 当你在函数中使用
eval()
时,你就使用了闭包。你在eval
中用到的文字可以指向外部函数的局部变量,而且在eval
中你也可以使用eval('val foo=...')
来创建局部变量。 - 当你在函数中使用
new Function(...)
时,不会创建一个闭包(这个新的函数不能引用外部函数的局部变量)。 - JavaScript中的闭包就好像保存了一份局部变量的备份,他们保持在函数退出时的状态。
- 最好将闭包当作是一个函数的入口创建的,而局部变量是被添加进这个闭包的。
- 当一个带有闭包的函数被调用时,总会保存一组新的局部变量。
- 两个看似代码相同的函数却有不同的行为,是因为
隐藏的
闭包在作怪。我不认为JavaScript代码能够判断出一个函数引用是否有闭包。 - 如果你尝试做任何动态代码的改动(例如:
myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));
),如果myFunction
是个闭包,那就不会起作用(当然,你不会想在运行时里进行源代码的字符串替换,除非...)。 - 在函数中定义多层函数是有可能的,这样你就可以得到多个级别的闭包。
- 我认为在通常情况下,闭包是函数及被捕获的变量的术语,请注意在这篇文章里我没有用到闭包的定义。
JavaScript闭包只学这篇就够了的更多相关文章
- JavaScript闭包,只学这篇就够了
# 闭包不是魔法 这篇文章使用一些简单的代码例子来解释JavaScript闭包的概念,即使新手也可以轻松参透闭包的含义. 其实只要理解了核心概念,闭包并不是那么的难于理解.但是,网上充斥了太多学术性的 ...
- 高效解决「SQLite」数据库并发访问安全问题,只这一篇就够了
Concurrent database access 本文译自:https://dmytrodanylyk.com/articles/concurrent-database/ 对于 Android D ...
- 【javascript闭包】转载一篇不错的解释,也有几个大牛的链接
初学闭包时一直以为很简单.但伴随对一个问题深入学习后,才算真正理解了闭包,同时也发现连<<JavaScript高级程序设计>>中都些不准确的地方. 我不准备从头介绍闭包的概念, ...
- Java多线程超级详解(只看这篇就够了)
多线程能够提升程序性能,也属于高薪必能核心技术栈,本篇会全面详解Java多线程.@mikechen 主要包含如下几点: 基本概念 很多人都对其中的一些概念不够明确,如同步.并发等等,让我们先建立一个数 ...
- 学Redis这篇就够了!
学Redis这篇就够了! 作者:王爷科技 https://www.toutiao.com/i6713520017595433485 Redis 简介 & 优势 Redis 数据类型 发布订 ...
- rodert教你学FFmpeg实战这一篇就够了
rodert教你学FFmpeg实战这一篇就够了 建议收藏,以备查阅 pdf阅读版: 链接:https://pan.baidu.com/s/11kIaq5V6A_pFX3yVoTUvzA 提取码:jav ...
- Pycharm新手教程,只需要看这篇就够了
pycharm是一款高效的python IDE工具,它非常强大,且可以跨平台,是新手首选工具!下面我给第一次使用这款软件的朋友做一个简单的使用教程,希望能给你带来帮助! 目前pycharm一共有两个版 ...
- windows server 2019 域控批量新增不用,只看这一篇就够了,别的不用看
windows server 2019 域控批量新增不用,只看这一篇就够了,别的不用看 1. 新建excel表格 A B C D E 姓 名 全名 登录名 密码 李 四 李四 李四 test123!@ ...
- 一篇文章图文并茂地带你轻松学完 JavaScript 闭包
JavaScript 闭包 为了更好地理解 JavaScript 闭包,笔者将先从 JavaScript 执行上下文以及 JavaScript 作用域开始写起,如果读者对这方面已经了解了,可以直接跳过 ...
随机推荐
- 使用python操作mysql数据库
这是我之前使用mysql时用到的一些库及开发的工具,这里记录下,也方便我查阅. python版本: 2.7.13 mysql版本: 5.5.36 几个python库 1.mysql-connector ...
- Spring Security -SpEL表达式
Spring Security -SpEL表达式 开启SpEL表达式 <!-- use-expressions是否开启 SpEL表达式 o.s.s.web.access.expression.W ...
- Swift实现JSON转Model - HandyJSON使用讲解
背景: 很多时候,我们从服务端请求下的数据都是Json格式,我们需要拿这些数据显示到我们的UI界面. 因此,我们的做法基本都会先将json转为方便使用的数据模型,或者也可以直接转字典解决. 在OC中, ...
- peoplesoft function PSTREENODE 通过 deptid 获得部门树 全路径 名称
create or replace function getUnitFullName(deptid in varchar) return varchar2 is r ); c int; n ); m ...
- Makefile学习总结
Makefile用法分析 在linux开发中,应用程序的编译基本都采用GNU的make工具,而make搭配Makefile来实现工程代码的编译,在越是大型复杂的项目中,make的强悍之处越是明 ...
- Watson Explorer Analytical Components 3 - use case scenarios
The followings are the user case scenarios that WEX can be used for generating value. 1.Customer Ins ...
- Struts 之 通配符 路径匹配 常量用法 配置默认值
Struts 框架学习 Action的开发的几种方式 方式1 : 继承ActionSupport 如果使用Struts校验功能,必须继承此类 方式2 : 实现Action接口 方式3 :不继承 ...
- Java 异常处理笔记
Java程序运行过程中所发生的异常事件可分为两类: §错误(Error):JVM系统内部错误.资源耗尽等严重情况 §违例(Exception): 其它因编程错误或偶然的外在因素导致的一般性问题,例如: ...
- “盛大游戏杯”第15届上海大学程序设计联赛夏季赛暨上海高校金马五校赛题解&&源码【A,水,B,水,C,水,D,快速幂,E,优先队列,F,暴力,G,贪心+排序,H,STL乱搞,I,尼姆博弈,J,差分dp,K,二分+排序,L,矩阵快速幂,M,线段树区间更新+Lazy思想,N,超级快速幂+扩展欧里几德,O,BFS】
黑白图像直方图 发布时间: 2017年7月9日 18:30 最后更新: 2017年7月10日 21:08 时间限制: 1000ms 内存限制: 128M 描述 在一个矩形的灰度图像上,每个 ...
- pc端的企业网站(IT修真院test8)详解1-2
今天接着说test8的页面还原. 头部header和尾部footer 我一开始是想直接使用bootstrap的栅格系统来实现的.但是内容的定位出了不少麻烦. 那么就索性用原生html+css来还原页面 ...