理解 JavaScript 闭包[未完成]

原文链接:JavaScript Closures

目录

简介

返回目录

Closure
所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

闭包是 ECMAScript (JavaScript)最强大的特性之一,但用好闭包的前提是必须理解闭包。闭包的创建相对容易,人们甚至会在不经意间创建闭包,但这些无意创建的闭包却存在潜在的危害,尤其是在比较常见的浏览器环境下。如果想要扬长避短地使用闭包这一特性,则必须了解它们的工作机制。而闭包工作机制的实现很大程度上有赖于标识符(或者说对象属性)解析过程中作用域的角色。

关于闭包,最简单的描述就是 ECMAScript 允许使用内部函数--即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。也就是说,内部函数会在外部函数返回后被执行。而当这个内部函数执行时,它仍然必需访问其外部函数的局部变量、参数以及其他内部函数。这些局部变量、参数和函数声明(最初时)的值是外部函数返回时的值,但也会受到内部函数的影响。

遗憾的是,要适当地理解闭包就必须理解闭包背后运行的机制,以及许多相关的技术细节。虽然本文的前半部分并没有涉及 ECMA 262 规范指定的某些算法,但仍然有许多无法回避或简化的内容。对于个别熟悉对象属性名解析的人来说,可以跳过相关的内容,但是除非你对闭包也非常熟悉,否则最好是不要跳下面几节。

对象属性名解析

返回目录

ECMAScript 认可两类对象:原生(Native)对象和宿主(Host)对象,其中宿主对象包含一个被称为内置对象的原生对象的子类(ECMA 262 3rd Ed Section 4.3)。原生对象属于语言,而宿主对象由环境提供,比如说可能是文档对象、DOM 等类似的对象。

原生对象具有松散和动态的命名属性(对于某些实现的内置对象子类别而言,动态性是受限的--但这不是太大的问题)。对象的命名属性用于保存值,该值可以是指向另一个对象(Objects)的引用(在这个意义上说,函数也是对象),也可以是一些基本的数据类型,比如:String、Number、Boolean、Null 或 Undefined。其中比较特殊的是 Undefined 类型,因为可以给对象的属性指定一个 Undefined 类型的值,而不会删除对象的相应属性。而且,该属性只是保存着 undefined 值。 

下面简要介绍一下如何设置和读取对象的属性值,并最大程度地体现相应的内部细节。

值的赋予

返回目录

对象的命名属性可以通过为该命名属性赋值来创建,或重新赋值。即,对于:

var objectRef = new Object(); //创建一个普通的 JavaScript 对象。

可以通过下面语句来创建名为 “testNumber” 的属性:

objectRef.testNumber = 5;
/* – 或- */
objectRef["testNumber"] = 5;

在赋值之前,对象中没有“testNumber” 属性,但在赋值后,则创建一个属性。之后的任何赋值语句都不需要再创建这个属性,而只会重新设置它的值:

objectRef.testNumber = 8;
/* – or:- */
objectRef["testNumber"] = 8;

稍后我们会介绍,Javascript 对象都有原型(prototypes)属性,而这些原型本身也是对象,因而也可以带有命名的属性。但是,原型对象命名属性的作用并不体现在赋值阶段。同样,在将值赋给其命名属性时,如果对象没有该属性则会创建该命名属性,否则会重设该属性的值。

值的读取

返回目录

当读取对象的属性值时,原型对象的作用便体现出来。如果对象的原型中包含属性访问器(property accessor)所使用的属性名,那么该属性的值就会返回:

/* 为命名属性赋值。如果在赋值前对象没有相应的属性,那么赋值后就会得到一个:*/
objectRef.testNumber = 8;

/* 从属性中读取值 */
var val = objectRef.testNumber;

/* 现在, – val – 中保存着刚赋给对象命名属性的值 8*/

而且,由于所有对象都有原型,而原型本身也是对象,所以原型也可能有原型,这样就构成了所谓的原型链。原型链终止于链中原型为 null 的对象。Object 构造函数的默认原型就有一个 null 原型,因此:

var objectRef = new Object(); //创建一个普通的 JavaScript 对象。

创建了一个原型为 Object.prototype 的对象,而该原型自身则拥有一个值为 null 的原型。也就是说, objectRef 的原型链中只包含一个对象-- Object.prototype。但对于下面的代码而言:

/* 创建 – MyObject1 – 类型对象的函数*/
function MyObject1(formalParameter){
/* 给创建的对象添加一个名为 – testNumber – 的属性并将传递给构造函数的第一个参数作为该属性的值:*/
this.testNumber = formalParameter;
}
/* 创建 – MyObject2 – 类型对象的函数*/
function MyObject2(formalParameter){
/* 给创建的对象添加一个名为 – testString – 的属性并将传递给构造函数的第一个参数作为该属性的值:*/
this.testString = formalParameter;
}

/* 接下来的操作用 MyObject1 类的实例替换了所有与 MyObject2 类的实例相关联的原型。而且,为 MyObject1 构造函数传递了参数 – 8 – ,因而其 – testNumber – 属性被赋予该值:*/
MyObject2.prototype = new MyObject1( 8 );

/* 最后,将一个字符串作为构造函数的第一个参数,创建一个 – MyObject2 – 的实例,并将指向该对象的引用赋给变量 – objectRef – :*/
var objectRef = new MyObject2( “String_Value” );

被变量 objectRef 所引用的 MyObject2 的实例拥有一个原型链。该链中的第一个对象是在创建后被指定给 MyObject2 构造函数的 prototype 属性的 MyObject1 的一个实例。MyObject1 的实例也有一个原型,即与 Object.prototype 所引用的对象对应的默认的 Object 对象的原型。最后, Object.prototype 有一个值为 null 的原型,因此这条原型链到此结束。

当某个属性访问器尝试读取由 objectRef 所引用的对象的属性值时,整个原型链都会被搜索。在下面这种简单的情况下:

var val = objectRef.testString;

- 因为 objectRef 所引用的 MyObject2 的实例有一个名为“testString”的属性,因此被设置为“String_Value”的该属性的值被赋给了变量 val。但是:

var val = objectRef.testNumber;

- 则不能从 MyObject2 实例自身中读取到相应的命名属性值,因为该实例没有这个属性。然而,变量 val 的值仍然被设置为 8,而不是未定义--这是因为在该实例中查找相应的命名属性失败后,解释程序会继续检查其原型对象。而该实例的原型对象是 MyObject1 的实例,这个实例有一个名为“testNumber”的属性并且值为 8,所以这个属性访问器最后会取得值 8。而且,虽然
MyObject1MyObject2 都没有定义 toString 方法,但是当属性访问器通过 objectRef 读取 toString 属性的值时:

var val = objectRef.toString;

- 变量 val 也会被赋予一个函数的引用。这个函数就是在 Object.prototypetoString 属性中所保存的函数。之所以会返回这个函数,是因为发生了搜索 objectRef 原型链的过程。当在作为对象的 objectRef 中发现没有“toString”属性存在时,会搜索其原型对象,而当原型对象中不存在该属性时,则会继续搜索原型的原型。而原型链中最终的原型是 Object.prototype,这个对象确实有一个 toString 方法,因此该方法的引用被返回。

最后:

var val = objectRef.madeUpProperty;

- 返回 undefined,因为在搜索原型链的过程中,直至 Object.prototype 的原型--null,都没有找到任何对象有名为“madeUpPeoperty”的属性,因此最终返回 undefined

不论是在对象或对象的原型中,读取命名属性值的时候只返回首先找到的属性值。而当为对象的命名属性赋值时,如果对象自身不存在该属性则创建相应的属性。

这意味着,如果像 objectRef.testNumber = 3 执行一条赋值语句,那么 MyObject2 实例自身也会创建一个名为“testNumber”的属性,而之后任何读取该命名属性的尝试都将获得相同的新值。这时候,属性访问器不会再进一步搜索原型链,但 MyObject1 实例值为 8 的“testNumber”属性并没有被修改。给
objectRef 对象的赋值只是遮挡了其原型链中相应的属性。

注意:ECMAScript 为 Object 类型定义了一个内部 [[prototype]] 属性。这个属性不能通过脚本直接访问,但在属性访问器解析过程中,则需要用到这个内部 [[prototype]] 属性所引用的对象链--即原型链。可以通过一个公共的 prototype 属性,来对与内部的 [[prototype]] 属性对应的原型对象进行赋值或定义。这两者之间的关系在 ECMA 262(3rd edition)中有详细描述,但超出了本文要讨论的范畴。

标识符解析、执行环境和作用域链

执行环境

返回目录

执行环境是 ECMAScript 规范(ECMA 262 第 3 版)用于定义 ECMAScript 实现必要行为的一个抽象的概念。对如何实现执行环境,规范没有作规定。但由于执行环境中包含引用规范所定义结构的相关属性,因此执行环境中应该保有(甚至实现)带有属性的对象--即使属性不是公共属性。

