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)。这种实现方式比基于栈的实现性能要低,然而,任何一种实现总是可以优化的: 可以分析函数是否使用了自由变量,函数式参数或者函数式值,然后根据情况来决定 —— 是将数据存放在堆栈中还是堆中。

延伸阅读

此文章所在专题列表如下:

  1. 我们应该如何去了解JavaScript引擎的工作原理
  2. JavaScript探秘:编写可维护的代码的重要性
  3. JavaScript探秘:谨慎使用全局变量
  4. JavaScript探秘:var预解析与副作用
  5. JavaScript探秘:for循环(for Loops)
  6. JavaScript探秘:for-in循环(for-in Loops)
  7. JavaScript探秘:Prototypes强大过头了
  8. JavaScript探秘:eval()是“魔鬼”
  9. JavaScript探秘:用parseInt()进行数值转换
  10. JavaScript探秘:基本编码规范
  11. JavaScript探秘:函数声明与函数表达式
  12. JavaScript探秘:命名函数表达式
  13. JavaScript探秘:调试器中的函数名
  14. JavaScript探秘:JScript的Bug
  15. JavaScript探秘:JScript的内存管理
  16. JavaScript探秘:SpiderMonkey的怪癖
  17. JavaScript探秘:命名函数表达式替代方案
  18. JavaScript探秘:对象Object
  19. JavaScript探秘:原型链 Prototype chain
  20. JavaScript探秘:构造函数 Constructor
  21. JavaScript探秘:可执行的上下文堆栈
  22. 执行上下文其一:变量对象与活动对象
  23. 执行上下文其二:作用域链 Scope Chains
  24. 执行上下文其三:闭包 Closures
  25. 执行上下文其四:This指针
  26. JavaScript探秘:强大的原型和原型链
  27. JavaScript函数其一:函数声明
  28. JavaScript函数其二:函数表达式
  29. JavaScript函数其三:分组中的函数表达式
  30. JavaScript函数其四:函数构造器
  31. JavaScript变量对象其一:VO的声明
  32. JavaScript变量对象其二:VO在不同的执行上下文中
  33. JavaScript变量对象其三:执行上下文的两个阶段
  34. JavaScript变量对象其四:关于变量
  35. JavaScript变量对象其五:__parent__ 属性
  36. JavaScript作用域链其一:作用域链定义
  37. JavaScript作用域链其二:函数的生命周期
  38. JavaScript作用域链其三:作用域链特征
  39. JavaScript闭包其一:闭包概论
  40. JavaScript闭包其二:闭包的实现
  41. JavaScript闭包其三:闭包的用法
时间: 2024-10-25 09:38:59

JavaScript闭包其一:闭包概论 函数式编程中一些基本定义的相关文章

函数式编程中的函数—函数式编程的多态

函数式编程中的函数—函数式编程的多态 保存了计算上下文和计算过程的特殊值 保存了计算上下文和计算过程的有输入输出的可计算结构. 保存了计算上下文和计算过程的可配置结构:(柯里化). 函数作为一种特殊的值和结构,可以由更高阶的函数对其进行组合.变换.柯里化等操作: 函数作为输入和输出:本质上都是可配置.可计算结构. 可配置的是作为参量输入的计算上下文: 可计算是计算上下文配置完备后直接进行计算: 普通函数: 参数函数(闭包函数): 高阶函数: 返回函数(内部函数): 值函数:被其它函数引用和调用的

C#函数式编程中的标准高阶函数详解

何为高阶函数 大家可能对这个名词并不熟悉,但是这个名词所表达的事物却是我们经常使用到的.只要我们的函数的参数能够接收函数,或者函数能够返回函数,当然动态生成的也包括在内.那么我们就将这类函数叫做高阶函数.但是今天我们的标题并不是高阶函数,而是标准高阶函数,既然加上了这个标准,就意味着在函数式编程中有一套标准的函数,便于我们每次调用.而今天我们将会介绍三个标准函数,分别为Map.Filter.Fold. Map 这个函数的作用就是将列表中的每项从A类型转换到B类型,并形成一个新的类型.下面我们可以

C#函数式编程中的部分应用详解

