原文连接:http://www.cn-cuckoo.com/2007/08/01/understand-javascript-closures-72.htmljavascript
英文原文:http://www.jibbering.com/faq/faq_notes/closures.htmlhtml
要成为高级 JavaScript 程序猿,就必须理解闭包。前端
本文结合 ECMA 262 规范具体解释了闭包的内部工做机制。让 JavaScript 编程人员对闭包的理解从“嵌套的函数”深刻到“标识符解析、执行环境和做用域链”等等 JavaScript 对象背后的执行机制其中。真正领会到闭包的实质。java
闭包是 ECMAScript (JavaScript)最强大的特性之中的一个,但用好闭包的前提是必须理解闭包。闭包的建立相对easy。人们甚至会在不经意间建立闭包,但这些无心建立的闭包却存在潜在的危害。尤为是在比較常见的浏览器环境下。假设想要扬长避短地使用闭包这一特性,则必须了解它们的工做机制。而闭包工做机制的实现很是大程度上有赖于标识符(或者说对象属性)解析过程当中做用域的角色。算法
关于闭包。最简单的描写叙述就是 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
。而且。尽管 MyObject1
和 MyObject2
都未定义toString
方法。但是当属性訪问器经过 objectRef
读取 toString
属性的值时:
var val = objectRef.toString;
变量 val
也会被赋予一个函数的引用。这个函数就是在 Object.prototype
的 toString
属性中所保存的函数。
之因此会返回这个函数,是因为发生了搜索 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
对象,这是一个相似数组的对象。它以整数索引的数组成员一一相应地保存着调用函数时所传递的參数。这个对象也有 length
和 callee
属性(这两个属性与咱们讨论的内容无关。详见规范)。而后,会为活动对象建立一个名为“arguments”的属性。该属性引用前面建立的 arguments
对象。
接着,为运行环境分配做用域。做用域由对象列表(链)组成。
每个函数对象都有一个内部的 [[scope]]
属性(该属性咱们稍后会具体介绍),这个属性也由对象列表(链)组成。
指定给一个函数调用运行环境的做用域,由该函数对象的 [[scope]]
属性所引用的对象列表(链)组成,同一时候,活动对象被加入到该对象列表的顶部(链的前端)。
以后会发生由 ECMA 262 中所谓“可变”对象完毕的“变量实例化”的过程。仅仅只是此时使用活动对象做为可变对象(这里很是重要,请注意:它们是同一个对象)。此时会将函数的形式參数建立为可变对象的命名属性,假设调用函数时传递的參数与形式參数一致。则将对应參数的值赋给这些命名属性(不然。会给命名属性赋 undefined
值)。对于定义的内部函数,会以其声明时所用名称为可变对象建立同名属性。而对应的内部函数则被建立为函数对象并指定给该属性。变量实例化的最后一步是将在函数内部声明的所有局部变量建立为可变对象的命名属性。
依据声明的局部变量建立的可变对象的属性在变量实例化过程当中会被赋予 undefined
值。
在运行函数体内的代码、并计算对应的赋值表达式以前不会对局部变量运行真正的实例化。
其实。拥有 arguments
属性的活动对象和拥有与函数局部变量相应的命名属性的可变对象是同一个对象。
所以,可以将标识符arguments
做为函数的局部变量来看待。
最后。要为使用 this
keyword而赋值。
假设所赋的值引用一个对象,那么前缀以 this
keyword的属性訪问器就是引用该对象的属性。
假设所赋(内部)值是 null,那么 this
keyword则引用全局对象。
建立全局运行环境的过程会稍有不一样,因为它没有參数,因此不需要经过定义的活动对象来引用这些參数。但全局运行环境也需要一个做用域,而它的做用域链实际上仅仅由一个对象--全局对象--组成。全局运行环境也会有变量实例化的过程。它的内部函数就是涉及大部分 JavaScript 代码的、常规的顶级函数声明。
而且,在变量实例化过程当中全局对象就是可变对象,这就是为何全局性声明的函数是全局对象属性的缘由。全局性声明的变量相同如此。
全局运行环境也会使用 this
对象来引用全局对象。
调用函数时建立的运行环境会包括一个做用域链,这个做用域链是经过将该运行环境的活动(可变)对象加入到保存于所调用函数对象的[[scope]]
属性中的做用域链前端而构成的。因此。理解函数对象内部的 [[scope]]
属性的定义过程相当重要。
在 ECMAScript 中。函数也是对象。函数对象在变量实例化过程当中会依据函数声明来建立,或者是在计算函数表达式或调用 Function
构造函数时建立。
经过调用 Function
构造函数建立的函数对象。其内部的 [[scope]]
属性引用的做用域链中始终仅仅包括全局对象。
经过函数声明或函数表达式建立的函数对象,其内部的 [[scope]]
属性引用的则是建立它们的运行环境的做用域链。
在最简单的状况下,比方声明例如如下全局函数:-
function exampleFunction(formalParameter){
… // 函数体内的代码
}
- 当为建立全局运行环境而进行变量实例化时。会依据上面的函数声明建立对应的函数对象。因为全局运行环境的做用域链中仅仅包括全局对象,因此它就给本身建立的、并以名为“exampleFunction”的属性引用的这个函数对象的内部 [[scope]]
属性,赋予了仅仅包括全局对象的做用域链。
当在全局环境中计算函数表达式时,也会发生相似的指定做用域链的过程:-
var exampleFuncRef = function(){
… // 函数体代码
}
在这样的状况下。不一样的是在全局运行环境的变量实例化过程当中。会先为全局对象建立一个命名属性。而在计算赋值语句以前,临时不会建立函数对象。也不会将该函数对象的引用指定给全局对象的命名属性。
但是。终于仍是会在全局运行环境中建立这个函数对象(当计算函数表达式时。译者注),而为这个建立的函数对象的 [[scope]]
属性指定的做用域链中仍然仅仅包括全局对象。内部的函数声明或表达式会致使在包括它们的外部函数的运行环境中建立对应的函数对象。所以这些函数对象的做用域链会略微复杂一些。在如下的代码中,先定义了一个带有内部函数声明的外部函数。而后调用外部函数:
/* 建立全局变量 - 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
。
闭包可以用于建立额外的做用域,经过该做用域可以将相关的和具备依赖性的代码组织起来。以便将意外交互的风险降到最低。若是有一个用于构建字符串的函数,为了不反复性的链接操做(和建立众多的中间字符串),咱们的愿望是使用一个数组按顺序来存储字符串的各个部分,而后再使用 Array.prototype.join
方法(以空字符串做为其參数)输出结果。这个数组将做为输出的缓冲器,但是将数组做为函数的局部变量又会致使在每次调用函数时都又一次建立一个新数组,这在每次调用函数时仅仅又一次指定数组中的可变内容的状况下并不是必要的。
一种解决方式是将这个数组声明为全局变量,这样就可以重用这个数组,而没必要每次都创建新数组。
但这个方法的结果是,除了引用函数的全局变量会使用这个缓冲数组外,还会多出一个全局属性引用数组自身。如此不只使代码变得不easy管理。而且,假设要在其它地方使用这个数组时,开发人员必须要再次定义函数和数组。
这样一来,也使得代码不easy与其它代码整合,因为此时不只要保证所使用的函数名在全局命名空间中是惟一的,而且还要保证函数所依赖的数组在全局命名空间中也必须是惟一的。
而经过闭包可使做为缓冲器的数组与依赖它的函数关联起来(优雅地打包),同一时候也能够维持在全局命名空间外指定的缓冲数组的属性名,免除了名称冲突和意外交互的危急。
当中的关键技巧在于经过运行一个单行(in-line)函数表达式建立一个额外的运行环境,而将该函数表达式返回的内部函数做为在外部代码中使用的函数。此时,缓冲数组被定义为函数表达式的一个局部变量。
这个函数表达式仅仅需运行一次,而数组也仅仅需建立一次,就可以供依赖它的函数反复使用。
如下的代码定义了一个函数,这个函数用于返回一个 HTML 字符串,当中大部份内容都是常量。但这些常量字符序列中需要穿插一些可变的信息。而可变的信息由调用函数时传递的參数提供。
经过运行单行函数表达式返回一个内部函数,并将返回的函数赋给一个全局变量,所以这个函数也可以称为全局函数。
而缓冲数组被定义为外部函数表达式的一个局部变量。它不会暴露在全局命名空间中。而且无论何时调用依赖它的函数都不需要又一次建立这个数组。
/* 声明一个全局变量 - getImgInPositionedDivHtml - 并将一次调用一个外部函数表达式返回的内部函数赋给它。 这个内部函数会返回一个用于表示绝对定位的 DIV 元素 包围着一个 IMG 元素 的 HTML 字符串,这样一来, 所有可变的属性值都由调用该函数时的參数提供: */ var getImgInPositionedDivHtml = (function(){ /* 外部函数表达式的局部变量 - buffAr - 保存着缓冲数组。 这个数组仅仅会被建立一次,生成的数组实例对内部函数而言永远是可用的 所以,可供每次调用这个内部函数时使用。 当中的空字符串用做数据占位符,对应的数据 将由内部函数插入到这个数组中: */ var buffAr = [ '<div id="', '', //index 1, DIV ID 属性 '" style="position:absolute;top:', '', //index 3, DIV 顶部位置 'px;left:', '', //index 5, DIV 左端位置 'px;width:', '', //index 7, DIV 宽度 'px;height:', '', //index 9, DIV 高度 'px;overflow:hidden;\"><img src=\"', '', //index 11, IMG URL '\" width=\"', '', //index 13, IMG 宽度 '\" height=\"', '', //index 15, IMG 高度 '\" alt=\"', '', //index 17, IMG alt 文本内容 '\"></div>' ]; /* 返回做为对函数表达式求值后结果的内部函数对象。 这个内部函数就是每次调用运行的函数 - getImgInPositionedDivHtml( ... ) - */ return (function(url, id, width, height, top, left, altText){ /* 将不一样的參数插入到缓冲数组对应的位置:*/ 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 buffAr.join(''); }); //:内部函数表达式结束。 })(); /*^^- :单行外部函数表达式。*/
假设一个函数依赖于还有一(或多)个其它函数,而其它函数又没有必要被其它代码直接调用。那么可以运用一样的技术来包装这些函数,而经过一个公开暴露的函数来调用它们。
这样。就将一个复杂的多函数处理过程封装成了一个具备移植性的代码单元。
有关闭包的一个多是最广为人知的应用是 Douglas Crockford’s technique for the emulation of private instance variables in ECMAScript objects。
这样的应用方式可以扩展到各类嵌套包括的可訪问性(或可见性)的做用域结构,包括 the emulation of private static members for ECMAScript objects。
闭包可能的用途是无限的。可能理解其工做原理才是把握怎样使用它的最好指南。
在建立可訪问的内部函数的函数体以外解析该内部函数就会构成闭包。这代表闭包很是easy建立,但这样一来可能会致使一种结果。即没有认识到闭包是一种语言特性的 JavaScript 做者,会依照内部函数能完毕多种任务的想法来使用内部函数。
但他们对使用内部函数的结果并不明了,而且根本意识不到建立了闭包。或者那样作意味着什么。
正例如如下一节谈到 IE 中内存泄漏问题时所说起的,意外建立的闭包可能致使严重的负面效应,而且也会影响到代码的性能。问题不在于闭包自己,假设能够真正作到慎重地使用它们,反而会有助于建立高效的代码。换句话说。使用内部函数会影响到效率。
使用内部函数最多见的一种状况就是将其做为 DOM 元素的事件处理器。
好比,如下的代码用于向一个连接元素加入 onclick 事件处理器:
/* 定义一个全局变量,经过如下的函数将它的值 做为查询字符串的一部分加入到连接的 - href - 中: */ var quantaty = 5; /* 当给这个函数传递一个连接(做为函数中的參数 - linkRef -)时。 会将一个 onclick 事件处理器指定给该连接,该事件处理器 将全局变量 - quantaty - 的值做为字符串加入到连接的 - href - 属性中。而后返回 true 使该连接在单击后定位到由 - href - 属性包括的查询字符串指定的资源: */ function addGlobalQueryOnClick(linkRef){ /* 假设可以将參数 - linkRef - 经过类型转换为 ture (说明它引用了一个对象): */ if(linkRef){ /* 对一个函数表达式求值,并将对该函数对象的引用 指定给这个连接元素的 onclick 事件处理器: */ linkRef.onclick = function(){ /* 这个内部函数表达式将查询字符串 加入到附加事件处理器的元素的 - href - 属性中: */ this.href += ('?quantaty='+escape(quantaty)); return true; }; } }
无论何时调用 addGlobalQueryOnClick
函数。都会建立一个新的内部函数(经过赋值构成了闭包)。
从效率的角度上看,假设仅仅是调用一两次 addGlobalQueryOnClick
函数并无什么大的妨碍。但假设频繁使用该函数,就会致使建立不少大相径庭的函数对象(每对内部函数表达式求一次值。就会产生一个新的函数对象)。
上面样例中的代码没有关注内部函数在建立它的函数外部可以訪问(或者说构成了闭包)这一事实。实际上。相同的效果可以经过还有一种方式来完毕。即单独地定义一个用于事件处理器的函数。而后将该函数的引用指定给元素的事件处理属性。这样,仅仅需建立一个函数对象。而所有使用相同事件处理器的元素都可以共享对这个函数的引用:
/* 定义一个全局变量,经过如下的函数将它的值 做为查询字符串的一部分加入到连接的 - href - 中: */ var quantaty = 5; /* 当把一个连接(做为函数中的參数 - linkRef -)传递给这个函数时。 会给这个连接加入一个 onclick 事件处理器,该事件处理器会 将全局变量 - quantaty - 的值做为查询字符串的一部分加入到 连接的 - href - 中,而后返回 true,以便单击连接时定位到由 做为 - href - 属性值的查询字符串所指定的资源: */ function addGlobalQueryOnClick(linkRef){ /* 假设 - linkRef - 參数能够经过类型转换为 true (说明它引用了一个对象): */ if(linkRef){ /* 将一个对全局函数的引用指定给这个连接 的事件处理属性。使函数成为连接元素的事件处理器: */ linkRef.onclick = forAddQueryOnClick; } } /* 声明一个全局函数,做为连接元素的事件处理器, 这个函数将一个全局变量的值做为要加入事件处理器的 连接元素的 - href - 值的一部分: */ function forAddQueryOnClick(){ this.href += ('?quantaty='+escape(quantaty)); return true; }
在上面样例的第一个版本号中。内部函数并无做为闭包发挥应有的做用。在那种状况下,反而是不使用闭包更有效率,因为不用反复建立不少本质上一样的函数对象。
相似地考量相同适用于对象的构造函数。与如下代码中的构造函数框架相似的代码并不罕见:
function ExampleConst(param){ /* 经过对函数表达式求值建立对象的方法, 并将求值所得的函数对象的引用赋给要建立对象的属性: */ this.method1 = function(){ ... // 方法体。 }; this.method2 = function(){ ... // 方法体。 }; this.method3 = function(){ ... // 方法体。
}; /* 把构造函数的參数赋给对象的一个属性:*/ this.publicProp = param; }
每当经过 new ExampleConst(n)
使用这个构造函数建立一个对象时,都会建立一组新的、做为对象方法的函数对象。所以。建立的对象实例越多,对应的函数对象也就越多。
Douglas Crockford 提出的模仿 JavaScript 对象私有成员的技术。就利用了将对内部函数的引用指定给在构造函数中构造对象的公共属性而造成的闭包。
假设对象的方法没有利用在构造函数中造成的闭包,那么在实例化每个对象时建立的多个函数对象。会使实例化过程变慢。而且将有不少其它的资源被占用。以知足建立不少其它函数对象的需要。
这那种状况下,仅仅建立一次函数对象,并把它们指定给构造函数 prototype
的对应属性显然更有效率。
这样一来。它们就能被构造函数建立的所有对象共享了:
function ExampleConst(param){ /* 将构造函数的參数赋给对象的一个属性:*/ this.publicProp = param; } /* 经过对函数表达式求值。并将结果函数对象的引用 指定给构造函数原型的对应属性来建立对象的方法: */ ExampleConst.prototype.method1 = function(){ ... // 方法体。 }; ExampleConst.prototype.method2 = function(){ ... // 方法体。 }; ExampleConst.prototype.method3 = function(){ ... // 方法体。 };
Internet Explorer Web 浏览器(在 IE 4 到 IE 6 中核实)的垃圾收集系统中存在一个问题,即假设 ECMAScript 和某些宿主对象构成了 “循环引用”。那么这些对象将不会被看成垃圾收集。此时所谓的宿主对象指的是不论什么 DOM 节点(包括 document 对象及其后代元素)和 ActiveX 对象。假设在一个循环引用中包括了一或多个这种对象,那么这些对象直到浏览器关闭都不会被释放。而它们所占用的内存相同在浏览器关闭以前都不会交回系统重用。
当两个或多个对象以首尾相连的方式相互引用时,就构成了循环引用。
比方对象 1 的一个属性引用了对象 2 ,对象 2 的一个属性引用了对象 3,而对象 3 的一个属性又引用了对象 1。对于纯粹的 ECMAScript 对象而言,仅仅要没有其它对象引用对象 一、二、3。也就是说它们仅仅是相互之间的引用,那么仍然会被垃圾收集系统识别并处理。但是。在 Internet Explorer 中。假设循环引用中的不论什么对象是 DOM 节点或者 ActiveX 对象,垃圾收集系统则不会发现它们之间的循环关系与系统中的其它对象是隔离的并释放它们。终于它们将被保留在内存中,直到浏览器关闭。
闭包很easy构成循环引用。
假设一个构成闭包的函数对象被指定给,比方一个 DOM 节点的事件处理器,而对该节点的引用又被指定给函数对象做用域中的一个活动(或可变)对象,那么就存在一个循环引用。DOM_Node.onevent ->function_object.[[scope]] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。造成这样一个循环引用是垂手可得的。而且略微浏览一下包括相似循环引用代码的站点(通常会出现在站点的每个页面中),就会消耗大量(甚至全部)系统内存。
多加注意可以避免造成循环引用,而在没法避免时,也可以使用补偿的方法。比方使用 IE 的 onunload 事件来来清空(null)事件处理函数的引用。时刻意识到这个问题并理解闭包的工做机制是在 IE 中避免此类问题的关键。
【注】本文内容讲的很是深刻,贴出来,一是本身学习以备后查,二是分享给你们,回想一下基础的知识。
顺便多想一下。用了这么长时间的javascript,你真的了解吗?