我们先来说几个词,函数、函数对象、函数实例、函数引用、构造器/类、闭包、上下文。当然,一个个的词,很累。但没办法,要精确。

函数这个,用得多词义也乱,大多数时候是眉毛胡子不分地乱用的,所以即使是官方文档上或正规书籍中,它与后面的几个词是不清不楚。为了简便,我们这里只用它的一个含义:代码文本中的一个函数,它是一块代码文本,形式上的,为人——程序员所理解的。

函数对象这个,一般的语言中不这么讲,因为函数一般就是执行体,而不属于对象系统。但中文的“对象”有“目标”的含义,所以一些不负责的翻译也会用错这个词。那么,我们在JS中讨论函数对象,应该是指:将函数理解归入对象系统时的一个概念。这种情况下,一个函数是一个具体的函数对象(而非它的类别)。就对象(实例)本身来讲,对象就是一个属性列表,或者说,对象是一个数据结构。因而这个函数对象也具有成员,例如:
function afunc() {
}
alert(afunc.toString());
我们访问toString成员时,本质上就是将afunc作为“函数的面向对象形式”来理解;我们调用afunc()时,就是将它作为“函数的执行期对象”来理解。

所谓“执行期对象”只是我现下临时使用的一个词,与“面向对象系统”没什么关系。那么什么是执行期对象呢?对于解释引擎来说,执行期对象就是一段源代码序列,或者源代码序列所编译成的中间代码序列。这个代码序列在内存中的某个位置上,从出口入口,但他不见得“一定是”源代码文本。举例来说,eval()中的代码文本,就被临时编译成一个用于执行的序列“执行期对象”。这个,在JS引擎内部,也称为代码上下文,也称为闭包,也称为XXXX。这个东西也很乱,有很多种说法。

我们现在面临着静态的东西,动态的东西,编译前的、后的。总之在不同环境中混用了名词,所以我们就乱了。那么怎么理解上面的内容呢?我们先回顾上面说的:

文本       执行期
------------------------------
函数   ->   函数对象


执行期作为对象理解      执行期作为函数理解
----------------------------------------------------------------
afunc.toString                afunc()为了避免歧义,我们先不讨论执行“执行期作为对象理解”的问题,也就是说,我们下面要不再讲“函数对象”这个问题。而且,为了区别出这个“函数的执行期对象”来,我们叫它“函数实例”。我们强调,这个函数实例,就是执行引擎中这个代码的一个映射:编译的,或不编译的。

那么这样讲的时候,就可以借用“引用”这个概念了,也就是说一个实例可以有多个引用。例如
----------------------------------------------------------------
// foo()是静态的代码文本
function foo() {
}

// 下面的代码是执行期的,“x=foo”作为语句, 为foo函数实例创建了一个引用...
var x = foo;
var y = foo;
----------------------------------------------------------------

现在,引用与实例清楚了。用这两个名词,只是为了借“实例/引用”之间的关系,来说明“函数的执行期对象”——这样一段引擎中的代码,而不是文本文件中的代码。

现在问,一个函数(文本)有多少个实例呢?实例又有多少个引用呢?答案是,一个函数(文本)可以有多个实例,每个实例又可以有多个引用。实例有多个引用比较常见,上面这个例子就是了:foo有x,y两个引用。

那么一个函数多个实例呢?
-----
function MyTest() {
  function afunc() {  }
  return afunc;
}

var f1 = MyTest();
var f2 = MyTest();
-----
这个就是了。这个用表达式“f1 === f2”一看就知道。它们是两个不同的函数,但是,对应了相同的代码文本。接下来,我们来讲闭包。什么是闭包呢?闭包就是“属性表”,闭包就是一个数据块,闭包就是一个存放着“Name=Value”的对照表。就这么简单。但是,必须强调,闭包是运行期概念,一个函数实例——例如上面的f1/f2在没有“执行”时,是没有闭包的!

为什么?怎么会?!这个我在《JavaScript语言精髓与编程实践》的第228页开始反复讲,试图用引擎内部结构来说明“调用对象”、“全局对象”这样一些概念。很复杂。但是,我们能不能简单地讲讲什么是“函数不执行,就没有闭包”这个问题呢?

其实很简单,函数还没执行的话,他就是上述的一个函数实例。这个实例本身,存有一套函数内部的、初始的“Name=value”对照表。例如说:
function MyFunc() {
  var xxx;
  function fff() { }
}
那么这个对照表就有名称xxx,以及"fff = ..."的名称/值对。但是,这些都是初始化时的状态——没执行,也就什么也没变化过。

