JavaScript闭包其一:闭包概论 函数式编程中一些基本定义
http://www.nowamagic.net/librarys/veda/detail/1707前面介绍了作用域链和变量对象,现在再讲闭包就容易理解了。闭包其实大家都已经谈烂了。尽管如此,这里还是要试着从理论角度来讨论下闭包,看看ECMAScript中的闭包内部究竟是如何工作的。
在直接讨论ECMAScript闭包之前,还是有必要来看一下函数式编程中一些基本定义。
众所周知,在函数式语言中(ECMAScript也支持这种风格),函数即是数据。就比方说,函数可以赋值给变量,可以当参数传递给其他函数,还可以从函数里返回等等。这类函数有特殊的名字和结构。
定义
A functional argument (“Funarg”) — is an argument which value is a function.
函数式参数(“Funarg”) —— 是指值为函数的参数。
例子:
function exampleFunc(funArg) {
funArg();
} exampleFunc(function () {
alert('funArg');
});
上述例子中funarg的实际参数其实是传递给exampleFunc的匿名函数。
反过来,接受函数式参数的函数称为高阶函数(high-order function 简称:HOF)。还可以称作:函数式函数或者偏数理或操作符。上述例子中,exampleFunc 就是这样的函数。
此前提到的,函数不仅可以作为参数,还可以作为返回值。这类以函数为返回值的函数称为带函数值的函数(functions with functional value or function valued functions)。
(function functionValued() {
return function () {
alert('returned function is called');
};
})()();
可以以正常数据形式存在的函数(比方说:当参数传递,接受函数式参数或者以函数值返回)都称作 第一类函数(一般说第一类对象)。在ECMAScript中,所有的函数都是第一类对象。
函数可以作为正常数据存在(例如:当参数传递,接受函数式参数或者以函数值返回)都称作第一类函数(一般说第一类对象)。
在ECMAScript中,所有的函数都是第一类对象。
接受自己作为参数的函数,称为自应用函数(auto-applicative function 或者 self-applicative function):
(function selfApplicative(funArg) { if (funArg && funArg === selfApplicative) {
alert('self-applicative');
return;
} selfApplicative(selfApplicative); })();
以自己为返回值的函数称为自复制函数(auto-replicative function 或者 self-replicative function)。通常,“自复制”这个词用在文学作品中:
(function selfReplicative() {
return selfReplicative;
})();
自复制函数的其中一个比较有意思的模式是让仅接受集合的一个项作为参数来接受从而代替接受集合本身。
// 接受集合的函数
function registerModes(modes) {
modes.forEach(registerMode, modes);
} // 用法
registerModes(['roster', 'accounts', 'groups']); // 自复制函数的声明
function modes(mode) {
registerMode(mode); // 注册一个mode
return modes; // 返回函数自身
} // 用法,modes链式调用
modes('roster')('accounts')('groups') //有点类似:jQueryObject.addClass("a").toggle().removClass("b")
但直接传集合用起来相对来说,比较有效并且直观。
在函数式参数中定义的变量,在“funarg”激活时就能够访问了(因为存储上下文数据的变量对象每次在进入上下文的时候就创建出来了):
function testFn(funArg) {
// funarg激活时, 局部变量localVar可以访问了
funArg(10); // 20
funArg(20); // 30 } testFn(function (arg) {
var localVar = 10;
alert(arg + localVar);
});
然而,在ECMAScript中,函数是可以封装在父函数中的,并可以使用父函数上下文的变量。这个特性会引发funarg问题。
Funarg问题
在面向堆栈的编程语言中,函数的局部变量都是保存在栈上的,每当函数激活的时候,这些变量和函数参数都会压入到该堆栈上。
当函数返回的时候,这些参数又会从栈中移除。这种模型对将函数作为函数式值使用的时候有很大的限制(比方说,作为返回值从父函数中返回)。绝大部分情况下,问题会出现在当函数有自由变量的时候。
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量
例子:
function testFn() { var localVar = 10; function innerFn(innerParam) {
alert(innerParam + localVar);
} return innerFn;
} var someFn = testFn();
someFn(20); // 30
上述例子中,对于innerFn函数来说,localVar就属于自由变量。
对于采用面向栈模型来存储局部变量的系统而言,就意味着当testFn函数调用结束后,其局部变量都会从堆栈中移除。这样一来,当从外部对innerFn进行函数调用的时候,就会发生错误(因为localVar变量已经不存在了)。
而且,上述例子在面向栈实现模型中,要想将innerFn以返回值返回根本是不可能的。因为它也是testFn函数的局部变量,也会随着testFn的返回而移除。
还有一个问题是当系统采用动态作用域,函数作为函数参数使用的时候有关。
看如下例子(伪代码):
var z = 10; function foo() {
alert(z);
} foo(); // 10 – 使用静态和动态作用域的时候 (function () { var z = 20;
foo(); // 10 – 使用静态作用域, 20 – 使用动态作用域 })(); // 将foo作为参数的时候是一样的
(function (funArg) { var z = 30;
funArg(); // 10 – 静态作用域, 30 – 动态作用域 })(foo);
我们看到,采用动态作用域,变量(标识符)的系统是通过变量动态栈来管理的。因此,自由变量是在当前活跃的动态链中查询的,而不是在函数创建的时候保存起来的静态作用域链中查询的。
这样就会产生冲突。比方说,即使Z仍然存在(与之前从栈中移除变量的例子相反),还是会有这样一个问题: 在不同的函数调用中,Z的值到底取哪个呢(从哪个上下文,哪个作用域中查询)?
上述描述的就是两类funarg问题 —— 取决于是否将函数以返回值返回(第一类问题)以及是否将函数当函数参数使用(第二类问题)。
为了解决上述问题,就引入了 闭包的概念。
闭包
闭包是代码块和创建该代码块的上下文中数据的结合。
让我们来看下面这个例子(伪代码):
var x = 20; function foo() {
alert(x); // 自由变量"x" == 20
} // 为foo闭包
fooClosure = {
call: foo // 引用到function
lexicalEnvironment: {x: 20} // 搜索上下文的上下文
};
上述例子中,“fooClosure”部分是伪代码。对应的,在ECMAScript中,“foo”函数已经有了一个内部属性——创建该函数上下文的作用域链。
“lexical”通常是省略的。上述例子中是为了强调在闭包创建的同时,上下文的数据就会保存起来。当下次调用该函数的时候,自由变量就可以在保存的(闭包)上下文中找到了,正如上述代码所示,变量“z”的值总是10。
定义中我们使用的比较广义的词 —— “代码块”,然而,通常(在ECMAScript中)会使用我们经常用到的函数。当然了,并不是所有对闭包的实现都会将闭包和函数绑在一起,比方说,在Ruby语言中,闭包就有可能是: 一个过程对象(procedure object), 一个lambda表达式或者是代码块。
对于要实现将局部变量在上下文销毁后仍然保存下来,基于栈的实现显然是不适用的(因为与基于栈的结构相矛盾)。因此在这种情况下,上层作用域的闭包数据是通过 动态分配内存的方式来实现的(基于“堆”的实现),配合使用垃圾回收器(garbage collector简称GC)和 引用计数(reference counting)。这种实现方式比基于栈的实现性能要低,然而,任何一种实现总是可以优化的: 可以分析函数是否使用了自由变量,函数式参数或者函数式值,然后根据情况来决定 —— 是将数据存放在堆栈中还是堆中。
延伸阅读
此文章所在专题列表如下:
- 我们应该如何去了解JavaScript引擎的工作原理
- JavaScript探秘:编写可维护的代码的重要性
- JavaScript探秘:谨慎使用全局变量
- JavaScript探秘:var预解析与副作用
- JavaScript探秘:for循环(for Loops)
- JavaScript探秘:for-in循环(for-in Loops)
- JavaScript探秘:Prototypes强大过头了
- JavaScript探秘:eval()是“魔鬼”
- JavaScript探秘:用parseInt()进行数值转换
- JavaScript探秘:基本编码规范
- JavaScript探秘:函数声明与函数表达式
- JavaScript探秘:命名函数表达式
- JavaScript探秘:调试器中的函数名
- JavaScript探秘:JScript的Bug
- JavaScript探秘:JScript的内存管理
- JavaScript探秘:SpiderMonkey的怪癖
- JavaScript探秘:命名函数表达式替代方案
- JavaScript探秘:对象Object
- JavaScript探秘:原型链 Prototype chain
- JavaScript探秘:构造函数 Constructor
- JavaScript探秘:可执行的上下文堆栈
- 执行上下文其一:变量对象与活动对象
- 执行上下文其二:作用域链 Scope Chains
- 执行上下文其三:闭包 Closures
- 执行上下文其四:This指针
- JavaScript探秘:强大的原型和原型链
- JavaScript函数其一:函数声明
- JavaScript函数其二:函数表达式
- JavaScript函数其三:分组中的函数表达式
- JavaScript函数其四:函数构造器
- JavaScript变量对象其一:VO的声明
- JavaScript变量对象其二:VO在不同的执行上下文中
- JavaScript变量对象其三:执行上下文的两个阶段
- JavaScript变量对象其四:关于变量
- JavaScript变量对象其五:__parent__ 属性
- JavaScript作用域链其一:作用域链定义
- JavaScript作用域链其二:函数的生命周期
- JavaScript作用域链其三:作用域链特征
- JavaScript闭包其一:闭包概论
- JavaScript闭包其二:闭包的实现
- JavaScript闭包其三:闭包的用法
JavaScript闭包其一:闭包概论 函数式编程中一些基本定义的更多相关文章
- Python函数式编程中map()、reduce()和filter()函数的用法
Python中map().reduce()和filter()三个函数均是应用于序列的内置函数,分别对序列进行遍历.递归计算以及过滤操作.这三个内置函数在实际使用过程中常常和“行内函数”lambda函数 ...
- 理解函数式编程中的函数组合--Monoids(二)
使用函数式语言来建立领域模型--类型组合 理解函数式编程语言中的组合--前言(一) 理解函数式编程中的函数组合--Monoids(二) 继上篇文章引出<范畴论>之后,我准备通过几篇文章,来 ...
- javascript里的偏函数——本质函数式编程+闭包,返回函数
最终效果: var greet = function(greeting, name) { return greeting + ' ' + name; }; var sayHelloTo = _.par ...
- Python“函数式编程”中常用的函数
1.map(func,seq[,seq,...]) 对序列中的每个元素应用函数,python2中map()返回的是列表,python3中返回的是迭代器,可以用list()转换成列表.以下例子为pyth ...
- 多角度让你彻底明白yield语法糖的用法和原理及在C#函数式编程中的作用
如果大家读过dapper源码,你会发现这内部有很多方法都用到了yield关键词,那yield到底是用来干嘛的,能不能拿掉,拿掉与不拿掉有多大的差别,首先上一段dapper中精简后的Query方法,先让 ...
- VS2010和matlab2010混合编程中char16_t重定义的问题
原因是VS2010中的yvals.h添加了char16_t的定义,而Matlab的matrix.h也包含对char16_t的定义,所以同时包含这两个头文件的话,会导致重复定义char16_t的错误.只 ...
- 跨平台编程中的宏定义(__LINE__和__DATE__极其有用)
要用师兄的计算机算东西,无赖那上面是WINDOWS的系统,为了写出能够跨平台的代码,需要在代码中用到宏来选择编译.一种方法是自己在Makefile里面定义好该平台对应的宏.实际上,编译器基本上都会有一 ...
- linux shell编程中的数组定义、遍历
代码如下: 数组定义法1: arr=( ) # 注意是用空格分开,不是逗号!! 数组定义法2: array array[]="a" array[]="b" ar ...
- Java函数式编程:二、高阶函数,闭包,函数组合以及柯里化
承接上文:Java函数式编程:一.函数式接口,lambda表达式和方法引用 这次来聊聊函数式编程中其他的几个比较重要的概念和技术,从而使得我们能更深刻的掌握Java中的函数式编程. 本篇博客主要聊聊以 ...
随机推荐
- 【mysql优化 3】嵌套循环连接算法
原文地址:Nested-Loop Join Algorithms mysql在表之间执行连接操作,包括了使用循环嵌套算法或者其他在此基础上的变形. 循环嵌套连接算法: 一个简单的嵌套循环连接(NLJ: ...
- JDBC 学习笔记(三)—— JDBC 常用接口和类,JDBC 编程步骤
1. JDBC 常用接口和类 DriverManager 负责管理 JDBC 驱动的服务类,程序中主要的功能是获取连接数据库的 Connection 对象. Connection 代表一个数据库连接对 ...
- 【Luogu】P4035球形空间产生器(高斯消元)
题目链接 水比题,把圆方程展开减一下把平方都减掉半径的平方也减掉,高斯消元即可. 然后我只输出两位小数,爆了两次零.我好菜啊. #include<cstdio> #include<c ...
- [BZOJ3817]Sum
[BZOJ3817]Sum 试题描述 给定正整数N,R.求 输入 第一行一个数 T,表示有 T 组测试数据. 接下来 T 行,每行两个正整数 n,r. 输出 输出 T 行,每行一个整数表示答案. 输入 ...
- poj 2079 Triangle
Triangle Time Limit: 3000MS Memory Limit: 30000K Total Submissions: 9835 Accepted: 2951 Descript ...
- 转 如何在C++中调用C程序
如何在C++中调用C程序? C++和C是两种完全不同的编译链接处理方式,如果直接在C++里面调用C函数,会找不到函数体,报链接错误.要解决这个问题,就要在 C++文件里面显示声明一下哪些函数是C写 ...
- 11深入理解C指针之---指针和常量
该系列文章源于<深入理解C指针>的阅读与理解,由于本人的见识和知识的欠缺可能有误,还望大家批评指教. 指针作为C语言的左膀右臂,使用方便,修改容易,引用数据快速都是很有前景的应用.C语言中 ...
- linux信号-------初涉
一.信号的本质 软中断信号(signal,又简称为信号)用来通知进程发生了异步事件.在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的.信号是进程 ...
- Select函数实现
int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, ...
- Python入门--19--else语句、with语句
1.else与while连用: x=input('请输出一个整数:') while x>0 x=x-2 print(x) else: print('x已经小于等于零了!') 2.else与try ...