何谓函数式编程 相信大家在实际的开发中,很多情况下完成一个功能都需要借助多个类,那么我们这里的基本单元就是类.而函数式编程则更加细化,致使我们解决一个功能的基本单元是函数,而不是类,每个功能都是由多个函数构成,并且函数之间没有直接的关系.如果简单的文字描述还不足以让你理解,下面我们就配以图来演示. 如下图所示,图左是我们设计好的三个函数,而右边则是我们需要实现的功能.而我们需要做的就是利用这三个函数去完成对应的三个功能,笔者在这里只是进行简单而又形象的表述,实际的开发过程可能需要更多的函数,并且

C#函数式编程中的递归调用之尾递归详解

关于递归相信大家已经熟悉的不能再熟悉了,所以笔者在这里就不多费口舌,不懂的读者们可以在博客园中找到很多与之相关的博客.下面我们直接切入正题,开始介绍尾递归. 尾递归 普通递归和尾递归如果仅仅只是从代码的角度出发来看,我们可能发现不了他的特点,所以笔者利用两张堆栈上的图来展示具体的差距在哪,首先我们来看看普通的递归调用的情况,如下图1.1所示: 假设这里执行的函数是Func1,并且Func1中通过递归调用了自己,那么我们可以看到栈上在每次调用Func1的时候都会重新将函数返回地址等其他参数放入栈中

javascript里的偏函数——本质函数式编程+闭包,返回函数

除非你已经使用过其他的函数式编程,不然你可能对下面这两个概念很陌生:"偏函数应用"和"函数柯里化". 偏函数应用,英文是partial application,也可以译作"局部应用"."部分应用"."偏应用" 函数柯里化,英文是currying,也可以译作"局部套用"."加里化"."卡瑞化" 话虽如此,但是假如你一旦理解了这两个概念,你就完全可以在

前端函数式编程中的各种概念

本文不涉及深层次的解析,仅作为知识点的查阅.因为概念介绍的比较简单,所以只适合有一定基础的人群阅读. 1.什么是函数式编程? 利用函数把运算过程封装起来,通过组合各种函数来计算结果.(将一套业务流程分割成多个功能,每个功能都用函数封装起来,最后实现这个业务其实就是在调用这些功能函数,这就是函数式编程) 2.一些与函数式编程相关的概念 纯函数 - 指的是函数无副作用,相同的输入(函数参数)有相同的输出(函数返回) 闭包 - 存在内.外两层函数,内层函数对外层函数的局部变量进行来引用.(用途:定义一

Javascript 中的函数式编程

本文和大家分享的主要是javascript中函数式编程相关内容,一起来看看吧,希望对大家学习javascript有所帮助. 函数式编程(functional programming)或称函数程序设计,又称泛函编程,是一种编程范型,比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程. 函数式编程,近年来一直被炒得火热,国内外的开发者好像都在议论和提倡这种编程范式.在众多的函数式语言中,Jav

Golang-函数式编程(闭包)

github:https://github.com/ZhangzheBJUT/blog/blob/master/closure.md 一 函数式编程概论 在过去近十年时间里,面向对象编程大行其道,以至于在大学的教育里,老师也只会教给我们两种编程模型,面向过程和面向对象.孰不知,在面向对象思想产生之前,函数式编程已经有了数十年的历史.就让我们回顾这个古老又现代的编程模型,看看究竟是什么魔力将这个概念在21世纪的今天再次拉入我们的视野. 随着硬件性能的提升以及编译技术和虚拟机技术的改进,一些曾被性能

从闭包谈函数式编程

记得开始学习golang的时候,boss给我发了一篇paper,讲的是函数式编程,那个时候看了一遍,懵懵懂懂,只有一个感觉,函数式编程怎么好像讲的都是数学公式(说句实话,我那个时候还真不理解boss为什么给我发那篇paper,只是想反正多看点东西总不会错)?最近一段时间,go用的比较多,业务逻辑让我思考了很多问题,虽然go并非函数式语言,但却支持了很多函数式的概念,让编程变得简单.当然,在写代码的过程中,我就很怀疑,使用了函数式编程会不会导致运行效率变低? 这篇文章就从几方面学习函数式编程(by