现在,我们要开始执行了——闭包是执行期概念。我们调用MyFunc(),接下来立即就有问题,上面的xxx/yyy是直接使用呢?还是不直接使用?显然,我们不能直接使用,因为直接使用的话,我们下次执行时就有遗留了现场,就麻烦了。我们得保留一套原始值的。这么一来,引擎在执行期的解决方法就是:创建一个闭包(也就是新的对照表),将这些原始值复制一份过来。

所以,事实上是:当我们每次调用MyFunc()时,就会创建一个新的闭包并且复制一套初始化的“名称/值”列表。

再接下来,为什么会说“一个实例可以有多个闭包”呢?是这样,首先,我们在函数内部可以存取callee属性:
function MyTest() {
  return arguments.callee
}
而这种情况下,callee只能算“当前的MyTest函数实例”的一个引用。对于这个调用来说,我们只是在操作同一
个实例。但是我们看下面的代码:
MyTest()()
这里用“()”执行了两次,由于第一次返回了callee,如上所述,第二次与第一次都执行的是同一个实例。

回到前面的设问:我们执行时遗留了现场怎么办?

难道两次执行使用同一个闭包?当然不是,根本的规则在于:每次调用“()”都生成一个新的闭包。

这样,两次执行或者更多次执行时,就可以使用不同的闭包了,相互之间也不干扰。

所以回顾上面的流程,我说:函数(文本)有多个函数实例,每个函数实例在执行期可以有多个闭包。

注意的是,我在书的章节标题中写着“(在被调用时,)每个函数实例至少拥有一个闭包”,这里做了一个强调,就是“被调用时”。也就是执行“()”运算时,这个上面就讨论到了。但反过来说,“在没被调用时”呢?

函数实例如果未被调用(我的意思是一次也没有)时,它就没有闭包:
-------
F = function() {
};
-------
这个F在运行期可以访问到,例如存取toString()成员,但是由于它没有执行,所以它就没有闭包。当它执行过一次或多次时,根据每次执行的效果的不同,可能闭包在调用完之后就被释放了,也可能不被释放,这个取决于运行的情况。

最后,说明一下,闭包的释放发生在函数执行结束时,以及闭包内的标识符(向外部的引用)被释放时。这个,理解引用机制或内存收集机制的同学应该明白了。我们上面,讲过了
---
函数、函数实例、函数引用、闭包
---
这些概念。

但是,我们跳过了“函数对象”这个概念,但我们把它分类到“函数的面向对象形式”的范围中去了。我们接下来单独的说明它。当然,在这个语境中,我们还有“构造器/类”这样的一个概含要讲。

当我们访问MyFunc.toString之类的成员时,我们使用到“函数的面向对象形式”了。这种情况下,函数是一个对象,它怎么“被构建”的呢?有两种方法:
---
// 方法一:直接量声明
function myfunc() { }

// 匿名函数也是直接量,同上
x = function() { }

// 方法二:用Function()来构造
foo = new Function();
---
所以,无论是上面的myfunc,还是foo或x,都是函数对象。这是面向对象体系下的东东。

但是,JavaScript又规定了另外的一套逻辑,这就是:对象可以由构造器构造出来,构造器是一个函数。

所以“函数可以作为构造器理解”,这就使得JavaScript有了一个对象衍生的基础系统。在“基于类的对象系统”中,构造对象的,就称为类,所以事实上有一些文档中,JavaScript的构造器也称为类。例如instanceof运算,一般就解释为“<object> instanceOf <class>“,这里的class,填入一个构造器就成了。

构造器与函数之外没有表面上的区别。他们的区别仅在于你如何用它。例如:
---
// 做函数调用运算时, func就是函数
func();

// 做new运算时,func就是构造器
o = new func()
---
不过在习惯上,我们应该将一个构造器命名为大写字符开始,而将函数命名为小写字符开始。这是一种习惯,表明函数代码书写者的原始意图。是个很好的习惯。

与此相同的,“函数对象”只是函数的一种形式,也取决于我们如何用它。前面已经说到,当我们访问函数的属性时,它就是函数对象:
---
// 访问func的属性/成员时,func就是函数对象
func.argument.length
---

最后,hax说我“是拿JS引擎来解说”,这个的确没错。我上面的解释,以及书中的解释都是以“JS引擎执行期”为背景的。但是,术语的使用上倒没有混乱,不过没有办法的是:如果读者用静态的观念,去看执行期的描述,大概就会偏掉了。

又,抽象概念和实现机制混杂的问题是有的。但好象我也没办法,我们拿任何一个名称去指称它物时,就是抽象。我要描述一个东西,就得用抽象,实现机制也是要描述的嘛。。。。总不能通篇地用着“函数的面向对象形式”,以及“函数的执行期对象”吧。那样才会死人嘀。。。。