所有 JavaScript 代码都是在一个执行环境中被执行的。全局代码(作为内置的JS 文件执行的代码,或者 HTML 页面加载的代码)是在我称之为“全局执行环境”的执行环境中执行的,而对函数的每次调用(
有可能是作为构造函数)同样有关联的执行环境。通过 eval 函数执行的代码也有截然不同的执行环境,但因为 JavaScript 程序员在正常情况下一般不会使用 eval,所以这里不作讨论。有关执行环境的详细说明请参阅 ECMA 262(3rd edition)第 10.2 节。

当调用一个 JavaScript 函数时,该函数就会进入相应的执行环境。如果又调用了另外一个函数(或者递归地调用同一个函数),则又会创建一个新的执行环境,并且在函数调用期间执行过程都处于该环境中。当调用的函数返回后,执行过程会返回原始执行环境。因而,运行中的 JavaScript 代码就构成了一个执行环境栈。

在创建执行环境的过程中,会按照定义的先后顺序完成一系列操作。首先,在一个函数的执行环境中,会创建一个“活动”对象。活动对象是规范中规定的另外一种机制。之所以称之为对象,是因为它拥有可访问的命名属性,但是它又不像正常对象那样具有原型(至少没有定义的原型),而且不能通过 JavaScript 代码直接引用活动对象。

为函数调用创建执行环境的下一步是创建一个 arguments 对象,这是一个类似数组的对象,它以整数索引的数组成员一一对应地保存着调用函数时所传递的参数。这个对象也有 lengthcallee 属性(这两个属性与我们讨论的内容无关,详见规范)。然后,会为活动对象创建一个名为“arguments”的属性,该属性引用前面创建的 arguments对象。

接着,为执行环境分配作用域。作用域由对象列表(链)组成。每个函数对象都有一个内部的 [[scope]] 属性(该属性我们稍后会详细介绍),这个属性也由对象列表(链)组成。指定给一个函数调用执行环境的作用域,由该函数对象的 [[scope]] 属性所引用的对象列表(链)组成,同时,活动对象被添加到该对象列表的顶部(链的前端)。

之后会发生由 ECMA 262 中所谓“可变”对象完成的“变量实例化”的过程。只不过此时使用活动对象作为可变对象(这里很重要,请注意:它们是同一个对象)。此时会根据函数的形式参数为可变对象创建命名属性,如果调用函数时传递的参数与形式参数一致,则相应参数的值会赋给这些命名属性(否则,会给命名属性赋 undefined 值)。对于定义的内部函数,会以其声明时所用名称为可变对象创建同名属性,而这个内部函数则被创建为函数对象并指定给该属性。变量实例化的最后一步是根据函数内部声明的所有局部变量为可变对象创建命名属性。

根据声明的局部变量创建的可变对象属性在变量实例化期间会被赋予 undefined 值。在执行函数体内的代码、并计算相应的赋值表达式之前不会对局部变量执行真正的实例化。

事实上,拥有 arguments 属性的活动对象和拥有与函数局部变量一致的命名属性的可变对象是同一个对象。因此,可以将标识符 arguments 作为函数的局部变量来看待。

最后,要为使用 this 关键字而赋值。如果所赋的值引用一个对象,那么前缀以 this 关键字的属性访问器就是引用该对象的属性。如果所赋(内部)值是 null,那么 this 关键字则引用全局对象。

创建全局执行环境的过程会稍有不同,因为它没有参数,所以不需要通过定义的活动对象来引用这些参数。但全局执行环境也需要一个作用域,而它的作用域链实际上只由一个对象--全局对象--组成。全局执行环境也会有变量实例化的过程,它的内部函数就是涉及大部分 JavaScript 代码的、常规的顶级函数声明。而且,在变量实例化过程中全局对象就是可变对象,这就是为什么全局性声明的函数是全局对象属性的原因。全局性声明的变量同样如此。

全局执行环境也会使用 this 对象来引用全局对象。

作用域链与 [[scope]]

返回目录

调用函数时创建的执行环境会包含一个作用域链,这个作用域链是通过将该执行环境的活动(可变)对象添加到保存于所调用函数对象的 [[scope]] 属性中的作用域链前端而构成的。所以,理解函数对象内部的 [[scope]] 属性的定义过程是关键。

在 ECMAScript 中,函数也是对象。函数对象在变量实例化期间会根据函数声明来创建,或者是在计算函数表达式或调用 Function 构造器时创建。

通过调用 Function 构造器创建的函数对象,其内部的 [[scope]] 属性引用的作用域链中始终只包含全局对象。

通过函数声明或函数表达式创建的函数对象,其内部的 [[scope]] 属性引用的则是创建它们的执行环境的作用域链。

在最简单的情况下,比如声明如下全局函数:-

function exampleFunction(formalParameter){
// 函数体内的代码
}

- 当为创建全局执行环境而进行变量实例化时,会根据上面的函数声明创建相应的函数对象。因为全局执行环境的作用域链中只包含全局对象,所以它就给自己创建的、并以名为“exampleFunction”的属性引用的这个函数对象的内部 [[scope]] 属性,赋予了只包含全局对象的作用域链。

当在全局环境中计算函数表达式时,也会发生类似的指定作用域链的过程:-

var exampleFuncRef = function(){
// 函数体代码
}
- 在这种情况下,不同的是在全局执行环境的变量实例化过程中,会先创建全局对象的一个命名属性。而在计算赋值语句之前,暂时不会创建函数对象,也不会将该函数对象的引用指定给全局对象的命名属性。但是,最终还是会在全局执行环境中创建这个函数对象(当计算函数表达式时。译者注),而为这个创建的函数对象的 [[scope]] 属性指定的作用域链中仍然只包含全局对象。内部的函数声明或表达式会导致在包含它们的外部函数的执行环境中创建相应的函数对象,因此这些函数对象的作用域链会稍微复杂一些。在下面的代码中,先定义了一个带有内部函数声明的外部函数,然后执行了外部函数:-

function exampleOuterFunction(formalParameter){
function exampleInnerFuncitonDec(){
… // 内部函数体代码
}
… // 其余的外部函数体代码
}
exampleOuterFunction( 5 );

与外部函数声明对应的函数对象会在全局执行环境的变量实例化过程中被创建。因此,外部函数对象的 [[scope]] 属性中会包含一个只有全局对象的“单项目”作用域链。

当在全局执行环境中调用 exampleOuterFunction 函数时,会为该函数调用创建一个新的执行环境和一个活动(可变)对象。这个新执行环境的作用域就由新的活动对象后跟外部函数对象的 [[scope]] 属性所引用的作用域链(只有全局对象)构成。在新执行环境的变量实例化过程中,会创建一个与内部函数声明对应的函数对象,而同时会给这个函数对象的 [[scope]] 属性指定创建该函数对象的执行环境(即新执行环境。译者注)的作用域值--即一个包含活动对象后跟全局对象的作用域链。

到目前为止,所有过程都是自动、或者由源代码的结构所控制的。但我们发现,执行环境的作用域链定义了执行环境所创建的函数对象的 [[scope]] 属性,而函数对象的 [[scope]] 属性则定义了它的执行环境的作用域(包括相应的活动对象)。不过,ECMAScript 也提供了用于修改作用域链 with 语句。

with 语句会计算一个表达式,如果该表达式是一个对象,那么就将这个对象添加到当前执行环境的作用域链中(在活动<可变>对象之前)。然后,执行 with 语句(它自身也可能是一个语句块)中的其他语句。之后,又恢复到它之前的执行环境的作用域链中。

with 语句不会影响在变量实例化过程中根据函数声明创建函数对象。但是,可以在一个 with 语句内部对函数表达式求值:-

/* 创建全局变量 - y - 它引用一个对象:- */ var y = {x:5}; // 带有一个属性 - x - 的对象直接量 function exampleFuncWith(){ var z; /* 将全局对象 - y - 引用的对象添加到作用域链的前端:- */ with(y){ /* 对函数表达式求值,以创建函数对象并将该函数对象的引用指定给局部变量 - z - :- */ z = function(){ ... // 内部函数表达式中的代码; } } ... } /* 执行 - exampleFuncWith - 函数:- */

exampleFuncWith();在调用 exampleFuncWith 函数时创建的执行环境中包含一个由其活动对象后跟全局对象构成的作用域链。而在执行 with 语句时,又会把全局变量 y 引用的对象添加到这个作用域链的前端。在对其中的函数表达式求值的过程中,所创建函数对象的 [[scope]] 属性与创建它的执行环境的作用域保持一致--即,该属性会引用一个由对象 y 后跟来自对外部函数调用时所创建执行环境的活动对象,后跟全局对象的作用域链。当与 with 语句相关的语句块执行结束时,执行环境的作用域得以恢复(y 会被移除),但是已经创建的函数对象(z。译者注)的 [[scope]] 属性所引用的作用域链中位于最前面的仍然是对象 y

标识符解析

返回目录

标识符是沿作用域链逆向解析的。ECMA 262 将 this 归类为关键字而不是标识符,是不合理的。因为解析 this 值时始终要根据使用它的执行环境来判断,而与作用域链无关。

标识符解析的过程从作用域链中的第一个对象开始。检查该对象中是否包含与标识符对应的属性名。因为作用域链是一条对象链,所以这个检查过程中也会包含相应对象的原型链(如果有)。如果没有在作用域链的第一个对象中发现相应的值,解析过程会继续搜索下一个对象。这样依次类推直至找到作用域链中包含以标识符为属性名的对象为止,也有可能在作用域链的所有对象中都没有发现该标识符。

