浅析 JavaScript 中的闭包(-------------------------------------------)
一、前言
对于 JavaScript 来说,闭包是一个非常强大的特征。但对于刚开始接触的初学者来说它又似乎是特别高深的。今天我们一起来揭开闭包的神秘面纱。闭包这一块也有很多的文章介绍过了,今天我就浅谈一下自己对闭包的的一些理解,希望能提供一点鄙陋的见解帮助到正在学习的朋友。该文章中能使用口语化的我将尽量使用口语化的叙述方式,希望能让读者更好理解,毕竟文章写出来宗旨就是要让人读懂。文章难免有不足之处还希望帮忙指出。
二、Javascript 的作用域链
在了解闭包之前,我们先来看看几个准备知识。
变量的作用域
首先,什么是作用域?域,区域。简单来理解就是一个变量能被访问的范围(区域)。换言之就是这个变量能起作用的区域。按这个标准来划分我们将变量分为
全局变量
和局部变量
两种以定义的方式来区分有以下特点:
定义在函数内部的变量是局部变量,定义在函数外部的变量是全局变量。(这个并不只是 Javascript 语言的特点)局部变量在函数内部能被访问,在函数外部不能被直接访问,所以局部变量就是从定义它的地方开始到函数结束的位置结束。当然这里有个细节--
变量声明提升
。等下我们用一小段代码提一下变量声明提升是什么。我们先来看看局部变量和全局变量的代码var a = 0; function testFunc(){
var b = 1;
console.log('-------------函数内输出-------------');
console.log(a);//0
console.log(b);//1
} //调用函数
testFunc(); console.log('-------------函数外输出-------------');
console.log(a);//0
console.log(b);//Uncaught ReferenceError: b is not defined执行以上代码结果如下图所示
在代码的最后一行抛出了 b 未定义的异常.也就是说我们在函数外部访问不到在函数内部定义的局部变量。但是第六行代码的正常输出,可见在函数内部我们是可以访问到在函数外部定义的全局变量 a
变量声明提升
相信如果学过 C 语言的话,应该会很熟悉一句话 "先声明后使用"。就是说一个变量或者函数在使用它之前必须是要先找得到这个变量或函数的声明的。例如:
//C 语言正确写法
int a = 0;
printf(a); //错误写法,下面代码没办法通过标准编译(直接报异常)
printf(a);
int a = 0;我们再来看看 Javascript 代码
var a = 0;
console.log(a);//输出结果 0上面这种普通写法我们不探讨,重点看下面的这段代码
console.log(a);//输出结果 undefined
var a = "hello";
console.log(a);//输出结果 hello运行结果如下
上面这个例子就恰好说明了变量声明提升的特点,我们在没有声明变量 a 之前就直接访问变量a 输出结果为 undefined 而并不是直接报异常。所以最直观的感觉是变量的声明被提升到使用之前了。实质上代码如下:
var a;//声明被提升到这里
console.log(a);//输出结果 undefined
a = "hello";
console.log(a);//输出结果 hello小结一下
- 函数内部定义的变量是局部变量,函数外部定义的变量是全局变量。
- 局部变量不能被外界直接访问,全局变量可以在函数内被访问。
- 变量声明提升
嵌套函数的作用域特点
搞清楚上面的小结部分我们缕一缕思路继续探讨另一个话题,javascript 中的嵌套函数,我们先上一段代码:
function A(param){
var vara = 1;
function B(){
var varb = 2;
console.log("----Function B----------")
console.log(vara);//函数B中访问A函数中定义的变量
console.log(param);//A函数中传进来的变量
console.log(varb);//访问自身函数内定义的变量
}
B();
console.log("----Function A----------")
console.log(vara);//访问自身函数内定义的变量
console.log(param);//A函数中传进来的变量
console.log(varb);//访问B函数中定义的变量--异常
}
A("hello");运行结果如下:
由此可见嵌套函数(B)可以继承容器函数(A)的参数和变量,但是嵌套函数(B)中的变量对于他的容器函数来说却是B私有的,也就是说 A 无法访问 B 中定义的变量。换句话说,B 函数形成了一个相对独立的环境(空间)使得它自身的变量只能由它自己来访问,但是 A 函数里的变量 B 也可以访问,这里嵌套函数 B 就形成了一个闭包。有一句话很适合 B 来说 “你的就是我的,我的还是我的”
从语法上看是函数 A 包含着函数 B,但是从作用域上来看是函数 B 的作用域包含着函数 A 的作用域,关系如下图所示:
假设:函数 B 下面又包含了函数 C。此时函数 C 为函数 B 的嵌套函数,函数 B 为函数 C 的容器函数。对于C来说也具有刚刚讲过的 “你的就是我的,我的还是我的” 的特点。以此类推层层嵌套的话就形成了一条链条, 作用域按此规律也形成了 Javascript 中的作用域链。
三、闭包的特点
我们先来总结上面提到的两点
- 嵌套在容器函数(A)内部的嵌套函数(B)只能在容器函数(A)内被访问
- 嵌套函数(B)继承了容器函数(A)的变量,但是 B 函数中的变量只有它自己能访问,也就是嵌套函数(B)的作用域包含容器函数(A)的作用域。
闭包之保存变量
我们还是先上一段代码
function A(a){
function B(b){
return a + b;
}
return B;
}
var C = A(1);
var result = C(2);
console.log(result);//输出结果 3
函数 B 形成了一个闭包,A 函数调用之后返回函数 B 的引用。执行 C 之后发现结果等于3,这也就说明了我们调用 A 的时候 传进去的参数 1 没有被销毁,而是被保存起来了,这就是闭包保存变量的特点。
有保存就有销毁那我们被闭包保存的变量在什么时候销毁?答案是当 B 没有再被引用的时候,就会被销毁.
闭包的注意点--命名冲突
我们还是先上一段代码
function A(){
var num = 6;//外部的名为num 的变量
function B(num){
return num;//当做参数传进来的num 变量,命名冲突发生在这
}
return B;
}
var result = A()(10);
console.log(result);//输出结果10
上述代码的执行结果
通过上面的代码我们能看到有一个容器函数内的名为 num 的变量以及一个嵌套函数内同样名为 num 的变量。这样的执行代码结果以嵌套函数内的变量优先。可能这里说成就近原则更容易记得住。这个就是闭包在实际应用中应该注意的一点。
四、闭包在开发中的应用。
关于闭包在开发中的使用,最多的体现应该还是在 Javascript 插件的开发上面。使用闭包可以避免变量污染。也就是说你在闭包中使用的变量名称不会影响到其他地方同样名称,换个角度来讲,我将我嵌套函数内部的变量给保护起来了,外部没办法随便修改我内部定义的变了。也就是虽然名字一样但是你是你我是我。代码体现如下:
function A(){
function B(num){
var c = 10;//内部变量 c
return num + c;
}
return B;
} var c = 20;//外部变量c
var result = A()(c);
console.log(c);//20
console.log(result)//30
以上特点应用在插件开发中就可以很好的保护了插件本身,避免了外界的串改,保证了插件的稳定。
简单的插件
初步代码
//编写插件代码
var plugin = (function(){
function SayHi(str = '你好啊!'){
console.log(str);
}
return SayHi;
})(); //使用插件
plugin('hello');
plugin();
上面代码闭包部分我就不在累述了,我们来看看新出现的一种语法--自调用匿名函数:
(function{
//code
})();
实际作用是创建了一个匿名函数,并在创建后立即执行一次。作用等价于下面的代码,唯一的区别就是下面的函数不是匿名的。
//创建
var func = function(){
//code
}
//调用
func();
当然,我们编写插件不可能只提供一个API给外部使用,如何返回多个API,我们这里使用字面量形式返回。改进之后的代码如下
//编写插件代码
var plugin = (function(){
var _sayhi = function(str = '你好啊!'){
console.log(str);
}
var _sayhello = function(){
console.log("这个API能做很牛逼的事情");
}
return {
SayHi : _sayhi,
SayHello : _sayhello
}
})(); //通过插件提供的API使用插件
plugin.SayHi('hello');
plugin.SayHello();
执行结果
五、后语
今天对于闭包的看法暂时先写到这了,秉承着学以致用的原则,下两篇文章我将介绍 javascript 插件的几种开发形式,以及实践--开发一个原生的 Javascript 插件。
本文为作者原创,转载请注明出处!
前提
首先,我们需要知道 JavaScript 里面的函数会创建内部词法作用域,是的,JavaScript 是词法作用域,也就是说作用域与作用域的层级关系在你书写的时候就已经确定了,而不是调用的时候,调用的时候确定的称为动态作用域,由于不是本篇文章的重点,就不再详细解释了,举两个例子自己领悟:
1
2
3
4
5
6
7
8
9
10
|
var name = 'fruit'
function apple () {
console.log(name)
}
function orange () {
var name = 'orange'
apple()
}
orange() // fruit
|
由于 JavaScript 是词法作用域,所以 apple
函数的局部作用域的上层作用域是全局作用域,从书写的位置就看出来了。假设 JavaScript 是动态作用域,就要看函数的调用顺序了,由于 apple
是在 orange
中调用的,所以 apple
的上层作用域是 orange
的局部作用域,那样的话会输出 orange!
这样的话,就制定了一套作用域访问的规则,这也是会有闭包的原因之一!
什么是闭包?
函数记住并访问其所在的词法作用域,叫做闭包现象,而此时函数对作用域的引用叫做闭包。
当我看到这句话的时候,泪流满面,国外的作者就是一语道破真相。简单的说,闭包就是引用,对谁的引用呢,对作用域的引用,只不过这种引用是有条件的——首先要记住作用域,然后再访问作用域!
什么叫记住作用域?
首先,我们都知道,在 JavaScript 里面,如果函数被调用过了,并且以后不会被用到,那么垃圾回收机制就会销毁由函数创建的作用域,我们还知道,对象(函数也是对象)的传递属于传引用,也就是类似于C语言里面的指针,并不会把真正的值拷贝给变量,而是把对象所在的位置传递给变量,所以,当函数被传引用到一个还未销毁的作用域的某个变量,由于变量存在,所以函数得存在,又因为函数的存在依赖于函数所在的词法作用域,所以函数所在的词法作用域也得存在,这样一来,就记住了该词法作用域。也就解释了该节的标题!下面举个例子说明一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 没有闭包现象的时候
function apple () {
var count = 0
function output () {
console.log(count)
}
fruit(output)
}
function fruit (arg) {
console.log('fruit')
}
apple() // fruit
|
当我们在调用 apple
的时候,本来 apple
在执行完毕之后 apple
的局部作用域就应该被销毁,但是由于 fruit(output)
将 output
传引用给了 arg
,所以在 fruit
执行的这段时间内,arg
肯定是存在的,被引用的函数 output
也得存在,而 output
依赖的 apple
函数产生的局部作用域也得存在,这就是所谓的“记住”,把作用域给记住了!
但是,上面的例子是闭包现象吗?不是,因为函数 output
内部并没有访问记住的词法作用域的变量!在执行 fruit(output)
的过程中,只发生了 arg = output
的传引用赋值,而这个过程,只是把二者关联起来了,并没有去取 arg
引用的对象的值,所以实际上也并没有访问 output
所在的词法作用域!
记住并访问
上面的代码,稍微修改一下就会产生闭包现象了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
function apple () {
var count = 0
function output () {
console.log(count)
}
fruit(output)
}
function fruit (arg) {
arg()
}
apple() // 0
|
现在,调用 fruit
时,apple
的局部作用域处于“记住”的状态,这时候, fruit
内部调用了 arg()
,因为传引用,实际上访问并执行了 apple
局部作用域的 output
,不仅仅是这样,output
内部还访问了 count
变量,这两次对 apple
局部作用域的引用都是闭包!
所以,之所以说所有回调函数的调用都会产生闭包现象,也是因为这个回调函数被传给了另外一个函数的参数,所以在另外一个函数的作用域消失之前,回调函数所在的词法作用域都被记住了,由于回调函数一定会被执行,所以回调函数所在的词法作用域至少被访问了一次,也就是至少访问回调函数本身,而这个对作用域的引用就是闭包。
闭包的作用
根据上面的讲解,估计你自己都能倒背如流了:
- 记住了函数所在的词法作用域,使其不被销毁;
- 能够访问函数所在词法作用域的变量;
- 创建模块(设计私有变量、公有函数等等)
还有很多,就不一一说了,下面就是利用闭包来解决一个常见的问题:
1
2
3
4
5
6
|
for (var i = 0; i < 5; i++) {
// 为了方便说明,给函数起名叫 apple
setTimeout(function apple () {
console.log(i) // 5 个 5
}, 0)
}
|
首先读者们先思考一个问题,这会产生闭包吗?
其实,上面也也会产生闭包,只不过 apple
记住并访问的是全局作用域,为什么呢?因为回调函数被当做 setTimeout
的参数传引用过去了,假设 setTimeout
实现如下
1
2
3
4
|
var setTimeout = function (callback, delay) {
// 延迟
callback()
}
|
看到没,因为 setTimeout
属于异步函数,所以会等到 JS 执行完毕之后再调用 callback
,所以这段时间 callback
一直存在,所以函数 apple
也一直存在,所以全局作用域并不会等 JavaScript 执行完毕后就销毁(函数 apple
属于全局作用域的),这时候循环早结束了,所以 i
也变成了 5,于是乎,这个时候 apple
对全局作用域的引用称为闭包!
上面也说了回调函数调用都会产生闭包,这里就当举例说明一下!
那么怎么解决以上问题呢,很简单,让回调函数记住不同的作用域就行了!
1
2
3
4
5
6
7
8
|
for (var i = 0; i < 5; i++) {
// 为了方便说明,给函数起名叫 apple
(function baz (i) {
setTimeout(function apple () {
console.log(i)
}, 0)
})(i) // 0 1 2 3 4
}
|
上面用立即执行函数解决了问题,因为函数有局部作用域,所以调用 5 次函数会产生 5 个局部作用域,每个作用域的 i
由各次循环的 i
传递赋值,而每个作用域内都存在 apple
,都记住了各自的作用域,也就取到了不同的 i
!
不过通常来说,闭包都是按以下方式产生:
1
2
3
4
5
6
7
8
9
10
11
12
|
function apple () {
var name = 'apple'
var output = function () {
console.log(name)
}
return output
}
var out = apple()
out() // apple
|
上述将函数传引用给了全局作用域的变量,显然,闭包(对 apple
作用域的引用)在全局作用域都存在的情况下都可能发生,而且后面也执行了 out()
!
更常见的写法是下面这种:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
function Apple () {
var name = 'apple'
var output = function () {
console.log(name)
}
var setName = function (arg) {
name = arg
}
return {
output: output,
setName: setName
}
}
var apple = Apple()
apple.output() // apple
apple.setName('Apple')
apple.output() // Apple
|
这就是模块的一个例子,name
通常被称为私有变量!
结语
闭包没什么了不起的,这是被人玩的过于玄乎,其实这是人们很自然的想法:我在别的地方调用函数,总得保持函数正常运行吧!“闭包”这种机制很轻松的帮你解决了这个问题,我们不必搞懂闭包是什么也经常在实现它(如果这句话写在前面,会不会很多人都不看了,哈哈),这是语言设计者的过人之处,但是,你不搞懂它,总被人质疑:你不懂闭包吧!实际上,我们都实现了很多次闭包,所以,你把内部机制详细搞清楚了,就不会再害怕别人的质疑了,哈哈!当然,如果你喜欢钻研,更有必要了解其中的机制了,体会到寻找语言设计者设计思路的快感!
最后,再总结一下:函数记住并访问其所在的词法作用域,叫做闭包现象,而此时函数对作用域的引用叫做闭包。
最后的最后,再强调一下:闭包就是引用!
浅析 JavaScript 中的闭包(-------------------------------------------)的更多相关文章
- 浅析 JavaScript 中的闭包(Closures)
a { text-decoration: none; color: #4094c7 } h4,h5 { margin: 0; font-weight: 700; color: inherit; lin ...
- 浅析 JavaScript 中的 函数 currying 柯里化
原文:浅析 JavaScript 中的 函数 currying 柯里化 何为Curry化/柯里化? curry化来源与数学家 Haskell Curry的名字 (编程语言 Haskell也是以他的名字 ...
- 浅析 JavaScript 中的 函数 uncurrying 反柯里化
柯里化 柯里化又称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果. 因此柯里化的过程是 ...
- 让你分分钟学会Javascript中的闭包
Javascript中的闭包 前面的话: 闭包,是 javascript 中重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,你很难从定义去理解它 ...
- 难道这就是JavaScript中的"闭包"
其实对于JavaScript中的"闭包"还没真正理解,这次在实际Coding中似乎遇到了"闭包"的问题,仅此摘录,以待深究. 表现为jQuery的post方法回 ...
- 浅谈JavaScript中的闭包
浅谈JavaScript中的闭包 在JavaScript中,闭包是指这样一个函数:它有权访问另一个函数作用域中的变量. 创建一个闭包的常用的方式:在一个函数内部创建另一个函数. 比如: functio ...
- javascript中的闭包解析
学习javaScript已经有一段时间了,在这段时间里,已经感受到了JavaScript的种种魅力,这是一门神奇的语言,同时也是一门正在逐步完善的语言,相信在大家的逐步修改中,这门语言会逐步的完善下去 ...
- JavaScript中的闭包理解
原创文章,转载请注明:JavaScript中的闭包理解 By Lucio.Yang 1.JavaScript闭包 在小学期开发项目的时候,用node.js开发了服务器,过程中遇到了node.js的第 ...
- 【JS】JavaScript中的闭包
在JavaScript中,闭包指的是有权访问另一个函数作用域中的变量的函数:创建闭包最常见的方式就是在一个函数内创建另一个函数.如下例子: function A(propertyName){ retu ...
随机推荐
- LOJ 101 最大流(ISAP 模板)
开long long的最大流 #include<bits/stdc++.h> using namespace std; ;//点数的最大值 ;//边数的最大值 ; struct Edge ...
- 牛客网暑期ACM多校训练营(第七场)A Minimum Cost Perfect Matching(找规律)
题意: 给定n, 求一个0~n-1的全排列p, 使得的和最小 分析: 打表发现最优解肯定是和为0的, 然后如果为2的幂就是直接反转即可, 不然的话就要分开从前面到后面逐步拆分, 具体思想模拟一下n = ...
- 水题:51Nod1095-Anigram单词
1095 Anigram单词 基准时间限制:1 秒 空间限制:131072 KB 分值: 10 难度:2级算法题 Description 一个单词a如果通过交换单词中字母的顺序可以得到另外的单词b,那 ...
- HDU 3435 KM A new Graph Game
和HDU 3488一样的,只不过要判断一下是否有解. #include <iostream> #include <cstdio> #include <cstring> ...
- [转] 对 forEach(),map(),filter(),reduce(),find(),every(),some()的理解
1.forEach() 用法:array.forEach(function(item,index){}) 没有返回值,只是单纯的遍历 2.map() 用法:array.map(function(ite ...
- dubbo rpc filter实现剖析(二)
2.6.3版本,之前读的是2.4.9版本 本篇主要阐述dubbo rpc的filter的实现,包括作用,用法,原理,与Spring Cloud在这些能力的对比. 整个filter列表的获取过程在 co ...
- Python文件处理、函数的基本应用
可读可写: r+t:可读.可写 w+t:可写.可读with open('b.txt','w+t',encoding='utf-8') as f: print(f.readable()) p ...
- 51nod1832 先序遍历与后序遍历
对于给定的一个二叉树的先序遍历和后序遍历,输出有多少种满足条件的二叉树.两棵二叉树不同当且仅当对于某个x,x的左儿子编号不同或x的右儿子编号不同. Input 第一行一个正整数n(3<=n< ...
- Concept with HTTP API && RPC
RPC=Remote Produce Call 是一种技术的概念名词. HTTP是一种协议,RPC可以通过HTTP来实现,也可以通过Socket自己实现一套协议来实现.所以楼主可以换一个问法,为何RP ...
- 马士兵hadoop第三课:java开发hdfs(转)
马士兵hadoop第一课:虚拟机搭建和安装hadoop及启动 马士兵hadoop第二课:hdfs集群集中管理和hadoop文件操作 马士兵hadoop第三课:java开发hdfs 马士兵hadoop第 ...