当在对象中使用如上所述的属性访问器时,也会发生同样的标识符解析操作。当属性访问器中有相应的属性替换对象时,该对象在作用域链中被确定,而标识符则充当该对象的属性名。全局对象始终都位于作用域链的尾端。

因为与函数调用相关的执行环境将会把活动(可变)对象添加到链的前端,所以在函数体内使用的标识符会首先检查自己是否与形式参数、内部函数声明的名称或局部变量一致。这些都可以由活动(可变)对象的命名属性来确定。

闭包

自动垃圾收集

返回目录

ECMAScript 要求使用自动垃圾收集机制。但规范中并没有详细说明相关的细节,而是留给了实现来决定。但据了解,相当一些实现对它们的垃圾收集操作只赋予了很低的优先级。但是,大致的思想都是相同的,即如果对象不再“可引用(由于不存在对它的引用,使执行代码无法再访问到它)”时,该对象就成为垃圾收集的目标。因而,在将来的某个时刻会将这个对象销毁并将它所占用的一切资源释放,以便操作系统重新利用。

正常情况下,当退出一个执行环境时就会满足类似的条件。此时,作用域链结构中的活动(可变)对象以及在该执行环境中创建的任何对象--包括函数对象,都不再“可引用”,因此将成为垃圾收集的目标。

构成闭包

返回目录

闭包是通过在对一个函数调用的执行环境中返回一个函数对象而构成的。比如,在对函数调用的过程中,将一个对内部函数对象的引用指定给另一个对象的属性。或者,直接将这样一个(内部)函数对象的引用指定给一个全局变量、或者一个全局性对象的属性,或者一个作为参数以引用方式传递给外部函数的对象。例如:-

function exampleClosureForm(arg1, arg2){ var localVar = 8; function exampleReturned(innerArg){ return ((arg1 + arg2)/(innerArg + localVar)); } /* 返回一个定义为 exampleReturned 的内部函数的引用 -:- */ return exampleReturned; } var globalVar = exampleClosureForm(2, 4);

这种情况下,在调用外部函数 exampleClosureForm 的执行环境中所创建的函数对象就不会被当作垃圾收集,因为该函数对象被一个全局变量所引用,而且仍然是可以访问的,甚至可以通过 globalVar(n) 来执行。

的确,情况比正常的时候要复杂一些。因为现在这个被变量 globalVar 引用的内部函数对象的 [[scope]] 属性所引用的作用域链中,包含着属于创建该内部函数对象的执行环境的活动对象(和全局对象)。由于在执行被 globalVar 引用的函数对象时,每次都要把该函数对象的 [[scope]] 属性所引用的整个作用域链添加到创建的(内部函数的)执行环境的作用域中(即此时的作用域中包括:内部执行环境的活动对象、外部执行环境的活动对象、全局对象。译者注), 所以这个(外部执行环境的)活动对象不会被当作垃圾收集。

闭包因此而构成。此时,内部函数对象拥有自由的变量,而位于该函数作用域链中的活动(可变)对象则成为与变量绑定的环境。

由于活动(可变)对象受限于内部函数对象(现在被 globalVar 变量引用)的 [[scope]] 属性中作用域链的引用,所以活动对象连同它的变量声明--即属性的值,都会被保留。而在对内部函数调用的执行环境中进行作用域解析时,将会把与活动(可变)对象的命名属性一致的标识符作为该对象的属性来解析。活动对象的这些属性值即使是在创建它的执行环境退出后,仍然可以被读取和设置。

在上面的例子中,当外部函数返回(退出它的执行环境)时,其活动(可变)对象的变量声明中记录了形式参数、内部函数定义以及局部变量的值。arg1 属性的值为 2,而 arg2 属性的值为 4localVar 的值是 8,还有一个 exampleReturned 属性,它引用由外部函数返回的内部函数对象。(为方便起见,我们将在后面的讨论中,称这个活动<可变>对象为 “ActOuter1″)。

如果再次调用 exampleClosureForm 函数,如:-

var secondGlobalVar = exampleClosureForm(12, 3);

- 则会创建一个新的执行环境和一个新的活动对象。而且,会返回一个新的函数对象,该函数对象的 [[scope]] 属性引用的作用域链与前一次不同,因为这一次的作用域链中包含着第二个执行环境的活动对象,而这个活动对象的属性 arg1 值为 12 而属性 arg2 值为 3。(为方便起见,我们将在后面的讨论中,称这个活动<可变>对象为 “ActOuter2″)。

通过第二次执行 exampleClosureForm 函数,第二个也是截然不同的闭包诞生了。

通过执行 exampleClosureForm 创建的两个函数对象分别被指定给了全局变量 globalVarsecondGlobalVar,并返回了表达式 ((arg1 + arg2)/(innerArg + localVar))。该表达式对其中的四个标识符应用了不同的操作符。如何确定这些标识符的值是体现闭包价值的关键所在。

我们来看一看,在执行由 globalVar 引用的函数对象--如 globalVar(2)--时的情形。此时,会创建一个新的执行环境和相应的活动对象(我们将称之为“ActInner1”),并把该活动对象添加到执行的函数对象的 [[scope]] 属性所引用的作用域链的前端。ActInner1 会带有一个属性 innerArg,根据传递的形式参数,其值被指定为 2。这个新执行环境的作用域链变是: ActInner1->ActOuter1->global object.

为了返回表达式 ((arg1 + arg2)/(innerArg + localVar)) 的值,要沿着作用域链进行标识符解析。表达式中标识符的值将通过依次查找作用域链中的每个对象(与标识符名称一致)的属性来确定。

作用域链中的第一个对象是 ActInner1,它有一个名为 innerArg 的属性,值是 2。所有其他三个标识符在 ActOuter1 中都有对应的属性:arg12arg2
4localVar8。最后,函数调用返回 ((2 + 2)/(2 + 8))

现在再来看一看由 secondGlobalVar 引用的同一个函数对象的执行情况,比如 secondGlobalVar(5)。我们把这次新执行环境的活动对象称为 “ActInner2”,则作用域链就变成了:ActInner2->ActOuter2->global object。ActInner2 返回 innerArg 的值 5,而 ActOuter2 分别返回 arg1arg2localVar 的值 1238。函数调用返回的值就是 ((12 + 3)/(5 + 8))

如果再执行一次 secondGlobalVar,则又会有一个新活动对象被添加到作用域链的前端,但 ActOuter2 仍然是链中的第二个对象,而他的命名属性会再次用于完成标识符 arg1arg2localVar 的解析。

这就是 ECMAScript 的内部函数获取、维持和访问创建他们的执行环境的形式参数、声明的内部函数以及局部变量的过程。这个过程说明了构成闭包以后,内部的函数对象在其存续过程中,如何维持对这些值的引用、如何对这些值进行读取的机制。即,来自创建内部函数对象的执行环境的活动(可变)对象,会保留在该函数对象的 [[scope]] 属性所引用的作用域链中。直到所有对这个内部函数的引用被释放,这个函数对象才会成为垃圾收集的目标(连同它的作用域链中任何不再需要的对象)。

内部函数自身也可能有内部函数。在通过函数执行返回内部函数构成闭包以后,相应的闭包自身也可能会返回内部函数从而构成它们自己的闭包。每次作用域链嵌套,都会增加由创建内部函数对象的执行环境引发的新活动对象。ECMAScript 规范要求作用域链是临时性的,但对作用域链的长度却没有加以限制。在具体实现中,可能会存在实际的限制,但还没有发现有具体数量的报告。目前来看,嵌套的内部函数所拥有的潜能,仍然超出了使用它们的人的想像能力。

通过闭包可以做什么?

返回目录

Strangely the answer to that appears to be anything and everything.
I am told that closures enable ECMAScript to emulate anything, so the
limitation is the ability to conceive and implement the emulation. That
is a bit esoteric and it is probably better to start with something a
little more practical.

例 1:为函数引用设置延时

返回目录

A common use for a closure is to provide parameters for the execution
of a function prior to the execution of that function. For example,
when a function is to be provided as the first argument to the
setTimout function that is common in web browser
environments.

setTimeout schedules the execution of a function (or a
string of javascript source code, but not in this context), provided as
its first argument, after an interval expressed in milliseconds (as its
second argument). If a piece of code wants to use
setTimeout it calls the setTimeout function
and passes a reference to a function object as the first argument and
the millisecond interval as the second, but a reference to a function
object cannot provide parameters for the scheduled execution of that
function.

However, code could call another function that returned a reference to
an inner function object, with that inner function object being passed
by reference to the setTimeout function. The parameters to
be used for the execution of the inner function are passed with the
call to the function that returns it. setTimout executes
the inner function without passing arguments but that inner function
can still access the parameters provided by the call to the outer
function that returned it:-

function callLater(paramA, paramB, paramC){ /* Return a reference to an anonymous inner function created with a function expression:- */ return (function(){ /* This inner function is to be executed with - setTimeout - and when it is executed it can read, and act upon, the parameters passed to the outer function:- */ paramA[paramB] = paramC; }); } ... /* Call the function that will return a reference to the inner function object created in its execution context. Passing the parameters that the inner function will use when it is eventually executed as arguments to the outer function. The returned reference to the inner function object is assigned to a local variable:- */ var functRef = callLater(elStyle, "display", "none"); /* Call the setTimeout function, passing the reference to the inner function assigned to the - functRef - variable as the first argument:- */ hideMenu=setTimeout(functRef, 500);

例 2: 通过对象实例方法关联函数

返回目录

There are many other circumstances when a reference to a function
object is assigned so that it would be executed at some future time
where it is useful to provide parameters for the execution of that
function that would not be easily available at the time of execution
but cannot be known until the moment of assignment.

One example might be a javascript object that is designed to
encapsulate the interactions with a particular DOM element. It has
doOnClick, doMouseOver and
doMouseOut methods and wants to execute those methods
when the corresponding events are triggered on the DOM element, but
there may be any number of instances of the javascript object created
associated with different DOM elements and the individual object
instances do not know how they will be employed by the code that
instantiated them. The object instances do not know how to reference
themselves globally because they do not know which global variables
(if any) will be assigned references to their instances.

So the problem is to execute an event handling function that has an
association with a particular instance of the javascript object, and
knows which method of that object to call.

The following example uses a small generalised closure based function
that associates object instances with element event handlers.
Arranging that the execution of the event handler calls the specified
method of the object instance, passing the event object and a reference
to the associated element on to the object method and returning the
method’s return value.

/* A general function that associates an object instance with an event handler. The returned inner function is used as the event handler. The object instance is passed as the - obj - parameter and the name of the method that is to be called on that object is passed as the - methodName - (string) parameter. */ function associateObjWithEvent(obj, methodName){ /* The returned inner function is intended to act as an event handler for a DOM element:- */ return (function(e){ /* The event object that will have been parsed as the - e - parameter on DOM standard browsers is normalised to the IE event object if it has not been passed as an argument to the event handling inner function:- */ e = e||window.event; /* The event handler calls a method of the object - obj - with the name held in the string - methodName - passing the now normalised event object and a reference to the element to which the event handler has been assigned using the - this - (which works because the inner function is executed as a method of that element because it has been assigned as an event handler):- */ return obj[methodName](e, this); }); } /* This constructor function creates objects that associates themselves with DOM elements whose IDs are passed to the constructor as a string. The object instances want to arrange than when the corresponding element triggers onclick, onmouseover and onmouseout events corresponding methods are called on their object instance. */ function DhtmlObject(elementId){ /* A function is called that retrieves a reference to the DOM element (or null if it cannot be found) with the ID of the required element passed as its argument. The returned value is assigned to the local variable - el -:- */ var el = getElementWithId(elementId); /* The value of - el - is internally type-converted to boolean for the - if - statement so that if it refers to an object the result will be true, and if it is null the result false. So that the following block is only executed if the - el - variable refers to a DOM element:- */ if(el){ /* To assign a function as the element's event handler this object calls the - associateObjWithEvent - function specifying itself (with the - this - keyword) as the object on which a method is to be called and providing the name of the method that is to be called. The - associateObjWithEvent - function will return a reference to an inner function that is assigned to the event handler of the DOM element. That inner function will call the required method on the javascript object when it is executed in response to events:- */ el.onclick = associateObjWithEvent(this, "doOnClick"); el.onmouseover = associateObjWithEvent(this, "doMouseOver"); el.onmouseout = associateObjWithEvent(this, "doMouseOut"); ... } } DhtmlObject.prototype.doOnClick = function(event, element){ ... // doOnClick method body. } DhtmlObject.prototype.doMouseOver = function(event, element){ ... // doMouseOver method body. } DhtmlObject.prototype.doMouseOut = function(event, element){ ... // doMouseOut method body. }

And so any instances of the DhtmlObject can associate themselves
with the DOM element that they are interested in without any need
to know anything about how they are being employed by other code,
impacting on the global namespace or risking clashes with other
instances of the DhtmlObject.

例 3:包装相关的功能

返回目录

Closures can be used to create additional scopes that can be used to
group interrelated and dependent code in a way that minimises the risk
of accidental interaction. Suppose a function is to build a string and
to avoid the repeated concatenation operations (and the creation of
numerous intermediate strings) the desire is to use an array to store
the parts of the string in sequence and then output the results using
the Array.prototype.join method (with an empty string as its argument).
The array is going to act as a buffer for the output, but defining it
locally to the function will result in its re-creation on each
execution of the function, which may not be necessary if the only
variable content of that array will be re-assigned on each function
call.

One approach might make the array a global variable so that it can be
re-used without being re-created. But the consequences of that will be
that, in addition to the global variable that refers to the function
that will use the buffer array, there will be a second global property
that refers to the array itself. The effect is to render the code less
manageable, as, if it is to be used elsewhere, its author has to remember
to include both the function definition and the array definition. It
also makes the code less easy to integrate with other code because
instead of just ensuring that the function name is unique within the
global namespace it is necessary to ensure that the Array on which it
is dependent is using a name that is unique within the global
namespace.

A Closure allows the buffer array to be associated (and neatly
packaged) with the function that is dependent upon it and
simultaneously keep the property name to which the buffer array as
assigned out of the global namespace and free of the risk of name
conflicts and accidental interactions.

The trick here is to create one additional execution context by
executing a function expression in-line and have that function
expression return an inner function that will be the function that is
used by external code. The buffer array is then defined as a local
variable of the function expression that is executed in-line. That only
happens once so the Array is only created once, but is available to
the function that depends on it for repeated use.

The following code creates a function that will return a string of
HTML, much of which is constant, but those constant character
sequences need to be interspersed with variable information provided
as parameter to the function call.

A reference to an inner function object is returned from the in-line
execution of a function expression and assigned to a global variable
so that it can be called as a global function. The buffer array is
defined as a local variable in the outer function expression. It is
not exposed in the global namespace and does not need to be re-created
whenever the function that uses it is called.

/* A global variable - getImgInPositionedDivHtml - is declared and assigned the value of an inner function expression returned from a one-time call to an outer function expression. That inner function returns a string of HTML that represents an absolutely positioned DIV wrapped round an IMG element, such that all of the variable attribute values are provided as parameters to the function call:- */ var getImgInPositionedDivHtml = (function(){ /* The - buffAr - Array is assigned to a local variable of the outer function expression. It is only created once and that one instance of the array is available to the inner function so that it can be used on each execution of that inner function. Empty strings are used as placeholders for the date that is to be inserted into the Array by the inner function:- */ var buffAr = [ '<div id="', '', //index 1, DIV ID attribute '" style="position:absolute;top:', '', //index 3, DIV top position 'px;left:', '', //index 5, DIV left position 'px;width:', '', //index 7, DIV width 'px;height:', '', //index 9, DIV height 'px;overflow:hidden;\"><img src=\"', '', //index 11, IMG URL '\" width=\"', '', //index 13, IMG width '\" height=\"', '', //index 15, IMG height '\" alt=\"', '', //index 17, IMG alt text '\"></div>' ]; /* Return the inner function object that is the result of the evaluation of a function expression. It is this inner function object that will be executed on each call to - getImgInPositionedDivHtml( ... ) -:- */ return (function(url, id, width, height, top, left, altText){ /* Assign the various parameters to the corresponding locations in the buffer array:- */ buffAr[1] = id; buffAr[3] = top; buffAr[5] = left; buffAr[13] = (buffAr[7] = width); buffAr[15] = (buffAr[9] = height); buffAr[11] = url; buffAr[17] = altText; /* Return the string created by joining each element in the array using an empty string (which is the same as just joining the elements together):- */ return buffAr.join(''); }); //:End of inner function expression. })(); /*^^- :The inline execution of the outer function expression. */

If one function was dependent on one (or several) other functions, but
those other functions were not expected to be directly employed by any
other code, then the same technique could be used to group those
functions with the one that was to be publicly exposed. Making a
complex multi-function process into an easily portable and encapsulated
unit of code.

其他例子

返回目录

Probably one of the best known applications of closures is
Douglas
Crockford’s technique for the emulation of private instance variables
in ECMAScript objects
. Which can be extended to all sorts of
structures of scope contained nested accessibility/visibility, including

the emulation of private static members for ECMAScript objects
.

The possible application of closures are endless, understanding how
they work is probably the best guide to realising how they can be
used.

意外的闭包

返回目录

Rendering any inner function accessible outside of the body of the
function in which it was created will form a closure. That makes
closures very easy to create and one of the consequences is that
javascript authors who do not appreciate closures as a language feature
can observe the use of inner functions for various tasks and employ
inner functions, with no apparent consequences, not realising that
closures are being created or what the implications of doing that are.

Accidentally creating closures can have harmful side effects as the
following section on the IE memory leak problem describes, but they can
also impact of the efficiency of code. It is not the closures
themselves, indeed carefully used they can contribute significantly
towards the creation of efficient code. It is the use of inner
functions that can impact on efficiency.

A common situation is where inner functions are used is as event
handlers for DOM elements. For example the following code might be used
to add an onclick handler to a link element:-

/* Define the global variable that is to have its value added to the - href - of a link as a query string by the following function:- */ var quantaty = 5; /* When a link passed to this function (as the argument to the function call - linkRef -) an onclick event handler is added to the link that will add the value of a global variable - quantaty - to the - href - of that link as a query string, then return true so that the link will navigate to the resource specified by the - href - which will by then include the assigned query string:- */ function addGlobalQueryOnClick(linkRef){ /* If the - linkRef - parameter can be type converted to true (which it will if it refers to an object):- */ if(linkRef){ /* Evaluate a function expression and assign a reference to the function object that is created by the evaluation of the function expression to the onclick handler of the link element:- */ linkRef.onclick = function(){ /* This inner function expression adds the query string to the - href - of the element to which it is attached as an event handler:- */ this.href += ('?quantaty='+escape(quantaty)); return true; }; } }

Whenever the addGlobalQueryOnClick function is called a
new inner function is created (and a closure formed by its assignment).
From the efficiency point of view that would not be significant if the
addGlobalQueryOnClick function was only called once or
twice, but if the function was heavily employed many distinct function
objects would be created (one for each evaluation of the inner function
expression).

The above code is not taking advantage of the fact that inner functions
are becoming accessible outside of the function in which they are being
created (or the resulting closures). As a result exactly the same effect
could be achieved by defining the function that is to be used as the
event handler separately and then assigning a reference to that
function to the event handling property. Only one function object would
be created and all of the elements that use that event handler would
share a reference to that one function:-

/* Define the global variable that is to have its value added to the - href - of a link as a query string by the following function:- */ var quantaty = 5; /* When a link passed to this function (as the argument to the function call - linkRef -) an onclick event handler is added to the link that will add the value of a global variable - quantaty - to the - href - of that link as a query string, then return true so that the link will navigate to the resource specified by the - href - which will by then include the assigned query string:- */ function addGlobalQueryOnClick(linkRef){ /* If the - linkRef - parameter can be type converted to true (which it will if it refers to an object):- */ if(linkRef){ /* Assign a reference to a global function to the event handling property of the link so that it becomes the element's event handler:- */ linkRef.onclick = forAddQueryOnClick; } } /* A global function declaration for a function that is intended to act as an event handler for a link element, adding the value of a global variable to the - href - of an element as an event handler:- */ function forAddQueryOnClick(){ this.href += ('?quantaty='+escape(quantaty)); return true; }

As the inner function in the first version is not being used to exploit
the closures produced by its use, it would be more efficient not to use
an inner function, and thus not repeat the process of creating many
essentially identical function objects.

A similar consideration applies to object constructor functions. It is
not uncommon to see code similar to the following skeleton constructor:-

function ExampleConst(param){ /* Create methods of the object by evaluating function expressions and assigning references to the resulting function objects to the properties of the object being created:- */ this.method1 = function(){ ... // method body. }; this.method2 = function(){ ... // method body. }; this.method3 = function(){ ... // method body. }; /* Assign the constructor's parameter to a property of the object:- */ this.publicProp = param; }

Each time the constructor is used to create an object, with
new ExampleConst(n), a new set of function objects are
created to act as its methods. So the more object instances that are
created the more function objects are created to go with them.

Douglas Crockford’s technique for emulating private members on
javascript objects exploits the closure resulting form assigning
references to inner function objects to the public properties of a
constructed object from within its constructor. But if the methods of
an object are not taking advantage of the closure that they will form
within the constructor the creation of multiple function objects for
each object instantiation will make the instantiation process slower
and more resources will be consumed to accommodate the extra function
objects created.

In that case it would be more efficient to create the function object
once and assign references to them to the corresponding properties of
the constructor’s prototype so they may be shared by all
of the objects created with that constructor:-

function ExampleConst(param){ /* Assign the constructor's parameter to a property of the object:- */ this.publicProp = param; } /* Create methods for the objects by evaluating function expressions and assigning references to the resulting function objects to the properties of the constructor's prototype:- */ ExampleConst.prototype.method1 = function(){ ... // method body. }; ExampleConst.prototype.method2 = function(){ ... // method body. }; ExampleConst.prototype.method3 = function(){ ... // method body. };

Internet Explorer 的内存泄漏问题

返回目录

The Internet Explorer web browser (verified on versions 4 to 6 (6 is
current at the time of writing)) has a fault in its garbage collection
system that prevents it from garbage collecting ECMAScript and some
host objects if those host objects form part of a “circular”
reference. The host objects in question are any DOM Nodes (including
the document object and its descendants) and ActiveX objects. If a
circular reference is formed including one or more of them, then
none of the objects involved will be freed until the browser is closed
down, and the memory that they consume will be unavailable to the
system until that happens.

A circular reference is when two or more objects refer to each other in
a way that can be followed and lead back to the starting point. Such
as object 1 has a property that refers to object 2, object 2 has a
property that refers to object 3 and object 3 has a property that
refers back to object 1. With pure ECMAScript objects as soon as no
other objects refer to any of objects 1, 2 or 3 the fact that they only
refer to each other is recognised and they are made available for
garbage collection. But on Internet Explorer, if any of those objects
happen to be a DOM Node or ActiveX object, the garbage collection
cannot see that the circular relationship between them is isolated
from the rest of the system and free them. Instead they all stay in
memory until the browser is closed.

Closures are extremely good at forming circular references. If a
function object that forms a closure is assigned as, for example, and
event handler on a DOM Node, and a reference to that Node is assigned
to one of the Activation/Variable objects in its scope chain then a
circular reference exists.
DOM_Node.onevent ->function_object.[[scope]] ->scope_chain ->Activation_object.nodeRef ->DOM_Node.

It is very easy to do, and a bit of browsing around a site that forms
such a reference in a piece of code common to each page can consume
most of the systems memory (possibly all).

Care can be taken to avoid forming circular references and remedial
action can be taken when they cannot otherwise be avoided, such as
using IE’s onunload event to null event handling function
references. Recognising the problem and understanding closures
(and their mechanism) is the key to avoiding this problem with IE.

comp.lang.javascript FAQ notes T.O.C.

  • 撰稿 Richard Cornford,2004 年 3 月.
  • 修改建议来自: 
    • Martin Honnen.
    • Yann-Erwan Perio (Yep).
    • Lasse Reichstein Nielsen. (definition of closure)
    • Mike Scirocco.
    • Dr John Stockton.

推荐两本2008年可能走红的技术书

第一本:《Prototype 和 script.aculo.us》
你不曾知道的 JavaScript 世界!
 http://www.pragmaticprogrammer.com/titles/cppsu/index.html
 
作者:Christophe Porteneuve
页数: 330
ISBN: 1-934356-01-8
日期: 2007年10月
 
Prototype and script.aculo.us
在 Web 2.0 时代的 JavaScript 编程中,还没有因为纠缠不清的跨浏览器细节而感到厌烦吗?投入 Prototype 和 script.aculo.us 的怀抱吧,这两个极其流行的 JavaScript 类库,会让你的 JavaScript 编程体验胜似闲庭信步。不仅对 AJAX 编程的支持独树一帜,而且在拖放、自动完成、高级视觉效果和其他极其有用的特性方面也令人不禁拍案叫绝。通过使用这两个库,你要做的事只剩写一两行脚本,而且这些脚本看起来比 Ruby 的代码有过之而不无及!

关于本书
Web 应用程序的种类越来越丰富,而且日益具有了更高的交互性。但是,JavaScript、DOM、CSS 和一大堆的 Web 标准虽然相当复杂,却仍然不能保证结果被浏览器兼容。

而 Prototype 和 script.aculo.us 类库这两个有待发掘的宝库,却能够轻松地抚平浏览器间的种种差异,使得实现最常用的功能变成了小事一桩。通过学习本书,你可以迅速地掌握如何发挥这两个卓而不群的类库的强大威力。

深入 Prototype 中,你会发现,这个类库把 JavaScript 武装得如此强大,甚至会有人误以为是 Ruby 的代码。遍历 DOM、处理事件、运筹 AJAX 以及从根本上简化大多数的脚本编写量--这些在 Prototype 中都成为了一件轻而易举的事。

在开发基于高级界面功能的应用时,script.aculo.us 使得每一位 Web 开发者得以梦想成真--无论是需要创建能够自动完成的文本输入框,还是实现适当的文本编辑器,还是提供自定义的拖放功能,或者是通过视觉效果吸引用户的眼球,亦或是提高构建 DOM 片段的效率,应有尽有--而且也都是通过轻量级代码实现。

本书会引导你深入研究这些功能的全部细节,通过融合不同的服务器端技术,如 PHP、vanilla Ruby 和 Ruby On Rails,运用大量的实例来示范这些功能的方方面面。有能力的用户还可以学习到这些类库的设计原理,以及如何根据自己的需要发布并扩展这些类库。
第二本:《Prototype 和 Scriptaculous实战》
http://www.manning.com/crane3/

Dave Crane and Bear Bibeault with Tom Locke
出版日期:2007年3月
页数:544
ISBN: 1-933988-03-7

本书简介

Common Ajax tasks should be easy, and with Prototype and Scriptaculous they are. Prototype and Scriptaculous are libraries of reusable JavaScript code that simplify Ajax development. Prototype provides helpful methods and objects that extend JavaScript in a safe, consistent way. Its clever Ajax request model simplifies cross-browser development. Scriptaculous, which is based on Prototype, offers handy pre-fabricated widgets for rich UI development.

Prototype and Scriptaculous in Action is a comprehensive, practical guide that walks you feature-by-feature through the two libraries. First, you抣l use Scriptaculous to make easy but powerful UI improvements. Then you抣l dig into Prototype抯 elegant and sparse syntax. See how a few characters of Prototype code can save a dozen lines of JavaScript. By applying these techniques, you can concentrate on the function and flow of your application instead of the coding details. This book is written for web developers with a working knowledge of JavaScript.

《精通正则表达式》书评

作者:孟岩(原文) 

IT产业新技术日新月异,令人目不暇接,然而在这其中,真正称得上伟大东西的却寥寥无几。1998年,被誉为“软件世界的爱迪生”,发明了 BSD、 TCP/IP、csh、vi 和 NFS 的 SUN 首席科学家 Bill Joy 曾经不无调侃地说,在计算机体系结构领域里,缓存是唯一称得上伟大的思想,其他的一切发明和技术不过是在不同场景下应用这一思想而已。在计算机软件领域里,情形也大体相似。如果罗列这个领域中的伟大发明,我相信绝不会超过二十项。在这个名单当中,当然应该包括分组交换网络、Web、Lisp、哈希算法、UNIX、编译技术、关系模型、面向对象、XML 这些大名鼎鼎的家伙,而正则表达式也绝对不应该被漏掉。正则表达式具有伟大技术发明的一切特点,它简单,优美,功能强大,妙用无穷。对于很多实际工作来讲,正则表达式简直是灵丹妙药,能够成百倍地提高开发效率和程序质量。CSDN 的创始人蒋涛先生在早年开发专业软件产品时,就曾经体验过这一工具的巨大威力,并且一直印象深刻。而我的一位从事网络编辑工作的朋友,最近也领略了正则表达式的威力——他用 Perl 开发了一个不足 20 行的小程序,使用正则表达式将一项原本每天耗用10个人时的工作在一分钟之内自动完成。而正则表达式在生物信息学和人类基因图谱的研究中所发挥的关键作用,更是被传为佳话。无论对于软件开发者,还是从事其他知识工作的专业人士,正则表达式都是最有利的工具之一。

所谓正则表达式,就是一种描述字符串结构模式的形式化表达方法。在发展的初期,这套方法仅限于描述正则文本,故此得名“正则表达式(regular expression)”。随着正则表达式研究的深入和发展,特别是 Perl 语言的实践和探索,正则表达式的能力已经大大突破了传统的、数学上的限制,成为威力巨大的实用工具,在几乎所有主流语言中获得支持。为什么正则表达式具有如此巨大的魅力?一方面,因为正则表达式处理的对象是字符串,或者抽象地说,是一个对象序列,而这恰恰是当今计算机体系的本质数据结构,我们围绕计算机所做的大多数工作,都归结为在这个序列上的操作,因此,正则表达式用途广阔。另 一方面,与大多数其他技术不同,正则表达式具有超强的结构描述能力,而在计算机中,正是不同的结构把无差别的字节组织成千差万别的软件对象,再组合成为无所不能的软件系统,因此,描述了结构,就等于描述了系统。在这方面,正则表达式的地位是独特的。正因为这两点,在现在的软件开发和日常数据处理工作中,正则表达式已经是必不可少的工具。如果一个开发工具不支持正则表达式,那它就会被视为玩具语言,如果一个编辑器不支持正则表达式,那它就会被称为阳春应用。 连人们原本并不指望应用正则表达式的商用数据库,各家厂商也竞相以支持正则表达式为卖点。正则表达式的声势之隆,是毋庸置疑的。

非常奇怪的是,这样一个了不起的技术,在我国却并没有得到充分推广。以其价值而言,正则表达式不但值得每一个专业程序员掌握,而且值得所有知识工作者去了 解。然而现实情况是,不但一般知识工作者大多闻所未闻,很多专业程序员也视之为畏途。为什么会出现这种情况呢?原因有二。其一,正则表达式产生和发展在 UNIX 文化体系之中,而我国软件开发社群的知识结构长期受到微软的决定,UNIX 文化影响甚微。在 2002 年推出 .NET 平台之前,微软在其各项主流平台、产品与开发工具当中,均未对正则表达式给予足够重视,相应地,我们的开发者们对正则表达式也就知之不多。第二,也是更重要的原因,就是正则表达式并不是那么好掌握的,在通向驾驭正则表达式强大力量的道路上,还是有那么几只拦路虎的,而要打虎过岗,不但要花点功夫,还要有正确的方法。

学习正则表达式,入门不难,看一些例子,试着模仿模仿,就可以粗通,并且在工作中解决不少问题。然而大部分学习者也就就此止步,他们对自己说:“正则表达式不过如此,我就学到这里了,以后现用现学就行了”。他们以为自己可以像学习其他技术一样,在实践中逐渐提高正则表达式的应用水平。然而事实上,正则表达式并不是每天都会用到,而其密码般的形象,随着时间的推移很容易被忘记,所以经常发生的情况是,开发者对于正则表达式的记忆迅速消退,每次遇到新的问题, 都要查资料,重新唤回记忆,对于稍微复杂一点的问题,只好求助于现成的解决方案。反反复复,长期如此,不但应用水平难以明显提升,而且逐渐对这项技术产生一定的恐惧感和厌烦情绪。这还只是应用阶段,正则表达式应用的高级阶段,要求开发者此外还必须充分理解正则表达式的能力范围,能够将一些正则表达式技术组合应用,达成超乎一般想像的效果。为了高效、正确地解决实际问题,有的时候甚至要求深入理解正则表达式的原理,甚至对于如何实现正则表达式引擎都有所了解,在此基础上,规避陷阱,优化设计,提高程序执行效率。要达到这样的程度,不经过系统的学习是不可能的。

系统学习正则表达式并不是一件容易的事情,仅仅通过阅读一些“HOW TO”的快餐式的文章是不行的,必须有更完整、更系统的资料指导学习。如果你在国外技术社区里询问如何才能系统学习正则表达式,几乎所有的领域专家都会向你推荐一本书——Jeffrey Friedl 的《精通正则表达式》,也就是本书。

这本《精通正则表达式》是系统学习正则表达式的唯一最权威著作。可以说,在今天,如果想理解和掌握正则表达式,想要建立关于这一技术的完整概念体系,想充分发挥其巨大能量,这本书几乎是无法绕开的必经之路。甚至可以说,如果你没有读过这本书,那么你对于正则表达式的理解和应用能力一定达不到升堂入室的程度。本书第一版出于十年之前,自那时起就成为正则表达式领域最全面、最受欢迎的代表著作,数以万计的读者通过这本书掌握了正则表达式,成为行家里手。在任何时候,任何地方,只要提到正则表达式著作,人们都会提到这本书。这本书的质量之高,声誉之盛,使得几乎没有人企图挑战它的地位,从而在正则表达式图书领域形成独特的“一夫当关”的局面,称其为正则表达式圣经,绝对当之无愧。

为什么这本书能够表现得如此出色?我认为这其中有三个原因。其一,作者本人具有多年程序开发经验,理论基础深厚,实战经验丰富,对正则表达式这个主题透彻理解,因此在技术上得心应手,底气十足,对于技术上的难点不回避、不含糊。作者高超的技术水平是本书质量的强大保证。其二,作者思路对头,素材组织得当,用例丰富。正则表达式根植于数学理论,却又能在日常俗事上发挥巨大的效用。写这种类型的技术,思路稍微一偏差,就可能走歪路,不是太理论,就是太琐碎,不是太枯燥,就是太浅薄,实在很难把握。作者清楚地认识到,这本书的读者不是计算机科学家,但也不是满足于“知其然而不知其所以然”的快餐式代码小子,而是具有一定理论素养,却又始终以实践为本的专业开发者。他们需要的是面向实践的理论和思想,是实实在在的实战能力,只有满足这种需要,才能够真正打动读者。 通读此书,可以说作者对这一路线的把握十分成功,保证了内容大方向的正确。其三,这本书的写法独具匠心,堪称典范。技术图书的主要使命是传播专业知识。而专业知识分为框架性知识和具体知识。框架性知识需要通过系统的阅读和学习掌握,而大量的具体知识,则主要通过日常工作的积累以及随用随查的的学习来逐渐填充起来。本书前六章,以顺序式记述的方式,将正则表达式的系统知识娓娓道来,读者像看故事书似的就建立起整个正则表达式的基本知识体系。而后面的内容,则是方便实际开发中频繁查阅之用,包括各大主流语言对正则表达式的支持细节,包含有大量案例。这样的写法,完全符合一般人学习的特点,因此书读起来非常惬意,非常有趣,而用的时候查起来又非常方便。这样的著述风格,实在值得学习。

读者可以在没有任何正则表达式的基础上开始阅读此书,只要勤动脑,加强理解,适当动手练习,将能够在不长的时间里掌握正则表达式的思想和技术精华,这一点已经被很多人验证过,我本人也是这本书的受益者之一。正因为这本书独一无二的地位和高度的可读性,也因为正则表达式作为一项了不起的技术发明所具有的巨大威力,我非常希望更多的读者能够通过认真地学习本书而掌握这一强大技术,并享受阅读的乐趣。

正则表达式的敏感性和特殊性

敏感性和特殊性这两个概念来源于像统计学和流行病学这样的学科中的量化标准。宽泛地说,敏感性可以用实际找到的匹配项中的正确匹配项数除以在匹配全部相关字符序列的情况下应该找到的匹配项数来度量。而特殊性则可以用实际找到的匹配项中的正确匹配项数除以找到的匹配项总数来表示。在使用正则表达式时,敏感性越高,则表明找到的真正匹配项数量越接近要找的全部匹配项;而特殊性越高,则表明找到的匹配项中正确的匹配项越多。

匹分敏感性和特殊性的关键是要明确三个数量:正确的找到的要找的。根据上面的定义,敏感性是指用正确的除以要找的,这个比例越高说明正则表达式越敏感--比如要找 100 个,实际找到了 150 个,而其中有 100 个是正确的,那么敏感性就是 100%;而特殊性则是指用正确的除以找到的,这个比例越高说明正则表达式越特殊<即更具有针对性>--比如上面说找到的 150 个当中有 100 个是正确的,那么特殊性就是 66.67%。所以可以理解为敏感性是从量的角度衡量匹配目标的完成情况,而特殊性则是从质的角度来衡量匹配目标的完成情况。

试验一下:电子邮件地址

1. 打开 PowerGrep,在 Search 文本区域中输入模式 \w*(?<=\w)\.?\w+@.*

2. Folder 文本框中输入文件夹名 C:\BRegExp\Ch09 。如果你把下载的测试文件放在其他地方,请修改这里的路径。

3. File mask 文本框中输入文件名 emailornotemail.txt,并单击 Search 按钮。

4. Results 区域中观察结果,如下图所示。

email_1.gif

这是一次进步,而模式也更加具有特殊性。因为这次没有匹配第 127 8 行中不想要的字符序列。然而,第 3 行中的字符序列 John@somewhere.invalid,并不是一个有效的电子邮件地址。

可以通过将电子邮件地址的域名部分更加特殊化来排除这个不想要的匹配项。众所周知,所有的域名都是字母字符序列后跟一个句点字符,然后跟着三(comnetorg biz)或四(info)个字母字符。在本例中,我们先不考虑 example.co.uk 这样的域名。那么,下面的模式可以作为与刚刚描述的结构对应的一个适当的模式:

\w+\.\w{3,4}

其中 \w+ 甚至会匹配单个字符的域名(.com.net .org 域名允许使用单个字符)。而 \. 转义序列匹配一个句点字符,\w{3,4} 则匹配三或四个字母字符。

将这个模式与前面使用的模式组合起来就是:

\w*(?<=\w)\.?\w+@\w+\.\w{3,4}

5. Search 文本区域中输入模式 \w*(?<=\w)\.?\w+@\w+\.\w{3,4},并单击 Search 按钮。

6. 观察结果。注意第 3 行中不想要的匹配项这次没有匹配。然而,在第 6 行中一个前面没有提及的问题却浮出水面。第 6 行中的电子邮件地址中包含两个 @ 字符,而这是不允许的。

要解决这个问题一种方法是使用向前查找限定在匹配第一个 @ 字符后,不允许后面出现另一个 @ 字符。如果我们继续假设在电子邮件地址中只允许使用字母字符,那么就可以通过向前查找来限定在第一次匹配非字母字符或句点字符之前只有一个 @ 字符。

可以通过下面的模式实现这种限定:

\w*(?<=\w)\.?\w+@(?=[\w\.]+\W)\w+\.\w{3,4}

7. Search 文本区域中把模式修改为 \w*(?<=\w)\.?\w+@(?=[\w\.]+\W)\w+\.\w{3,4},并单击 Search 按钮。

8. 观察结果。结果如下图所示。

email_2.gif

遗憾的是,向前查找并没有解决第 3 行和第 6 行中不想要匹配的问题。我们还需要限定模式要匹配的是一行中的全部文本。换句话说,要添加 ^ 元字符指定一行的开始位置和 $ 元字符指定该行的结束位置。

9. Search 文本区域中把模式修改为 ^\w*(?<=\w)\.?\w+@(?=[\w\.]+\W)\w+\.\w{3,4}$,并单击 Search 按钮。

10. 观察结果。结果如下图所示。

email_3.gif

令人高兴的是,这次成功地排除了第 3 行和第 6 行中不想要的匹配项。至少在这个简单的测试数据文件中,我们获得了 100% 的敏感性和 100% 特殊性。

敏感性和特殊性这两个概念来源于量化研究的科学,比如统计学和流行病学。在这两种学科中,都是用数字来表示敏感性和特殊性的,而且通常使用百分比。因此,对于前面的例子来说,因为使用第一个正则表达式模式时找到了所有正确的电子邮件地址,所以敏感性达到了 100%;而因为 10 个匹配结果中的 6 个(从不是有效的电子邮件地址的角度来说)都是错误的匹配,所以特殊性仅有 40%。而到了该例子的最后,修改后正则表达式的特殊性也上升到了 100%

Perl 中的内置变量

这篇文章是从网络中转载的,据说是来自“来源:黑客基地 作者:黑客基地”。如果您是原作者,请联系本站(lsf@cn-cuckoo.com)以便更正,谢谢!

$- 当前页可打印的行数,属于Perl格式系统的一部分
$! 根据上下文内容返回错误号或者错误串
$” 列表分隔符
$# 打印数字时默认的数字输出格式
$ Perl解释器的进程ID
$% 当前输出通道的当前页号
$Content$amp; 与上个格式匹配的字符串
$( 当前进程的组ID
$) 当前进程的有效组ID
$* 设置1表示处理多行格式.现在多以/s和/m修饰符取代之
$, 当前输出字段分隔符
$. 上次阅读的文件的当前输入行号
$/ 当前输入记录分隔符,默认情况是新行
$: 字符设置,此后的字符串将被分开,以填充连续的字段
$; 在仿真多维数组时使用的分隔符
$? 返回上一个外部命令的状态
$@ Perl解释器从eval语句返回的错误消息
$[ 数组中第一个元素的索引号
$\ 当前输出记录的分隔符
$] Perl解释器的子版本号
$^ 当前通道最上面的页面输出格式名字
$^A 打印前用于保存格式化数据的变量
$^D调试标志的值
$^E在非UNIX环境中的操作系统扩展错误信息
$^F最大的文件捆述符数值
$^H由编译器激活的语法检查状态
$^I内置控制编辑器的值
$^L发送到输出通道的走纸换页符
$^M备用内存池的大小
$^O操作系统名
$^P指定当前调试值的内部变量
$^R正则表达式块的上次求值结果
$^S当前解释器状态
$^T从新世纪开始算起,脚步本以秒计算的开始运行的时间
$^W警告开关的当前值
$^X Perl二进制可执行代码的名字
$_ 默认的输入/输出和格式匹配空间
$  控制对当前选择的输出文件句柄的缓冲
$~ 当前报告格式的名字

Mastering Regular Expressions,3rd

《Mastering Regular Expressions,3rd》(以下简称《MRE3》)这本书的中译版也要上市了,中文名叫《精通正则表达式》,由电子工业出版社的博文视点推出。而《Beginning Regular Expressions》(以下简称《BRE》)可能还要等四个月以上才能面世。

本文的目的是就这两本“国内首册”正则表达式技术书的引进版作一比较。

首先,来看一下《MRE3》的英文目录(PDF下载):

Preface
1 Introduction to Regular Expressions
    Solving Real Problems
    Regular Expressions as a Language
        The Filename Analogy
        The Language Analogy
    The Regular-Expression Frame of Mind
        If You Have Some Regular-Expression Experience
        Searching Text Files: Egrep
    Egrep Metacharacters
        Start and End of the Line
        Character Classes
        Matching Any Character with Dot
        Alternation
        Ignoring Differences in Capitalization
        Word Boundaries
        In a Nutshell
        Optional Items
        Other Quantifiers: Repetition
        Parentheses and Backreferences
        The Great Escape
    Expanding the Foundation
        Linguistic Diversification
        The Goal of a Regular Expression
        A Few More Examples
        Regular Expression Nomenclature
        Improving on the Status Quo
        Summary
    Personal Glimpses
2 Extended Introductory Examples
    About the Examples
        A Short Introduction to Perl
    Matching Text with Regular Expressions
        Toward a More Real-World Example
        Side Effects of a Successful Match
        Intertwined Regular Expressions
        Inter mission
    Modifying Text with Regular Expressions
        Example: Form Letter
        Example: Prettifying a Stock Price
        Automated Editing
        A Small Mail Utility
        Adding Commas to a Number with Lookaround
        Text-to-HTML Conversion
        That Doubled-Word Thing
3 Over view of Regular Expression Features and Flavors
    A Casual Stroll Across the Regex Landscape
        The Origins of Regular Expressions
        At a Glance
    Care and Handling of Regular Expressions
        Integrated Handling
        Procedural and Object-Oriented Handling
        A Search-and-Replace Example
        Search and Replace in Other Languages
        Care and Handling: Summary
    Strings, Character Encodings, and Modes
        Strings as Regular Expressions
        Character-Encoding Issues
        Unicode
        Regex Modes and Match Modes
    Common Metacharacters and Features
        Character Representations
        Character Classes and Class-Like Constructs
        Anchors and Other “Zero-Width Assertions”
        Comments and Mode Modifiers
        Grouping, Capturing, Conditionals, and Control
    Guide to the Advanced Chapters
4 The Mechanics of Expression Processing
    Start Your Engines!
        Two Kinds of Engines
        New Standards
        Regex Engine Types
        From the Department of Redundancy Department
        Testing the Engine Type
    Match Basics
        About the Examples
        Rule 1: The Match That Begins Earliest Wins
        Engine Pieces and Parts
        Rule 2: The Standard Quantifiers Are Greedy
    Regex-Directed Versus Text-Directed
        NFA Engine: Regex-Directed
        DFA Engine: Text-Dir ected
        First Thoughts: NFA and DFA in Comparison
    Backtracking
        A Really Crummy Analogy
        Two Important Points on Backtracking
        Saved States
        Backtracking and Greediness
    More About Greediness and Backtracking
        Problems of Greediness
        Multi-Character “Quotes”
        Using Lazy Quantifiers
        Greediness and Laziness Always Favor a Match
        The Essence of Greediness, Laziness, and Backtracking
        Possessive Quantifiers and Atomic Grouping
        Possessive Quantifiers, ?+, ++, ++, and {m,n}+
        The Backtracking of Lookaround
        Is Alternation Greedy?
        Taking Advantage of Ordered Alternation
    NFA, DFA, and POSIX
        “The Longest-Leftmost”
        POSIX and the Longest-Leftmost Rule
        Speed and Efficiency
        Summary: NFA and DFA in Comparison
    Summary
5 Practical Regex Techniques
    Regex Balancing Act
    A Few Short Examples
        Continuing with Continuation Lines
        Matching an IP Addr ess
        Working with Filenames
        Matching Balanced Sets of Parentheses
        Watching Out for Unwanted Matches
        Matching Delimited Text
        Knowing Your Data and Making Assumptions
        Stripping Leading and Trailing Whitespace
    HTML-Related Examples
        Matching an HTML Tag
        Matching an HTML Link
        Examining an HTTP URL
        Validating a Hostname
        Plucking Out a URL in the Real World
    Extended Examples
        Keeping in Sync with Your Data
        Parsing CSV Files
6 Crafting an Efficient Expression
    A Sobering Example
        A Simple Change—Placing Your Best Foot Forward
        Efficiency Versus Correctness
        Advancing Further—Localizing the Greediness
        Reality Check
    A Global View of Backtracking
        More Work for a POSIX NFA
        Work Required During a Non-Match
        Being More Specific
        Alternation Can Be Expensive
    Benchmarking
        Know What You’r e Measuring
        Benchmarking with PHP
        Benchmarking with Java
        Benchmarking with VB.NET
        Benchmarking with Ruby
        Benchmarking with Python
        Benchmarking with Tcl
    Common Optimizations
        No Free Lunch
        Everyone’s Lunch is Different
        The Mechanics of Regex Application
        Pre-Application Optimizations
        Optimizations with the Transmission
        Optimizations of the Regex Itself
    Techniques for Faster Expressions
        Common Sense Techniques
        Expose Literal Text
        Expose Anchors
        Lazy Versus Greedy: Be Specific
        Split Into Multiple Regular Expressions
        Mimic Initial-Character Discrimination
        Use Atomic Grouping and Possessive Quantifiers
        Lead the Engine to a Match
    Unrolling the Loop
        Method 1: Building a Regex From Past Experiences
        The Real “Unrolling-the-Loop” Pattern
        Method 2: A Top-Down View
        Method 3: An Internet Hostname
        Observations
        Using Atomic Grouping and Possessive Quantifiers
        Short Unrolling Examples
        Unrolling C Comments
    The Freeflowing Regex
        A Helping Hand to Guide the Match
        A Well-Guided Regex is a Fast Regex
        Wrapup
    In Summary: Think!
7 Perl
    Regular Expressions as a Language Component
        Perl’s Greatest Strength
        Perl’s Greatest Weakness
    Perl’s Regex Flavor
        Regex Operands and Regex Literals
        How Regex Literals Are Parsed
        Regex Modifiers
    Regex-Related Perlisms
        Expression Context
        Dynamic Scope and Regex Match Effects
        Special Variables Modified by a Match
    The qr/˙˙˙/ Operator and Regex Objects
        Building and Using Regex Objects
        Viewing Regex Objects
        Using Regex Objects for Efficiency
    The Match Operator
        Match’s Regex Operand
        Specifying the Match Target Operand
        Different Uses of the Match Operator
        Iterative Matching: Scalar Context, with /g
        The Match Operator’s Environmental Relations
    The Substitution Operator
        The Replacement Operand
        The /e Modifier
        Context and Return Value
    The Split Operator
        Basic Split
        Returning Empty Elements
        Split’s Special Regex Operands
        Split’s Match Operand with Capturing Parentheses
    Fun with Perl Enhancements
        Using a Dynamic Regex to Match Nested Pairs
        Using the Embedded-Code Construct
        Using local in an Embedded-Code Construct
        A Warning About Embedded Code and my Variables
        Matching Nested Constructs with Embedded Code
        Overloading Regex Literals
        Problems with Regex-Literal Overloading
        Mimicking Named Capture
    Perl Efficiency Issues
        “Ther e’s Mor e Than One Way to Do It”
        Regex Compilation, the /o Modifier, qr/˙˙˙/, and Efficiency
        Understanding the “Pre-Match” Copy
        The Study Function
        Benchmarking
        Regex Debugging Information
    Final Comments
8 Java
    Java’s Regex Flavor
        Java Support for \p{˙˙˙} and \P{˙˙˙}
        Unicode Line Terminators
    Using java.util.regex
    The Pattern.compile() Factory
        Pattern’s matcher method
    The Matcher Object
        Applying the Regex
        Querying Match Results
        Simple Search and Replace
        Advanced Search and Replace
        In-Place Search and Replace
        The Matcher’s Region
        Method Chaining
        Methods for Building a Scanner
        Other Matcher Methods
    Other Pattern Methods
        Pattern’s split Method, with One Argument
        Pattern’s split Method, with Two Arguments
    Additional Examples
        Adding Width and Height Attributes to Image Tags
        Validating HTML with Multiple Patterns Per Matcher
        Parsing Comma-Separated Values (CSV) Text
    Java Version Differences
        Differences Between 1.4.2 and 1.5.0
        Differences Between 1.5.0 and 1.6
9 .NET
    .NET’s Regex Flavor
        Additional Comments on the Flavor
    Using .NET Regular Expressions
        Regex Quickstart
        Package Overview
        Core Object Overview
    Core Object Details
        Creating Regex Objects
        Using Regex Objects
        Using Match Objects
        Using Group Objects
    Static “Convenience” Functions
        Regex Caching
    Support Functions
    Advanced .NET
        Regex Assemblies
        Matching Nested Constructs
        Capture Objects
10 PHP
    PHP’s Regex Flavor
    The Preg Function Interface
        “Pattern” Arguments
    The Preg Functions
        preg_match
        preg_matchRall
        preg_replace
        preg_replaceRcallback
        preg_split
        preg_grep
        preg_quote
    “Missing” Preg Functions
        preg_regex_to_pattern
        Syntax-Checking an Unknown Pattern Argument
        Syntax-Checking an Unknown Regex
    Recursive Expressions
        Matching Text with Nested Parentheses
        No Backtracking Into Recursion
        Matching a Set of Nested Parentheses
    PHP Efficiency Issues
        The S Pattern Modifier: “Study”
    Extended Examples
        CSV Parsing with PHP
        Checking Tagged Data for Proper Nesting
Index

页面: 1 2 3 4 5 6

儒略日(Julian Days)的来历

儒略日是由法国学者 Joseph Justus Scaliger (1540-1609)发明的, 名称可能是取自 Scaliger 的父亲, 意大利学者Julius Caesar Scaliger (1484-1558)。 天文学家已经用儒略周期为自 4713 BC 一月一日以来的每一天赋予了一个唯一的数字。 这就是所谓的儒略日(JD)。 JD 0 指定为 4713 BC 一月一日正午 UTC 到 4713 BC 一月二日正午 UTC 的 24 小时。 阅读更多 »

公历(Gregorian calendar)十二个月的来历

公历一年有12个月,但不少人并不知道12个月的英语名称的来历。公历起源于古罗马历法。罗马的英语原来只有10 个月,古罗马皇帝决定增加两个月放在年尾,后来朱里斯*凯撒大帝把这两个月移到年初,成为1月.2月,原来的1月.2月便成了3月.4月,依次类推。这就是今天世界沿用的公历。 阅读更多 »